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();
         }

Reply via email to