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