This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 1d86b6a03e10 CAMEL-25554: camel-core - route reload creates the routes
from templatedRoute again (#27745)
1d86b6a03e10 is described below
commit 1d86b6a03e109e6f66a423e526137e7c880f03cd
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 13 09:06:08 2026 +0200
CAMEL-25554: camel-core - route reload creates the routes from
templatedRoute again (#27745)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../main/java/org/apache/camel/RoutesBuilder.java | 19 +-
.../camel/impl/engine/DefaultRoutesLoader.java | 25 ++-
.../org/apache/camel/builder/RouteBuilder.java | 31 ++++
.../RouteReloadOnDemandTemplatedRouteTest.java | 192 +++++++++++++++++++++
.../dsl/yaml/RouteReloadTemplatedRouteTest.java | 3 +
5 files changed, 260 insertions(+), 10 deletions(-)
diff --git a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
b/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
index ce44150d794a..775f8edf4c31 100644
--- a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
+++ b/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel;
+import java.util.Collections;
import java.util.Set;
/**
@@ -54,9 +55,25 @@ public interface RoutesBuilder {
* Adds or updates the routes from this Route Builder to the CamelContext.
*
* @param context the Camel context
- * @return route ids for the routes that was updated
+ * @return route ids for the routes that were updated
* @throws Exception is thrown if initialization of routes failed
*/
Set<String> updateRoutesToCamelContext(CamelContext context) throws
Exception;
+ /**
+ * Adds or updates the templated routes from this Route Builder to the
CamelContext.
+ * <p/>
+ * This is called after the routes (and route templates) of all the Route
Builders that are updated together have
+ * been updated, as a templated route may use a route template from
another Route Builder.
+ *
+ * @param context the Camel context
+ * @return route ids for the routes that were created from the
templated routes
+ * @throws Exception is thrown if initialization of routes failed
+ * @since 4.24
+ */
+ default Set<String> updateTemplatedRoutesToCamelContext(CamelContext
context) throws Exception {
+ addTemplatedRoutesToCamelContext(context);
+ return Collections.emptySet();
+ }
+
}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoutesLoader.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoutesLoader.java
index 2b523d3a6528..8432804b0cab 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoutesLoader.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoutesLoader.java
@@ -318,17 +318,24 @@ public class DefaultRoutesLoader extends ServiceSupport
implements RoutesLoader,
}
for (RoutesBuilder builder : builders) {
// update any existing routes
- Set<String> ids =
builder.updateRoutesToCamelContext(getCamelContext());
- // check we do not have duplicate route ids in this group of
resources
- for (String id : ids) {
- if (answer.contains(id)) {
- throw new FailedToStartRouteException(
- id,
- "duplicate route id detected " + id + ". Please
correct ids to be unique among all your routes.");
- }
+ addUpdatedRouteIds(answer,
builder.updateRoutesToCamelContext(getCamelContext()));
+ }
+ for (RoutesBuilder builder : builders) {
+ // templated routes last, as they may use a route template from
another resource (same as when loading)
+ addUpdatedRouteIds(answer,
builder.updateTemplatedRoutesToCamelContext(getCamelContext()));
+ }
+ }
+
+ private static void addUpdatedRouteIds(Set<String> answer, Set<String>
ids) throws FailedToStartRouteException {
+ // check we do not have duplicate route ids in this group of resources
+ for (String id : ids) {
+ if (answer.contains(id)) {
+ throw new FailedToStartRouteException(
+ id,
+ "duplicate route id detected " + id + ". Please
correct ids to be unique among all your routes.");
}
- answer.addAll(ids);
}
+ answer.addAll(ids);
}
protected RoutesBuilderLoader resolveRoutesBuilderLoader(Resource
resource, boolean optional) throws Exception {
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java
b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java
index 7110c7642a6f..a3c90867d58b 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java
@@ -18,7 +18,9 @@ package org.apache.camel.builder;
import java.io.Reader;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Comparator;
+import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
@@ -823,6 +825,35 @@ public abstract class RouteBuilder extends BuilderSupport
implements RoutesBuild
return answer;
}
+ @Override
+ public Set<String> updateTemplatedRoutesToCamelContext(CamelContext
context) throws Exception {
+ Set<String> answer = new LinkedHashSet<>();
+ if (getTemplatedRouteCollection().getTemplatedRoutes().isEmpty()) {
+ return answer;
+ }
+
+ // a route created from a templated route replaces an existing route
with the same id, so the routes created
+ // are those whose definition was not in the model before
+ Model model =
context.getCamelContextExtension().getContextPlugin(Model.class);
+ Set<RouteDefinition> before = Collections.newSetFromMap(new
IdentityHashMap<>());
+ before.addAll(model.getRouteDefinitions());
+
+ populateTemplatedRoutes(context);
+
+ for (RouteDefinition route : model.getRouteDefinitions()) {
+ if (!before.contains(route) && route.getRouteId() != null) {
+ answer.add(route.getRouteId());
+ }
+ }
+
+ // the routes created must be started, same as in
updateRoutesToCamelContext
+ if (context.getRouteController() instanceof SupervisingRouteController
src) {
+ src.startRoutes(true);
+ }
+
+ return answer;
+ }
+
/**
* Configures the routes
*
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadOnDemandTemplatedRouteTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadOnDemandTemplatedRouteTest.java
new file mode 100644
index 000000000000..782e62d0ebcc
--- /dev/null
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadOnDemandTemplatedRouteTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.yaml;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Set;
+
+import org.apache.camel.ServiceStatus;
+import org.apache.camel.dsl.yaml.support.YamlTestSupport;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.RouteOnDemandReloadStrategy;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25554: a reload that removes all routes creates the routes from
templatedRoute entries again, also when the
+ * route template is in another file.
+ */
+class RouteReloadOnDemandTemplatedRouteTest extends YamlTestSupport {
+
+ /** Subclass to expose protected lifecycle methods for testing. */
+ static class TestableStrategy extends RouteOnDemandReloadStrategy {
+ TestableStrategy(String dir) {
+ super(dir);
+ }
+
+ @Override
+ public void doStart() throws Exception {
+ super.doStart();
+ }
+
+ @Override
+ public void doStop() throws Exception {
+ super.doStop();
+ }
+ }
+
+ private Path dir;
+
+ @Override
+ public void doSetup() throws Exception {
+ dir = Files.createTempDirectory("camel-reload-templated");
+ context.start();
+ }
+
+ @Override
+ public void doCleanup() throws Exception {
+ deleteRecursively(dir);
+ }
+
+ @Test
+ void onDemandReloadCreatesTheTemplatedRoutesAgain() throws Exception {
+ Path file = dir.resolve("routes.camel.yaml");
+ Files.writeString(file, template() + templatedRoutes("Hello") +
plainRoute("plain"));
+ loadRoutes(ResourceHelper.resolveResource(context, "file:" + file));
+ assertThat(context.getRoutes()).hasSize(3);
+
+ TestableStrategy strategy = new TestableStrategy(dir.toString());
+ strategy.setCamelContext(context);
+ strategy.setPattern("*.yaml");
+ strategy.doStart();
+ try {
+ Files.writeString(file, template() + templatedRoutes("Hi") +
plainRoute("plain changed"));
+ strategy.onReload("test");
+
+ assertThat(strategy.getLastError()).isNull();
+ assertThat(context.getRoutes()).hasSize(3);
+ assertRoute("plain", "direct:plain", "plain changed");
+ assertRoute("greet-a", "direct:a", "Hi a");
+ assertRoute("greet-b", "direct:b", "Hi b");
+ } finally {
+ strategy.doStop();
+ }
+ }
+
+ @Test
+ void updateRoutesWithTheTemplateInAFileAfterTheTemplatedRoutes() throws
Exception {
+ Path uses = dir.resolve("a-uses.camel.yaml");
+ Path templates = dir.resolve("b-templates.camel.yaml");
+ Files.writeString(uses, templatedRoutes("Hello") +
plainRoute("plain"));
+ Files.writeString(templates, template());
+ Resource usesResource = ResourceHelper.resolveResource(context,
"file:" + uses);
+ Resource templatesResource = ResourceHelper.resolveResource(context,
"file:" + templates);
+ loadRoutes(usesResource, templatesResource);
+ assertThat(context.getRoutes()).hasSize(3);
+
+ // what a reload that removes all routes does: the file with the
templated routes comes first, and its
+ // template is only known once the other file is loaded
+ context.getRouteController().removeAllRoutes();
+ context.removeRouteTemplates("*");
+ Set<String> ids =
PluginHelper.getRoutesLoader(context).updateRoutes(usesResource,
templatesResource);
+
+ assertThat(ids).containsExactlyInAnyOrder("plain", "greet-a",
"greet-b");
+ assertRoute("plain", "direct:plain", "plain");
+ assertRoute("greet-a", "direct:a", "Hello a");
+ assertRoute("greet-b", "direct:b", "Hello b");
+ }
+
+ private void assertRoute(String routeId, String uri, String expected) {
+
assertThat(context.getRouteController().getRouteStatus(routeId)).isEqualTo(ServiceStatus.Started);
+
assertThat(context.createFluentProducerTemplate().to(uri).request(String.class)).isEqualTo(expected);
+ }
+
+ private static String template() {
+ return """
+ - routeTemplate:
+ id: "greeting"
+ parameters:
+ - name: "name"
+ - name: "greeting"
+ from:
+ uri: "direct:{{name}}"
+ steps:
+ - setBody:
+ simple: "{{greeting}} {{name}}"
+ """;
+ }
+
+ private static String templatedRoutes(String greeting) {
+ return """
+ - templatedRoute:
+ routeTemplateRef: "greeting"
+ routeId: "greet-a"
+ parameters:
+ - name: "name"
+ value: "a"
+ - name: "greeting"
+ value: "%s"
+ - templatedRoute:
+ routeTemplateRef: "greeting"
+ routeId: "greet-b"
+ parameters:
+ - name: "name"
+ value: "b"
+ - name: "greeting"
+ value: "%s"
+ """.formatted(greeting, greeting);
+ }
+
+ private static String plainRoute(String body) {
+ return """
+ - route:
+ id: plain
+ from:
+ uri: direct:plain
+ steps:
+ - setBody:
+ constant: "%s"
+ """.formatted(body);
+ }
+
+ private static void deleteRecursively(Path path) throws IOException {
+ if (path == null || !Files.exists(path)) {
+ return;
+ }
+ Files.walkFileTree(path, new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes
attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException
exc) throws IOException {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+}
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadTemplatedRouteTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadTemplatedRouteTest.java
index a2aa2993be69..2913ba7a8d39 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadTemplatedRouteTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadTemplatedRouteTest.java
@@ -91,6 +91,9 @@ class RouteReloadTemplatedRouteTest extends YamlTestSupport {
assertThat(context.getRouteController().getRouteStatus("plain")).isEqualTo(ServiceStatus.Started);
assertThat(context.createFluentProducerTemplate().to("direct:plain").request(String.class))
.isEqualTo("plain Hi");
+ // the templated route is created again with the changed parameter
(CAMEL-25554)
+
assertThat(context.createFluentProducerTemplate().to("direct:greet").request(String.class))
+ .isEqualTo("Hi");
} finally {
strategy.doStop();
}