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