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 411c04677c89 CAMEL-25598: camel-core - routes created from route 
templates record their source files for reload and tooling (#27751)
411c04677c89 is described below

commit 411c04677c89f8e1ee4c3b0f0269fd497dec8e55
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 13 09:38:26 2026 +0200

    CAMEL-25598: camel-core - routes created from route templates record their 
source files for reload and tooling (#27751)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[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 66ba96b02761..01f65789c818 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
@@ -287,3 +287,17 @@ at a time: while an attempt waits for the server (up to 
the connect timeout), th
 are skipped, and `maxReconnectAttempts` counts these attempts one after the 
other. Previously every interval started
 another attempt, and when the server answered late each of them opened a 
WebSocket, so that every message was routed
 once per WebSocket. A consumer that is stopped no longer reconnects.
+
+=== 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