This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-25598
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 5ba69e5f6a4f354d00235eea9e8f3d1ee617701d
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 23:07:57 2026 +0200

    CAMEL-25598: camel-core - routes created from route templates record their 
source files for reload and tooling
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/dev-consoles-openapi.json |   8 +
 .../apache/camel/catalog/dev-consoles/route.json   |   8 +
 .../src/main/java/org/apache/camel/NamedRoute.java |  23 +++
 .../src/main/java/org/apache/camel/Route.java      |  28 +++-
 .../org/apache/camel/impl/engine/DefaultRoute.java |  17 +++
 .../org/apache/camel/dev-console/route.json        |   8 +
 .../apache/camel/impl/console/RouteDevConsole.java |  12 +-
 .../console/RouteDevConsoleTemplatedRouteTest.java |  90 +++++++++++
 .../java/org/apache/camel/impl/DefaultModel.java   |  21 ++-
 .../org/apache/camel/builder/RouteBuilder.java     |  12 ++
 .../org/apache/camel/model/RouteDefinition.java    |  17 +++
 .../api/management/mbean/ManagedRouteMBean.java    |   6 +
 .../camel/management/mbean/ManagedRoute.java       |  12 ++
 .../ManagedRouteTemplatedRouteSourceTest.java      |  70 +++++++++
 .../camel/support/RouteWatcherReloadStrategy.java  |  20 ++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  14 ++
 .../yaml/RouteReloadTemplatedRouteSourceTest.java  | 170 +++++++++++++++++++++
 17 files changed, 527 insertions(+), 9 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index be81daa9d834..6ce3ee396a8e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -5792,6 +5792,10 @@
                                                                                
                                "type": "boolean",
                                                                                
                                "description": "Whether the route was created 
by a route template"
                                                                                
                        },
+                                                                               
                        "routeTemplateId": {
+                                                                               
                                "type": "string",
+                                                                               
                                "description": "The id of the route template 
(or Kamelet) the route was created from (only present when known)"
+                                                                               
                        },
                                                                                
                        "from": {
                                                                                
                                "type": "string",
                                                                                
                                "description": "The route's endpoint URI"
@@ -5804,6 +5808,10 @@
                                                                                
                                "type": "string",
                                                                                
                                "description": "The source location (only 
present when known)"
                                                                                
                        },
+                                                                               
                        "templatedRouteSource": {
+                                                                               
                                "type": "string",
+                                                                               
                                "description": "The source that declares the 
templated route the route was created from (only present when known)"
+                                                                               
                        },
                                                                                
                        "state": {
                                                                                
                                "type": "string",
                                                                                
                                "description": "The route state"
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
index 179a07bc622d..b253d33122e1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route.json
@@ -113,6 +113,10 @@
               "type": "boolean",
               "description": "Whether the route was created by a route 
template"
             },
+            "routeTemplateId": {
+              "type": "string",
+              "description": "The id of the route template (or Kamelet) the 
route was created from (only present when known)"
+            },
             "from": {
               "type": "string",
               "description": "The route's endpoint URI"
@@ -125,6 +129,10 @@
               "type": "string",
               "description": "The source location (only present when known)"
             },
+            "templatedRouteSource": {
+              "type": "string",
+              "description": "The source that declares the templated route the 
route was created from (only present when known)"
+            },
             "state": {
               "type": "string",
               "description": "The route state"
diff --git a/core/camel-api/src/main/java/org/apache/camel/NamedRoute.java 
b/core/camel-api/src/main/java/org/apache/camel/NamedRoute.java
index f03a4cd04453..b2ec7e1058fc 100644
--- a/core/camel-api/src/main/java/org/apache/camel/NamedRoute.java
+++ b/core/camel-api/src/main/java/org/apache/camel/NamedRoute.java
@@ -77,4 +77,27 @@ public interface NamedRoute {
      */
     Resource getResource();
 
+    /**
+     * Gets the id of the route template (or Kamelet) this route was created 
from.
+     *
+     * @return the route template id, or null if this route was not created 
from a route template
+     * @since  4.24
+     */
+    @Nullable
+    default String getRouteTemplateId() {
+        return null;
+    }
+
+    /**
+     * Gets the {@link Resource} that declares the templated route this route 
was created from. The route itself (and
+     * its source location) is in the resource with the route template, see 
{@link #getResource()}.
+     *
+     * @return the resource, or null if this route was not created from a 
templated route loaded from a resource
+     * @since  4.24
+     */
+    @Nullable
+    default Resource getTemplatedRouteResource() {
+        return null;
+    }
+
 }
diff --git a/core/camel-api/src/main/java/org/apache/camel/Route.java 
b/core/camel-api/src/main/java/org/apache/camel/Route.java
index af6562c2adb5..8c51ca213638 100644
--- a/core/camel-api/src/main/java/org/apache/camel/Route.java
+++ b/core/camel-api/src/main/java/org/apache/camel/Route.java
@@ -185,13 +185,39 @@ public interface Route extends RuntimeConfiguration {
     String getConfigurationId();
 
     /**
-     * Gets the source resource that this route is located from
+     * Gets the source resource that this route is located from. For a route 
created from a route template (or a
+     * Kamelet) this is the resource with the route template.
      *
      * @return the source, or null if this route is not loaded from a resource
      */
     @Nullable
     Resource getSourceResource();
 
+    /**
+     * Gets the id of the route template (or Kamelet) this route was created 
from.
+     *
+     * @return the route template id, or null if this route was not created 
from a route template
+     * @since  4.24
+     */
+    @Nullable
+    default String getRouteTemplateId() {
+        return null;
+    }
+
+    /**
+     * Gets the resource that declares the templated route this route was 
created from. The source resource of the route
+     * is the resource with the route template (where its source location is).
+     * <p/>
+     * Tools that load routes again from their resources, such as route 
reloading in development mode, need both.
+     *
+     * @return the resource, or null if this route was not created from a 
templated route loaded from a resource
+     * @since  4.24
+     */
+    @Nullable
+    default Resource getTemplatedRouteResource() {
+        return null;
+    }
+
     /**
      * The source:line-number where the route input is located in the source 
code
      */
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java
index 0f7dcf35fd7d..8ef3ff027ac3 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java
@@ -32,6 +32,7 @@ import org.apache.camel.Consumer;
 import org.apache.camel.Endpoint;
 import org.apache.camel.ErrorHandlerFactory;
 import org.apache.camel.NamedNode;
+import org.apache.camel.NamedRoute;
 import org.apache.camel.Navigate;
 import org.apache.camel.Processor;
 import org.apache.camel.Route;
@@ -80,6 +81,8 @@ public class DefaultRoute extends ServiceSupport implements 
Route {
     private final Resource sourceResource;
     private final String sourceLocation;
     private final String sourceLocationShort;
+    private final String routeTemplateId;
+    private final Resource templatedRouteResource;
     private final List<Processor> eventDrivenProcessors = new ArrayList<>();
     private final List<InterceptStrategy> interceptStrategies = new 
ArrayList<>(0);
     private ManagementInterceptStrategy managementInterceptStrategy;
@@ -132,6 +135,10 @@ public class DefaultRoute extends ServiceSupport 
implements Route {
         this.sourceResource = resource;
         this.sourceLocation = LoggerHelper.getSourceLocation(route);
         this.sourceLocationShort = LoggerHelper.getLineNumberLoggerName(route);
+        // keep these as the route model is cleared once the route is created
+        NamedRoute namedRoute = route instanceof NamedRoute nr ? nr : null;
+        this.routeTemplateId = namedRoute != null ? 
namedRoute.getRouteTemplateId() : null;
+        this.templatedRouteResource = namedRoute != null ? 
namedRoute.getTemplatedRouteResource() : null;
     }
 
     @Override
@@ -221,6 +228,16 @@ public class DefaultRoute extends ServiceSupport 
implements Route {
         return sourceResource;
     }
 
+    @Override
+    public String getRouteTemplateId() {
+        return routeTemplateId;
+    }
+
+    @Override
+    public Resource getTemplatedRouteResource() {
+        return templatedRouteResource;
+    }
+
     @Override
     public String getSourceLocation() {
         return sourceLocation;
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
index 179a07bc622d..b253d33122e1 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route.json
@@ -113,6 +113,10 @@
               "type": "boolean",
               "description": "Whether the route was created by a route 
template"
             },
+            "routeTemplateId": {
+              "type": "string",
+              "description": "The id of the route template (or Kamelet) the 
route was created from (only present when known)"
+            },
             "from": {
               "type": "string",
               "description": "The route's endpoint URI"
@@ -125,6 +129,10 @@
               "type": "string",
               "description": "The source location (only present when known)"
             },
+            "templatedRouteSource": {
+              "type": "string",
+              "description": "The source that declares the templated route the 
route was created from (only present when known)"
+            },
             "state": {
               "type": "string",
               "description": "The route state"
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
index e772e77b52a4..50fba785f6f9 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDevConsole.java
@@ -89,9 +89,11 @@ public class RouteDevConsole extends AbstractDevConsole {
             @Metadata(description = "The route note (only present when 
configured)") String note,
             @Metadata(description = "Whether the route was created by a 
Kamelet") boolean createdByKamelet,
             @Metadata(description = "Whether the route was created by a route 
template") boolean createdByRouteTemplate,
+            @Metadata(description = "The id of the route template (or Kamelet) 
the route was created from (only present when known)") String routeTemplateId,
             @Metadata(description = "The route's endpoint URI") String from,
             @Metadata(description = "Whether the endpoint is remote") boolean 
remote,
             @Metadata(description = "The source location (only present when 
known)") String source,
+            @Metadata(description = "The source that declares the templated 
route the route was created from (only present when known)") String 
templatedRouteSource,
             @Metadata(description = "The route state") String state,
             @Metadata(description = "Whether the route supports suspension") 
boolean supportsSuspension,
             @Metadata(description = "Route uptime, human readable text") 
String uptime,
@@ -158,11 +160,17 @@ public class RouteDevConsole extends AbstractDevConsole {
             if (mrb.isCreatedByRouteTemplate()) {
                 sb.append(String.format("%n    Created By Route Template: %s", 
true));
             }
+            if (mrb.getRouteTemplateId() != null) {
+                sb.append(String.format("%n    Route Template Id: %s", 
mrb.getRouteTemplateId()));
+            }
             sb.append(String.format("%n    From: %s", mrb.getEndpointUri()));
             sb.append(String.format("%n    Remote: %s", 
mrb.isRemoteEndpoint()));
             if (mrb.getSourceLocation() != null) {
                 sb.append(String.format("%n    Source: %s", 
mrb.getSourceLocation()));
             }
+            if (mrb.getTemplatedRouteSource() != null) {
+                sb.append(String.format("%n    Templated Route Source: %s", 
mrb.getTemplatedRouteSource()));
+            }
             sb.append(String.format("%n    State: %s", mrb.getState()));
             Route r = getCamelContext().getRoute(mrb.getRouteId());
             sb.append(String.format("%n    Supports Suspension: %s", r != null 
&& r.supportsSuspension()));
@@ -304,8 +312,8 @@ public class RouteDevConsole extends AbstractDevConsole {
             Route r = getCamelContext().getRoute(mrb.getRouteId());
             list.add(new RouteEntry(
                     mrb.getRouteId(), mrb.getRouteGroup(), 
mrb.getNodePrefixId(), mrb.getDescription(), mrb.getNote(),
-                    mrb.isCreatedByKamelet(), mrb.isCreatedByRouteTemplate(), 
mrb.getEndpointUri(),
-                    mrb.isRemoteEndpoint(), mrb.getSourceLocation(), 
mrb.getState(),
+                    mrb.isCreatedByKamelet(), mrb.isCreatedByRouteTemplate(), 
mrb.getRouteTemplateId(), mrb.getEndpointUri(),
+                    mrb.isRemoteEndpoint(), mrb.getSourceLocation(), 
mrb.getTemplatedRouteSource(), mrb.getState(),
                     r != null && r.supportsSuspension(), mrb.getUptime(), 
lastError, stats, procList));
             return null;
         };
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteDevConsoleTemplatedRouteTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteDevConsoleTemplatedRouteTest.java
new file mode 100644
index 000000000000..89a7dab4e1d5
--- /dev/null
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteDevConsoleTemplatedRouteTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.impl.console;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25598: the route console shows the route template id and the source 
of the templated route of a route.
+ */
+public class RouteDevConsoleTemplatedRouteTest extends AbstractDevConsoleTest {
+
+    @Override
+    protected boolean useJmx() {
+        return true;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        RouteBuilder builder = new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("greeting").templateParameter("greeting")
+                        .from("direct:greet")
+                        .setBody(constant("{{greeting}}"));
+
+                
templatedRoute("greeting").routeId("greet").parameter("greeting", "Hello");
+
+                from("direct:plain").routeId("plain")
+                        .to("mock:plain");
+            }
+        };
+        builder.setResource(ResourceHelper.fromString("mem:my-routes.java", 
""));
+        return builder;
+    }
+
+    @Test
+    public void testTemplatedRouteJson() {
+        DevConsole console = assertConsoleExists("route");
+
+        JsonObject greet = route(callJson(console, 
Map.of(RouteDevConsole.FILTER, "greet")));
+        assertEquals("greet", greet.getString("routeId"));
+        assertEquals("greeting", greet.getString("routeTemplateId"));
+        assertEquals("mem:my-routes.java", 
greet.getString("templatedRouteSource"));
+
+        JsonObject plain = route(callJson(console, 
Map.of(RouteDevConsole.FILTER, "plain")));
+        assertEquals("plain", plain.getString("routeId"));
+        assertFalse(plain.containsKey("routeTemplateId"));
+        assertFalse(plain.containsKey("templatedRouteSource"));
+    }
+
+    @Test
+    public void testTemplatedRouteText() {
+        DevConsole console = assertConsoleExists("route");
+
+        String text = callText(console, Map.of(RouteDevConsole.FILTER, 
"greet"));
+        assertTrue(text.contains("Route Template Id: greeting"), text);
+        assertTrue(text.contains("Templated Route Source: 
mem:my-routes.java"), text);
+    }
+
+    private static JsonObject route(JsonObject json) {
+        List<JsonObject> routes = json.getCollection("routes");
+        assertEquals(1, routes.size());
+        return routes.get(0);
+    }
+}
diff --git 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
index c14a3d0dbde8..b1849c57b4b4 100644
--- 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
+++ 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
@@ -63,6 +63,7 @@ import 
org.apache.camel.model.transformer.TransformerDefinition;
 import org.apache.camel.model.validator.ValidatorDefinition;
 import org.apache.camel.spi.ModelReifierFactory;
 import org.apache.camel.spi.NodeIdFactory;
+import org.apache.camel.spi.Resource;
 import org.apache.camel.spi.RouteTemplateLoaderListener;
 import org.apache.camel.spi.RouteTemplateParameterSource;
 import org.apache.camel.support.CamelContextHelper;
@@ -536,6 +537,15 @@ public class DefaultModel implements Model {
             String parentRouteId, String parentProcessorId,
             RouteTemplateContext routeTemplateContext)
             throws Exception {
+        return doAddRouteFromTemplate(routeId, routeTemplateId, prefixId, 
group, parentRouteId, parentProcessorId,
+                routeTemplateContext, null);
+    }
+
+    private String doAddRouteFromTemplate(
+            String routeId, String routeTemplateId, String prefixId, String 
group,
+            String parentRouteId, String parentProcessorId,
+            RouteTemplateContext routeTemplateContext, Resource 
templatedRouteResource)
+            throws Exception {
 
         RouteTemplateDefinition target = null;
         for (RouteTemplateDefinition def : routeTemplateDefinitions) {
@@ -702,6 +712,12 @@ public class DefaultModel implements Model {
         routeCollection.setRoutes(getRouteDefinitions());
         routeCollection.prepareRoute(def);
 
+        // the route is in the resource with the route template (its source 
location is there too), and is declared
+        // in the resource with the templated route (if any); set after the 
route is prepared, as preparing it sets the
+        // resource of the route collection
+        def.setResource(target.getResource());
+        def.setTemplatedRouteResource(templatedRouteResource);
+
         // add route and return the id it was assigned
         addRouteDefinition(def);
         return def.getId();
@@ -759,8 +775,9 @@ public class DefaultModel implements Model {
             }
         }
         // Add the route
-        addRouteFromTemplate(templatedRouteDefinition.getRouteId(), 
templatedRouteDefinition.getRouteTemplateRef(),
-                templatedRouteDefinition.getPrefixId(), 
templatedRouteDefinition.getGroup(), routeTemplateContext);
+        doAddRouteFromTemplate(templatedRouteDefinition.getRouteId(), 
templatedRouteDefinition.getRouteTemplateRef(),
+                templatedRouteDefinition.getPrefixId(), 
templatedRouteDefinition.getGroup(), null, null,
+                routeTemplateContext, templatedRouteDefinition.getResource());
     }
 
     private RouteTemplateContext 
toRouteTemplateContext(TemplatedRouteDefinition templatedRouteDefinition) {
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 a3c90867d58b..35d2ce7e1db0 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
@@ -949,6 +949,18 @@ public abstract class RouteBuilder extends BuilderSupport 
implements RoutesBuild
             for (BeanFactoryDefinition def : beans) {
                 def.setResource(getResource());
             }
+            // the route templates and templated routes are created before the 
resource is set on their collections
+            // (the routes get their resource when they are prepared)
+            for (RouteTemplateDefinition def : 
getRouteTemplateCollection().getRouteTemplates()) {
+                if (def.getResource() == null) {
+                    def.setResource(getResource());
+                }
+            }
+            for (TemplatedRouteDefinition def : 
getTemplatedRouteCollection().getTemplatedRoutes()) {
+                if (def.getResource() == null) {
+                    def.setResource(getResource());
+                }
+            }
 
             for (RouteDefinition route : getRouteCollection().getRoutes()) {
                 // ensure the route is prepared after configure method is 
complete
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
index d28f710977b6..3e6d91a12072 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
@@ -99,6 +99,7 @@ public class RouteDefinition extends 
OutputDefinition<RouteDefinition>
     private RouteTemplateContext routeTemplateContext;
     private String routeTemplateId;
     private Resource resource;
+    private Resource templatedRouteResource;
     private String precondition;
 
     public RouteDefinition() {
@@ -898,6 +899,7 @@ public class RouteDefinition extends 
OutputDefinition<RouteDefinition>
      * The id of the route template (or Kamelet) this route was created from, 
or <tt>null</tt> if the route was not
      * created from a route template.
      */
+    @Override
     public String getRouteTemplateId() {
         return routeTemplateId;
     }
@@ -916,6 +918,21 @@ public class RouteDefinition extends 
OutputDefinition<RouteDefinition>
         this.resource = resource;
     }
 
+    /**
+     * The resource that declares the templated route this route was created 
from, or <tt>null</tt> if the route was not
+     * created from a templated route loaded from a resource. The route itself 
is in the resource with the route
+     * template, see {@link #getResource()}.
+     */
+    @Override
+    public Resource getTemplatedRouteResource() {
+        return templatedRouteResource;
+    }
+
+    @XmlTransient
+    public void setTemplatedRouteResource(Resource templatedRouteResource) {
+        this.templatedRouteResource = templatedRouteResource;
+    }
+
     // Properties
     // -----------------------------------------------------------------------
 
diff --git 
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
 
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
index aced2e336c66..ec05aa6110c9 100644
--- 
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
+++ 
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedRouteMBean.java
@@ -58,6 +58,12 @@ public interface ManagedRouteMBean extends 
ManagedPerformanceCounterMBean {
     @ManagedAttribute(description = "Route Source Location (Short)")
     String getSourceLocationShort();
 
+    @ManagedAttribute(description = "The id of the route template (or Kamelet) 
this route was created from")
+    String getRouteTemplateId();
+
+    @ManagedAttribute(description = "The source (resource) that declares the 
templated route this route was created from")
+    String getTemplatedRouteSource();
+
     @ManagedAttribute(description = "Route Configuration ID")
     String getRouteConfigurationId();
 
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
index 083ff5caa506..ef8271313045 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
@@ -58,6 +58,7 @@ import org.apache.camel.model.RoutesDefinition;
 import org.apache.camel.spi.InflightRepository;
 import org.apache.camel.spi.ManagementAgent;
 import org.apache.camel.spi.ManagementStrategy;
+import org.apache.camel.spi.Resource;
 import org.apache.camel.spi.RoutePolicy;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.PluginHelper;
@@ -195,6 +196,17 @@ public class ManagedRoute extends 
ManagedPerformanceCounter implements ManagedRo
         return sourceLocationShort;
     }
 
+    @Override
+    public String getRouteTemplateId() {
+        return route.getRouteTemplateId();
+    }
+
+    @Override
+    public String getTemplatedRouteSource() {
+        Resource resource = route.getTemplatedRouteResource();
+        return resource != null ? resource.getLocation() : null;
+    }
+
     @Override
     public String getRouteConfigurationId() {
         return configurationId;
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteTemplatedRouteSourceTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteTemplatedRouteSourceTest.java
new file mode 100644
index 000000000000..8c00d1cd1bfd
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteTemplatedRouteSourceTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.management;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.ResourceHelper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static 
org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_ROUTE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * CAMEL-25598: the route template id and the source of the templated route of 
a route are exposed in JMX.
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedRouteTemplatedRouteSourceTest extends 
ManagementTestSupport {
+
+    @Test
+    public void testTemplatedRouteSource() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+
+        ObjectName on = getCamelObjectName(TYPE_ROUTE, "greet");
+        assertEquals("greeting", mbeanServer.getAttribute(on, 
"RouteTemplateId"));
+        assertEquals("mem:my-routes.java", mbeanServer.getAttribute(on, 
"TemplatedRouteSource"));
+
+        on = getCamelObjectName(TYPE_ROUTE, "plain");
+        assertNull(mbeanServer.getAttribute(on, "RouteTemplateId"));
+        assertNull(mbeanServer.getAttribute(on, "TemplatedRouteSource"));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        RouteBuilder builder = new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("greeting").templateParameter("greeting")
+                        .from("direct:greet")
+                        .setBody(constant("{{greeting}}"));
+
+                
templatedRoute("greeting").routeId("greet").parameter("greeting", "Hello");
+
+                from("direct:plain").routeId("plain")
+                        .to("mock:plain");
+            }
+        };
+        builder.setResource(ResourceHelper.fromString("mem:my-routes.java", 
""));
+        return builder;
+    }
+
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
index 3699148eb873..7aad6a8ad530 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
@@ -376,11 +376,12 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
                 // times and fail on a duplicate route id, CAMEL-24866)
                 // (the file of a route may have been removed since it loaded: 
on-demand reload only passes the files
                 // that exist, so its routes are removed like those of a 
changed file, CAMEL-25427)
+                // (a route from a templated route needs the file with the 
route template, and the file with the
+                // templated route, CAMEL-25598; a route from a Kamelet is 
created again by its parent route)
                 getCamelContext().getRoutes().forEach(r -> {
-                    Resource rs = r.getSourceResource();
-                    if (rs != null && !isDeletedFile(rs) && 
!equalResourceLocation(resources, rs)
-                            && !equalResourceLocation(sources, rs)) {
-                        sources.add(rs);
+                    if (!r.isCreatedByKamelet()) {
+                        addRouteSource(resources, sources, 
r.getSourceResource());
+                        addRouteSource(resources, sources, 
r.getTemplatedRouteResource());
                     }
                 });
                 // first stop and remove all routes
@@ -600,6 +601,17 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
         }
     }
 
+    /**
+     * Adds the source of a running route to the sources to load again, unless 
it is one of the updated resources, it is
+     * already added, or its file is gone.
+     */
+    private static void addRouteSource(Collection<Resource> resources, 
List<Resource> sources, Resource rs) {
+        if (rs != null && !isDeletedFile(rs) && 
!equalResourceLocation(resources, rs)
+                && !equalResourceLocation(sources, rs)) {
+            sources.add(rs);
+        }
+    }
+
     /**
      * Whether the resource is a file that no longer exists. Only the location 
is looked at: a source restored from its
      * last loaded content is kept in memory and always exists, but its file 
may have been removed since (CAMEL-25427).
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index ec4c82b8e22a..45ea45b75622 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -121,3 +121,17 @@ holds only null values, now sends no header (an empty List 
was sent as the heade
 Every other value is converted to one String and sent as one header as before: 
a String (also when it contains
 commas), a `byte[]` and the other primitive arrays, a `Map`, and a Vert.x 
`JsonObject` or `JsonArray` (sent as JSON).
 A route that needs the old single header can set the header to a String, for 
example with `String.valueOf(list)`.
+
+=== camel-core - a route from a route template has the template's resource as 
its source
+
+A route created from a route template or a Kamelet now returns the resource 
with the route template from
+`Route.getSourceResource()` (it returned null). This is the resource its 
source location (`Route.getSourceLocation()`)
+was already in, so the debugger and message history consoles show the source 
code of these routes too.
+
+A route created from a `templatedRoute` also remembers the resource that 
declares the `templatedRoute`
+(`Route.getTemplatedRouteResource()`, the `TemplatedRouteSource` JMX 
attribute, and `templatedRouteSource` in the
+route dev console), and the route template id is available from 
`Route.getRouteTemplateId()`, the `RouteTemplateId`
+JMX attribute, and `routeTemplateId` in the route dev console.
+
+Code that took a null source resource to mean that a route was created from a 
route template should use
+`Route.isCreatedByRouteTemplate()` instead.
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadTemplatedRouteSourceTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadTemplatedRouteSourceTest.java
new file mode 100644
index 000000000000..d6b3d0fe6af4
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadTemplatedRouteSourceTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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 org.apache.camel.Route;
+import org.apache.camel.ServiceStatus;
+import org.apache.camel.dsl.yaml.support.YamlTestSupport;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.RouteWatcherReloadStrategy;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25598: a route created from a templated route records the file with 
its route template (as its source) and the
+ * file with the templated route, so a reload of another file loads both again.
+ */
+class RouteReloadTemplatedRouteSourceTest extends YamlTestSupport {
+
+    /** Subclass to expose protected lifecycle methods for testing without a 
live file watcher. */
+    static class TestableStrategy extends RouteWatcherReloadStrategy {
+        TestableStrategy(String dir) {
+            super(dir);
+        }
+
+        @Override
+        public void doStart() throws Exception {
+            super.doStart();
+        }
+
+        @Override
+        public void doStop() throws Exception {
+            super.doStop();
+        }
+    }
+
+    private Path dir;
+    private Path templates;
+    private Path uses;
+    private Path plain;
+
+    @Override
+    public void doSetup() throws Exception {
+        dir = Files.createTempDirectory("camel-reload-templated-source");
+        templates = dir.resolve("templates.camel.yaml");
+        uses = dir.resolve("uses.camel.yaml");
+        plain = dir.resolve("plain.camel.yaml");
+        Files.writeString(templates, """
+                - routeTemplate:
+                    id: "greeting"
+                    parameters:
+                      - name: "greeting"
+                    from:
+                      uri: "direct:greet"
+                      steps:
+                        - setBody:
+                            constant: "{{greeting}}"
+                """);
+        Files.writeString(uses, """
+                - templatedRoute:
+                    routeTemplateRef: "greeting"
+                    routeId: "greet"
+                    parameters:
+                      - name: "greeting"
+                        value: "Hello"
+                """);
+        Files.writeString(plain, plainRoute("plain"));
+        context.start();
+        loadRoutes(ResourceHelper.resolveResource(context, "file:" + 
templates),
+                ResourceHelper.resolveResource(context, "file:" + uses),
+                ResourceHelper.resolveResource(context, "file:" + plain));
+    }
+
+    @Override
+    public void doCleanup() throws Exception {
+        deleteRecursively(dir);
+    }
+
+    @Test
+    void routeRecordsItsSources() {
+        Route greet = context.getRoute("greet");
+        assertThat(greet.getRouteTemplateId()).isEqualTo("greeting");
+        assertThat(greet.getSourceResource().getLocation()).isEqualTo("file:" 
+ templates);
+        assertThat(greet.getSourceLocation()).startsWith("file:" + templates + 
":");
+        
assertThat(greet.getTemplatedRouteResource().getLocation()).isEqualTo("file:" + 
uses);
+
+        Route other = context.getRoute("plain");
+        assertThat(other.getRouteTemplateId()).isNull();
+        assertThat(other.getSourceResource().getLocation()).isEqualTo("file:" 
+ plain);
+        assertThat(other.getTemplatedRouteResource()).isNull();
+    }
+
+    @Test
+    void reloadOfAnotherFileKeepsTheTemplatedRoute() throws Exception {
+        TestableStrategy strategy = new TestableStrategy(dir.toString());
+        strategy.setCamelContext(context);
+        strategy.setPattern("*.yaml");
+        // the strategy is not started (no file watcher in the test): its 
reload callback is driven by hand
+        strategy.doStart();
+        try {
+            Files.writeString(plain, plainRoute("plain changed"));
+            strategy.getResourceReload().onReload(plain.toString(),
+                    ResourceHelper.resolveResource(context, "file:" + plain));
+
+            assertThat(strategy.getFailedCounter()).isZero();
+            assertThat(strategy.getLastError()).isNull();
+            assertThat(context.getRoutes()).hasSize(2);
+            
assertThat(context.getRouteController().getRouteStatus("greet")).isEqualTo(ServiceStatus.Started);
+            
assertThat(context.createFluentProducerTemplate().to("direct:greet").request(String.class))
+                    .isEqualTo("Hello");
+            
assertThat(context.createFluentProducerTemplate().to("direct:plain").request(String.class))
+                    .isEqualTo("plain changed");
+            
assertThat(context.getRoute("greet").getTemplatedRouteResource().getLocation()).isEqualTo("file:"
 + uses);
+        } finally {
+            strategy.doStop();
+        }
+    }
+
+    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;
+            }
+        });
+    }
+}

Reply via email to