This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25554 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 111b1e387bd21010f242898adfa6457ff5129d08 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Oct 12 22:31:21 2026 +0200 CAMEL-25554: camel-core - route reload creates the routes from templatedRoute again Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../main/java/org/apache/camel/RoutesBuilder.java | 17 ++ .../camel/impl/engine/DefaultRoutesLoader.java | 25 ++- .../org/apache/camel/builder/RouteBuilder.java | 31 ++++ .../RouteReloadOnDemandTemplatedRouteTest.java | 192 +++++++++++++++++++++ .../dsl/yaml/RouteReloadTemplatedRouteTest.java | 3 + 5 files changed, 259 insertions(+), 9 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..5fc1dbf1de22 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; /** @@ -59,4 +60,20 @@ public interface RoutesBuilder { */ 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 was 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(); }
