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 91c0f732343c CAMEL-25771: camel-core - a route checksum and revision,
so tools can tell which routes changed between two runs or after a reload
(#28213)
91c0f732343c is described below
commit 91c0f732343c2df48f2c7649801c96288553db82
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 22 22:15:00 2026 +0200
CAMEL-25771: camel-core - a route checksum and revision, so tools can tell
which routes changed between two runs or after a reload (#28213)
Co-authored-by: Guillaume Nodet <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../apache/camel/catalog/dev-consoles-openapi.json | 16 ++
.../camel/catalog/dev-consoles/route-dump.json | 4 +
.../catalog/dev-consoles/route-structure.json | 4 +
.../apache/camel/catalog/dev-consoles/route.json | 8 +
.../kamelet/KameletRouteChecksumTest.java | 72 ++++++
.../src/main/java/org/apache/camel/NamedRoute.java | 15 ++
.../src/main/java/org/apache/camel/Route.java | 28 +++
.../apache/camel/spi/ModelChecksumGenerator.java | 53 ++++
.../camel/impl/engine/AbstractCamelContext.java | 52 ++++
.../org/apache/camel/impl/engine/DefaultRoute.java | 18 ++
.../org/apache/camel/impl/engine/RouteService.java | 5 +
.../org/apache/camel/dev-console/route-dump.json | 4 +
.../apache/camel/dev-console/route-structure.json | 4 +
.../org/apache/camel/dev-console/route.json | 8 +
.../apache/camel/impl/console/RouteDevConsole.java | 8 +-
.../camel/impl/console/RouteDumpDevConsole.java | 7 +-
.../impl/console/RouteStructureDevConsole.java | 7 +-
.../impl/console/RouteChecksumDevConsoleTest.java | 76 ++++++
.../camel/impl/DefaultDumpRoutesStrategy.java | 5 +
.../org/apache/camel/model/RouteDefinition.java | 15 ++
.../apache/camel/model/RouteDefinitionHelper.java | 24 ++
.../org/apache/camel/impl/RouteChecksumTest.java | 271 +++++++++++++++++++++
.../RouteWatcherReloadChangedRoutesTest.java | 161 ++++++++++++
.../api/management/mbean/ManagedRouteMBean.java | 6 +
.../camel/management/mbean/ManagedRoute.java | 10 +
.../camel/management/ManagedRouteChecksumTest.java | 59 +++++
.../camel/support/RouteWatcherReloadStrategy.java | 26 +-
.../org/apache/camel/model-checksum-generator | 2 +
.../apache/camel/xml/LwModelChecksumGenerator.java | 63 +++++
.../org/apache/camel/xml/LwModelToXMLDumper.java | 120 ++++++++-
.../camel/xml/LwModelToXMLDumperRawUriTest.java | 52 ++++
.../ROOT/pages/camel-jbang-diagnostics.adoc | 13 +
.../modules/ROOT/pages/camel-jbang-managing.adoc | 5 +
.../jbang-commands/camel-jbang-get-route.adoc | 1 +
.../jbang-commands/camel-jbang-top-route.adoc | 1 +
.../modules/ROOT/pages/route-reload.adoc | 16 ++
docs/user-manual/modules/ROOT/pages/routes.adoc | 39 +++
.../endpoint/EndpointDslRouteChecksumTest.java | 55 +++++
.../META-INF/camel-jbang-commands-metadata.json | 4 +-
.../jbang/core/commands/TransformJavaRoutes.java | 9 +
.../commands/action/CamelRouteStructureAction.java | 6 +
.../core/commands/process/CamelRouteStatus.java | 16 ++
.../jbang/core/commands/process/CamelRouteTop.java | 5 +
.../commands/action/RouteActionJavaSourceTest.java | 56 +++++
.../commands/process/CamelRouteStatusTest.java | 28 +++
.../dsl/jbang/core/commands/mcp/RuntimeTools.java | 8 +-
46 files changed, 1444 insertions(+), 21 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 865ca3654eca..6c30c57da90d 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
@@ -5924,6 +5924,14 @@
"type": "string",
"description": "The source that declares the
templated route the route was created from (only present when known)"
},
+
"checksum": {
+
"type": "string",
+
"description": "The checksum of the route as
written, the same in every run: compare it with an earlier run to tell whether
the route changed (only present when known, as it needs camel-xml-io)"
+
},
+
"revision": {
+
"type": "integer",
+
"description": "The revision of the route in
this run: 1 when it is added, one more each time it is added again with another
checksum, such as by a reload (only present when the checksum is known)"
+
},
"state": {
"type": "string",
"description": "The route state"
@@ -6678,6 +6686,10 @@
"type": "string",
"description": "The source location (only
present when known)"
},
+
"checksum": {
+
"type": "string",
+
"description": "The checksum of the route as
written, the same in every run: the dump is of this version of the route (only
present when known, as it needs camel-xml-io)"
+
},
"format": {
"type": "string",
"description": "The dump format,
xml\/yaml\/java (only present when the dump succeeded)"
@@ -6974,6 +6986,10 @@
"type": "string",
"description": "The route description (only
present when configured)"
},
+
"checksum": {
+
"type": "string",
+
"description": "The checksum of the route as
written, the same in every run: the facts a tool keeps per line hold for this
version of the route (only present when known, as it needs camel-xml-io)"
+
},
"code": {
"type": "array",
"items": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-dump.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-dump.json
index dc181764ef54..558f375eb34d 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-dump.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-dump.json
@@ -96,6 +96,10 @@
"type": "string",
"description": "The source location (only present when known)"
},
+ "checksum": {
+ "type": "string",
+ "description": "The checksum of the route as written, the same
in every run: the dump is of this version of the route (only present when
known, as it needs camel-xml-io)"
+ },
"format": {
"type": "string",
"description": "The dump format, xml\/yaml\/java (only present
when the dump succeeded)"
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-structure.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-structure.json
index 34190b878fc1..f0e228f960d1 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-structure.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-structure.json
@@ -100,6 +100,10 @@
"type": "string",
"description": "The route description (only present when
configured)"
},
+ "checksum": {
+ "type": "string",
+ "description": "The checksum of the route as written, the same
in every run: the facts a tool keeps per line hold for this version of the
route (only present when known, as it needs camel-xml-io)"
+ },
"code": {
"type": "array",
"items": {
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 20556d0d9c73..b92777039028 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
@@ -133,6 +133,14 @@
"type": "string",
"description": "The source that declares the templated route the
route was created from (only present when known)"
},
+ "checksum": {
+ "type": "string",
+ "description": "The checksum of the route as written, the same
in every run: compare it with an earlier run to tell whether the route changed
(only present when known, as it needs camel-xml-io)"
+ },
+ "revision": {
+ "type": "integer",
+ "description": "The revision of the route in this run: 1 when it
is added, one more each time it is added again with another checksum, such as
by a reload (only present when the checksum is known)"
+ },
"state": {
"type": "string",
"description": "The route state"
diff --git
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletRouteChecksumTest.java
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletRouteChecksumTest.java
new file mode 100644
index 000000000000..a181aeaf6c94
--- /dev/null
+++
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletRouteChecksumTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.component.kamelet;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * CAMEL-25771: a Kamelet route gets its node prefix, and its id when it has
none of its own, from a counter, which the
+ * checksum leaves out: the same Kamelet route has the same checksum each time
it is created.
+ */
+class KameletRouteChecksumTest {
+
+ @Test
+ void theSameKameletRouteHasTheSameChecksumWhenCreatedAgain() throws
Exception {
+ // the counter goes on in the second run, as it does when a reload
creates the routes again
+ Map<String, String> first = checksums();
+ Map<String, String> second = checksums();
+
+ assertEquals(2, first.size(), first.toString());
+ first.values().forEach(checksum -> assertNotNull(checksum,
first.toString()));
+ assertEquals(first, second);
+ }
+
+ // the checksums of the Kamelet routes: the one with an id of its own, and
the one without
+ private static Map<String, String> checksums() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ routeTemplate("greeting").templateParameter("name")
+ .from("kamelet:source")
+ .setBody().simple("Hello {{name}}");
+
+
from("direct:own").routeId("own").kamelet("greeting/greet-own?name=own");
+
from("direct:none").routeId("none").kamelet("greeting?name=none");
+ }
+ });
+ context.start();
+ Map<String, String> answer = new TreeMap<>();
+ for (Route route : context.getRoutes()) {
+ if ("greeting".equals(route.getRouteTemplateId())) {
+ answer.put("greet-own".equals(route.getId()) ? "own" :
"none", route.getChecksum());
+ }
+ }
+ return answer;
+ }
+ }
+}
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 b2ec7e1058fc..1363186f910a 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
@@ -100,4 +100,19 @@ public interface NamedRoute {
return null;
}
+ /**
+ * Gets the checksum of the route as written: its steps, options and
expressions, with property placeholders as
+ * written, without the ids Camel generates and where the route is in its
source. The same route gives the same
+ * checksum in every run, so tools can tell which routes changed. It is
computed when the route is prepared, before
+ * the parts of route configurations are added to it.
+ *
+ * @return the checksum, or null when it is not known (camel-xml-io is not
on the classpath)
+ * @see org.apache.camel.spi.ModelChecksumGenerator
+ * @since 4.24
+ */
+ @Nullable
+ default String getChecksum() {
+ 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 7d1ca3dd1819..847034802837 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
@@ -218,6 +218,34 @@ public interface Route extends RuntimeConfiguration {
return null;
}
+ /**
+ * Gets the checksum of the route as written: its steps, options and
expressions, with property placeholders as
+ * written, without the ids Camel generates and where the route is in its
source, and with the secrets in endpoint
+ * URIs masked. The same route gives the same checksum in every run, so
tools can tell which routes changed between
+ * two runs.
+ *
+ * @return the checksum, or null when it is not known (camel-xml-io is not
on the classpath)
+ * @see org.apache.camel.spi.ModelChecksumGenerator
+ * @since 4.24
+ */
+ @Nullable
+ default String getChecksum() {
+ return null;
+ }
+
+ /**
+ * Gets the revision of the route in this run: 1 when the route is added,
one more each time it is added again with
+ * another checksum, such as when a reload changed it. A reload that
brings the route back as it was keeps the
+ * revision. The revision follows the route id, and starts over when the
application restarts (tools compare
+ * checksums across runs). Camel keeps the revisions of the 10000 route
ids used last.
+ *
+ * @return the revision, or 0 when it is not known (the checksum is not
known)
+ * @since 4.24
+ */
+ default int getRevision() {
+ return 0;
+ }
+
/**
* The source:line-number where the route input is located in the source
code
*/
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/ModelChecksumGenerator.java
b/core/camel-api/src/main/java/org/apache/camel/spi/ModelChecksumGenerator.java
new file mode 100644
index 000000000000..91342aa76539
--- /dev/null
+++
b/core/camel-api/src/main/java/org/apache/camel/spi/ModelChecksumGenerator.java
@@ -0,0 +1,53 @@
+/*
+ * 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.spi;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.NamedNode;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Computes the checksum of a route as written, so tools can tell which routes
changed between two runs or after a
+ * reload: the same route gives the same checksum in every run.
+ * <p/>
+ * The checksum is a hash of the whole route: its steps, their options and
expressions, with property placeholders as
+ * written. It leaves out what does not change the route: the ids Camel
generates, and where the route is in its source
+ * (moving the route in its file, or adding a route above it, keeps its
checksum).
+ * <p/>
+ * The implementation is provided by {@code camel-xml-io}, which writes every
option of the model, and is discovered via
+ * the service key {@link #FACTORY}. It is a single implementation, so a route
gets the same checksum whichever modules
+ * an application has.
+ *
+ * @since 4.24
+ */
+public interface ModelChecksumGenerator {
+
+ /**
+ * Service factory key.
+ */
+ String FACTORY = "model-checksum-generator";
+
+ /**
+ * Computes the checksum of the definition as written.
+ *
+ * @param context the CamelContext
+ * @param definition the definition, such as a route
+ * @return the checksum, a short hex digest, or <tt>null</tt>
when it cannot be computed
+ */
+ @Nullable
+ String generateChecksum(CamelContext context, NamedNode definition);
+}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index 1641883748e7..0bd7b42e86ed 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -139,6 +139,7 @@ import org.apache.camel.spi.ManagementNameStrategy;
import org.apache.camel.spi.ManagementStrategy;
import org.apache.camel.spi.MessageHistoryFactory;
import org.apache.camel.spi.MessageSizeStrategy;
+import org.apache.camel.spi.ModelChecksumGenerator;
import org.apache.camel.spi.ModelJAXBContextFactory;
import org.apache.camel.spi.ModelToJavaDumper;
import org.apache.camel.spi.ModelToStructureDumper;
@@ -228,6 +229,8 @@ public abstract class AbstractCamelContext extends
BaseService
implements CatalogCamelContext, Suspendable {
private static final Logger LOG =
LoggerFactory.getLogger(AbstractCamelContext.class);
+ // how many route ids keep their revision (CAMEL-25771)
+ private static final int MAX_ROUTE_REVISIONS = 10000;
private final InternalServiceManager internalServiceManager;
@@ -251,6 +254,15 @@ public abstract class AbstractCamelContext extends
BaseService
private final StopWatch stopWatch = new StopWatch(false);
private final ThreadLocal<Set<String>> componentsInCreation =
ThreadLocal.withInitial(HashSet::new);
private final Lock routesLock = new ReentrantLock();
+ // the revisions of the routes by id, guarded by routesLock (CAMEL-25771):
the ids used least recently go first
+ // once there are more than MAX_ROUTE_REVISIONS, so an application that
keeps adding routes with new ids, such as
+ // Kamelet routes without an id of their own, does not grow it for the
life of the JVM
+ private final Map<String, RouteRevision> routeRevisions = new
LinkedHashMap<>(16, 0.75f, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<String, RouteRevision>
eldest) {
+ return size() > MAX_ROUTE_REVISIONS;
+ }
+ };
private final Lock lock = new ReentrantLock();
private VetoCamelContextStartException vetoed;
private String managementName;
@@ -411,6 +423,7 @@ public abstract class AbstractCamelContext extends
BaseService
camelContextExtension.lazyAddContextPlugin(ModelToYAMLDumper.class,
this::createModelToYAMLDumper);
camelContextExtension.lazyAddContextPlugin(ModelToJavaDumper.class,
this::createModelToJavaDumper);
camelContextExtension.lazyAddContextPlugin(ModelToStructureDumper.class,
this::createModelToStructureDumper);
+
camelContextExtension.lazyAddContextPlugin(ModelChecksumGenerator.class,
this::createModelChecksumGenerator);
camelContextExtension.lazyAddContextPlugin(DeferServiceFactory.class,
this::createDeferServiceFactory);
camelContextExtension.lazyAddContextPlugin(AnnotationBasedProcessorFactory.class,
this::createAnnotationBasedProcessorFactory);
@@ -4688,6 +4701,15 @@ public abstract class AbstractCamelContext extends
BaseService
protected abstract ModelToStructureDumper createModelToStructureDumper();
+ /**
+ * The generator of the checksums of routes, or <tt>null</tt> without
camel-xml-io on the classpath: then the routes
+ * have no checksum.
+ */
+ protected ModelChecksumGenerator createModelChecksumGenerator() {
+ return
ResolverHelper.resolveBootstrapService(getCamelContextReference(),
ModelChecksumGenerator.FACTORY,
+ ModelChecksumGenerator.class).orElse(null);
+ }
+
protected abstract RestBindingJaxbDataFormatFactory
createRestBindingJaxbDataFormatFactory();
protected abstract RestBindingJacksonXmlDataFormatFactory
createRestBindingJacksonXmlDataFormatFactory();
@@ -4767,11 +4789,41 @@ public abstract class AbstractCamelContext extends
BaseService
routesLock.lock();
try {
routes.add(route);
+ // RouteService has set it before the route was announced; this is
for a route added another way
+ reviseRoute(route);
} finally {
routesLock.unlock();
}
}
+ /**
+ * Sets the revision of a route being added: 1 the first time its id is
added, one more when it is added again with
+ * another checksum (a reload that changed it), the same when it comes
back as it was. It is kept by route id, so it
+ * outlives the route being removed and added again; the {@value
#MAX_ROUTE_REVISIONS} ids used last are kept. The
+ * same route revised again keeps its revision, so RouteService sets it
before the route added event and
+ * {@link #addRoute(Route)} after it.
+ */
+ void reviseRoute(Route route) {
+ String checksum = route.getChecksum();
+ if (route.getId() == null || checksum == null || !(route instanceof
DefaultRoute defaultRoute)) {
+ return;
+ }
+ routesLock.lock();
+ try {
+ RouteRevision previous = routeRevisions.get(route.getId());
+ int revision = previous == null ? 1
+ : previous.checksum().equals(checksum) ?
previous.revision() : previous.revision() + 1;
+ routeRevisions.put(route.getId(), new RouteRevision(checksum,
revision));
+ defaultRoute.setRevision(revision);
+ } finally {
+ routesLock.unlock();
+ }
+ }
+
+ /** The checksum a route id was last added with, and its revision. */
+ private record RouteRevision(String checksum, int revision) {
+ }
+
public void removeRoute(Route route) {
routesLock.lock();
try {
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 5bf89e352077..03047b959c83 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
@@ -82,6 +82,8 @@ public class DefaultRoute extends ServiceSupport implements
Route {
private final String sourceLocation;
private final String sourceLocationShort;
private final String routeTemplateId;
+ private final String checksum;
+ private volatile int revision;
private final Resource templatedRouteResource;
private final List<Processor> eventDrivenProcessors = new ArrayList<>();
private final List<InterceptStrategy> interceptStrategies = new
ArrayList<>(0);
@@ -139,6 +141,7 @@ public class DefaultRoute extends ServiceSupport implements
Route {
NamedRoute namedRoute = route instanceof NamedRoute nr ? nr : null;
this.routeTemplateId = namedRoute != null ?
namedRoute.getRouteTemplateId() : null;
this.templatedRouteResource = namedRoute != null ?
namedRoute.getTemplatedRouteResource() : null;
+ this.checksum = namedRoute != null ? namedRoute.getChecksum() : null;
}
@Override
@@ -233,6 +236,21 @@ public class DefaultRoute extends ServiceSupport
implements Route {
return routeTemplateId;
}
+ @Override
+ public String getChecksum() {
+ return checksum;
+ }
+
+ @Override
+ public int getRevision() {
+ return revision;
+ }
+
+ /** Set by the CamelContext when the route is added: see {@link
Route#getRevision()}. */
+ void setRevision(int revision) {
+ this.revision = revision;
+ }
+
@Override
public Resource getTemplatedRouteResource() {
return templatedRouteResource;
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java
index 02e8933fb2a0..7ecd5487182c 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/RouteService.java
@@ -224,6 +224,11 @@ public class RouteService extends ChildServiceSupport {
startChildServices(route, childServices);
+ // the revision before the route is announced, so
listeners and JMX see it (CAMEL-25771)
+ if (camelContext instanceof AbstractCamelContext acc) {
+ acc.reviseRoute(route);
+ }
+
// fire event
EventHelper.notifyRouteAdded(camelContext, route);
}
diff --git
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-dump.json
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-dump.json
index dc181764ef54..558f375eb34d 100644
---
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-dump.json
+++
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-dump.json
@@ -96,6 +96,10 @@
"type": "string",
"description": "The source location (only present when known)"
},
+ "checksum": {
+ "type": "string",
+ "description": "The checksum of the route as written, the same
in every run: the dump is of this version of the route (only present when
known, as it needs camel-xml-io)"
+ },
"format": {
"type": "string",
"description": "The dump format, xml\/yaml\/java (only present
when the dump succeeded)"
diff --git
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-structure.json
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-structure.json
index 34190b878fc1..f0e228f960d1 100644
---
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-structure.json
+++
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-structure.json
@@ -100,6 +100,10 @@
"type": "string",
"description": "The route description (only present when
configured)"
},
+ "checksum": {
+ "type": "string",
+ "description": "The checksum of the route as written, the same
in every run: the facts a tool keeps per line hold for this version of the
route (only present when known, as it needs camel-xml-io)"
+ },
"code": {
"type": "array",
"items": {
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 20556d0d9c73..b92777039028 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
@@ -133,6 +133,14 @@
"type": "string",
"description": "The source that declares the templated route the
route was created from (only present when known)"
},
+ "checksum": {
+ "type": "string",
+ "description": "The checksum of the route as written, the same
in every run: compare it with an earlier run to tell whether the route changed
(only present when known, as it needs camel-xml-io)"
+ },
+ "revision": {
+ "type": "integer",
+ "description": "The revision of the route in this run: 1 when it
is added, one more each time it is added again with another checksum, such as
by a reload (only present when the checksum is 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 5012dbf2654a..eb956478637c 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
@@ -97,6 +97,8 @@ public class RouteDevConsole extends AbstractDevConsole {
@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 checksum of the route as written, the
same in every run: compare it with an earlier run to tell whether the route
changed (only present when known, as it needs camel-xml-io)") String checksum,
+ @Metadata(description = "The revision of the route in this run: 1
when it is added, one more each time it is added again with another checksum,
such as by a reload (only present when the checksum is known)") Integer
revision,
@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,
@@ -176,6 +178,9 @@ public class RouteDevConsole extends AbstractDevConsole {
if (mrb.getTemplatedRouteSource() != null) {
sb.append(String.format("%n Templated Route Source: %s",
mrb.getTemplatedRouteSource()));
}
+ if (mrb.getChecksum() != null) {
+ sb.append(String.format("%n Checksum: %s (revision %d)",
mrb.getChecksum(), mrb.getRevision()));
+ }
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()));
@@ -321,7 +326,8 @@ public class RouteDevConsole extends AbstractDevConsole {
list.add(new RouteEntry(
mrb.getRouteId(), mrb.getRouteGroup(),
mrb.getNodePrefixId(), mrb.getDescription(), mrb.getNote(),
mrb.isCreatedByKamelet(), mrb.isCreatedByRouteTemplate(),
mrb.getRouteTemplateId(), mrb.getEndpointUri(),
- mrb.isRemoteEndpoint(), mrb.getSourceLocation(),
mrb.getTemplatedRouteSource(), mrb.getState(),
+ mrb.isRemoteEndpoint(), mrb.getSourceLocation(),
mrb.getTemplatedRouteSource(), mrb.getChecksum(),
+ mrb.getRevision() > 0 ? mrb.getRevision() : null,
mrb.getState(),
r != null && r.supportsSuspension(), mrb.getUptime(),
varied > 0 ? varied : null, mixed > 0 ? mixed : null,
lastError,
stats, procList));
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDumpDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDumpDevConsole.java
index 7e4d707d2b4e..5ab34c280fa1 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDumpDevConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteDumpDevConsole.java
@@ -53,6 +53,7 @@ public class RouteDumpDevConsole extends AbstractDevConsole {
@Metadata(description = "The route ID") String routeId,
@Metadata(description = "The route's endpoint URI") String from,
@Metadata(description = "The source location (only present when
known)") String source,
+ @Metadata(description = "The checksum of the route as written, the
same in every run: the dump is of this version of the route (only present when
known, as it needs camel-xml-io)") String checksum,
@Metadata(description = "The dump format, xml/yaml/java (only
present when the dump succeeded)") String format,
@Metadata(description = "The dumped route source code (only
present when the dump succeeded)") List<CodeLine> code) {
}
@@ -105,6 +106,9 @@ public class RouteDumpDevConsole extends AbstractDevConsole
{
if (mrb.getSourceLocation() != null) {
sb.append(String.format("%n Source: %s",
mrb.getSourceLocation()));
}
+ if (mrb.getChecksum() != null) {
+ sb.append(String.format("%n Checksum: %s",
mrb.getChecksum()));
+ }
if (dump != null && !dump.isEmpty()) {
sb.append("\n\n");
for (String line : dump.split("\n")) {
@@ -154,7 +158,8 @@ public class RouteDumpDevConsole extends AbstractDevConsole
{
// ignore
}
- list.add(new RouteEntry(mrb.getRouteId(), mrb.getEndpointUri(),
mrb.getSourceLocation(), format, code));
+ list.add(new RouteEntry(
+ mrb.getRouteId(), mrb.getEndpointUri(),
mrb.getSourceLocation(), mrb.getChecksum(), format, code));
return null;
};
doCall(options, task);
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java
index 4ca790e6c125..d2cd8200a640 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteStructureDevConsole.java
@@ -76,6 +76,7 @@ public class RouteStructureDevConsole extends
AbstractDevConsole {
@Metadata(description = "The source location (only present when
known)") String source,
@Metadata(description = "The source line number (only present when
known)") Integer line,
@Metadata(description = "The route description (only present when
configured)") String description,
+ @Metadata(description = "The checksum of the route as written, the
same in every run: the facts a tool keeps per line hold for this version of the
route (only present when known, as it needs camel-xml-io)") String checksum,
@Metadata(description = "The route's structure, one entry per
processor (only present when the dump succeeded)") List<CodeEntry> code) {
}
@@ -125,6 +126,9 @@ public class RouteStructureDevConsole extends
AbstractDevConsole {
sb.append(String.format("%n Source: %s",
extractSourceLocationNoLineNumber(def.getResource().getLocation())));
}
+ if (def.getChecksum() != null) {
+ sb.append(String.format("%n Checksum: %s",
def.getChecksum()));
+ }
sb.append("\n\n");
// the body types line up in a column after the code (a long
line pushes only its own types out)
int width = 0;
@@ -186,7 +190,8 @@ public class RouteStructureDevConsole extends
AbstractDevConsole {
// ignore
}
- list.add(new RouteEntry(def.getRouteId(), def.getEndpointUrl(),
source, line, def.getDescription(), code));
+ list.add(new RouteEntry(
+ def.getRouteId(), def.getEndpointUrl(), source, line,
def.getDescription(), def.getChecksum(), code));
return null;
};
doCall(options, task);
diff --git
a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteChecksumDevConsoleTest.java
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteChecksumDevConsoleTest.java
new file mode 100644
index 000000000000..2075fe587691
--- /dev/null
+++
b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteChecksumDevConsoleTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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 org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * CAMEL-25771: the route consoles have the checksum of each route as written,
and the route console its revision, so a
+ * tool can tell which routes changed and which version of a route the
structure or the dump is of.
+ */
+class RouteChecksumDevConsoleTest extends ContextTestSupport {
+
+ @Override
+ protected boolean useJmx() {
+ // the route and route-dump consoles list the routes from JMX
+ return true;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start").routeId("myRoute").to("mock:result");
+ }
+ };
+ }
+
+ @Test
+ void testChecksumInTheRouteConsoles() {
+ String checksum = context.getRoute("myRoute").getChecksum();
+ Assertions.assertNotNull(checksum);
+
+ JsonObject route = firstRoute("route");
+ Assertions.assertEquals(checksum, route.getString("checksum"));
+ Assertions.assertEquals(1, route.getInteger("revision"));
+ String text = (String)
console("route").call(DevConsole.MediaType.TEXT);
+ Assertions.assertTrue(text.contains("Checksum: " + checksum + "
(revision 1)"), text);
+
+ Assertions.assertEquals(checksum,
firstRoute("route-structure").getString("checksum"));
+ text = (String)
console("route-structure").call(DevConsole.MediaType.TEXT);
+ Assertions.assertTrue(text.contains("Checksum: " + checksum), text);
+
+ Assertions.assertEquals(checksum,
firstRoute("route-dump").getString("checksum"));
+ }
+
+ private JsonObject firstRoute(String id) {
+ JsonObject out = (JsonObject)
console(id).call(DevConsole.MediaType.JSON);
+ return (JsonObject) ((JsonArray) out.getCollection("routes")).get(0);
+ }
+
+ private DevConsole console(String id) {
+ return
PluginHelper.getDevConsoleResolver(context).resolveDevConsole(id);
+ }
+}
diff --git
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
index ab7f14e0ebf2..2f023a1205a6 100644
---
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
+++
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultDumpRoutesStrategy.java
@@ -783,6 +783,11 @@ public class DefaultDumpRoutesStrategy extends
ServiceSupport implements DumpRou
if (loc != null) {
jo.put("source", loc);
}
+ // the checksum of the route as written: a tool that read the
routes from their source compares it with
+ // the running routes to tell which changed
+ if (def.getChecksum() != null) {
+ jo.put("checksum", def.getChecksum());
+ }
List<JsonObject> code = dumpAsJSon(lines);
jo.put("code", code);
list.add(jo);
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 3e6d91a12072..f93bca2ec0c9 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
@@ -98,6 +98,7 @@ public class RouteDefinition extends
OutputDefinition<RouteDefinition>
private Map<String, Object> templateDefaultParameters;
private RouteTemplateContext routeTemplateContext;
private String routeTemplateId;
+ private String checksum;
private Resource resource;
private Resource templatedRouteResource;
private String precondition;
@@ -909,6 +910,20 @@ public class RouteDefinition extends
OutputDefinition<RouteDefinition>
this.routeTemplateId = routeTemplateId;
}
+ /**
+ * The checksum of the route as written, computed when the route is
prepared, before the parts of route
+ * configurations are added to it; <tt>null</tt> when it is not known
(camel-xml-io is not on the classpath).
+ */
+ @Override
+ public String getChecksum() {
+ return checksum;
+ }
+
+ @XmlTransient
+ public void setChecksum(String checksum) {
+ this.checksum = checksum;
+ }
+
public Resource getResource() {
return resource;
}
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
index 658af1592e8d..d17c24cdbfc9 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
@@ -38,6 +38,7 @@ import org.apache.camel.NamedNode;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.model.rest.RestDefinition;
import org.apache.camel.model.rest.VerbDefinition;
+import org.apache.camel.spi.ModelChecksumGenerator;
import org.apache.camel.spi.NodeIdFactory;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.EndpointHelper;
@@ -421,6 +422,11 @@ public final class RouteDefinitionHelper {
// init the route inputs
initRouteInput();
+ // the checksum of the route as written, before the parts of route
configurations are merged into it
+ if (route.getChecksum() == null) {
+ route.setChecksum(checksum(context, route));
+ }
+
// abstracts is the cross cutting concerns
List<ProcessorDefinition<?>> abstracts = new ArrayList<>();
@@ -455,6 +461,24 @@ public final class RouteDefinitionHelper {
route.getOutputs().addAll(0, upper);
}
+ /**
+ * The checksum of a route as written: the same route gives the same
checksum in every run, so tools can tell which
+ * routes changed (see {@link ModelChecksumGenerator}). A tool that reads
a route from its source without running it
+ * gets the checksum of the running route from this method too.
+ *
+ * @param context the camel context
+ * @param route the route, before it is prepared
+ * @return the checksum, or <tt>null</tt> when it cannot be
computed (camel-xml-io is not on the classpath)
+ */
+ public static String checksum(CamelContext context, NamedNode route) {
+ if (context == null || route == null) {
+ return null;
+ }
+ ModelChecksumGenerator generator
+ =
context.getCamelContextExtension().getContextPlugin(ModelChecksumGenerator.class);
+ return generator != null ? generator.generateChecksum(context, route)
: null;
+ }
+
/**
* Sanity check the route, that it has input(s) and outputs.
*
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/RouteChecksumTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/RouteChecksumTest.java
new file mode 100644
index 000000000000..107d87bc0a24
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/impl/RouteChecksumTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.function.Consumer;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Route;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.builder.RouteConfigurationBuilder;
+import org.apache.camel.builder.TemplatedRouteBuilder;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.CamelEvent.RouteAddedEvent;
+import org.apache.camel.support.EventNotifierSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25771: each route has a checksum of the route as written, the same in
every run, and a revision that counts the
+ * times it came back changed in this run, so tools can tell which routes
changed.
+ */
+class RouteChecksumTest {
+
+ private static RouteBuilder orders(String target) {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+ .split(body()).parallelProcessing()
+ .to("log:line")
+ .end()
+ .to(target);
+ }
+ };
+ }
+
+ private static String checksum(String routeId, RoutesBuilder... builders)
throws Exception {
+ return checksum(routeId, null, builders);
+ }
+
+ private static String checksum(String routeId, Consumer<CamelContext>
setup, RoutesBuilder... builders)
+ throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ if (setup != null) {
+ setup.accept(context);
+ }
+ for (RoutesBuilder builder : builders) {
+ context.addRoutes(builder);
+ }
+ context.start();
+ Route route = context.getRoute(routeId);
+ assertNotNull(route, routeId);
+ return route.getChecksum();
+ }
+ }
+
+ @Test
+ void testSameRouteSameChecksumInEveryRun() throws Exception {
+ String first = checksum("orders", orders("mock:out"));
+ String second = checksum("orders", orders("mock:out"));
+
+ assertNotNull(first);
+ assertTrue(first.matches("[0-9a-f]{12}"), first);
+ assertEquals(first, second);
+ }
+
+ @Test
+ void testEditedRouteNewChecksum() throws Exception {
+ String before = checksum("orders", orders("mock:out"));
+
+ assertNotEquals(before, checksum("orders", orders("mock:other")));
+ // an option of an EIP is part of the route too
+ String sequential = checksum("orders", new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+ .split(body())
+ .to("log:line")
+ .end()
+ .to("mock:out");
+ }
+ });
+ assertNotEquals(before, sequential);
+ }
+
+ @Test
+ void testGeneratedIdsAndOtherRoutesLeftOut() throws Exception {
+ String alone = checksum("orders", orders("mock:out"));
+ // another route first: the ids Camel generates for the steps of
orders are not the same
+ String after = checksum("orders", new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:audit").to("log:audit").split(body()).to("log:a").end();
+ }
+ }, orders("mock:out"));
+
+ assertEquals(alone, after);
+ }
+
+ @Test
+ void testRouteConfigurationsLeftOut() throws Exception {
+ String plain = checksum("orders", orders("mock:out"));
+ // the onException of a route configuration is merged into every
route, which are not changed by it
+ String configured = checksum("orders", new RouteConfigurationBuilder()
{
+ @Override
+ public void configuration() {
+
routeConfiguration().onException(Exception.class).handled(true).to("log:failed");
+ }
+ }, orders("mock:out"));
+ assertEquals(plain, configured);
+
+ // one the route has itself is part of it
+ String own = checksum("orders", new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+
.onException(Exception.class).handled(true).to("log:failed").end()
+ .split(body()).parallelProcessing()
+ .to("log:line")
+ .end()
+ .to("mock:out");
+ }
+ });
+ assertNotEquals(plain, own);
+ }
+
+ @Test
+ void testPlaceholdersAsWritten() throws Exception {
+ String a = checksum("orders", context ->
context.getPropertiesComponent().setInitialProperties(props("mock:a")),
+ orders("{{target}}"));
+ String b = checksum("orders", context ->
context.getPropertiesComponent().setInitialProperties(props("mock:b")),
+ orders("{{target}}"));
+
+ // another value is configuration, not another route
+ assertEquals(a, b);
+ }
+
+ @Test
+ void testSourceLocationsLeftOutAlsoWhileDebugging() throws Exception {
+ String plain = checksum("orders", orders("mock:out"));
+ String debugging = checksum("orders", context -> {
+ context.setSourceLocationEnabled(true);
+ context.setDebugging(true);
+ }, orders("mock:out"));
+
+ assertEquals(plain, debugging);
+ }
+
+ @Test
+ void testRevisionCountsTheChanges() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(orders("mock:out"));
+ context.start();
+ assertEquals(1, context.getRoute("orders").getRevision());
+
+ // removed and added again as it was: the same revision
+ replace(context, orders("mock:out"));
+ assertEquals(1, context.getRoute("orders").getRevision());
+
+ // changed: the next revision, which it keeps when it comes back
the same
+ replace(context, orders("mock:other"));
+ assertEquals(2, context.getRoute("orders").getRevision());
+ replace(context, orders("mock:other"));
+ assertEquals(2, context.getRoute("orders").getRevision());
+
+ // changed back: still a change in this run
+ replace(context, orders("mock:out"));
+ assertEquals(3, context.getRoute("orders").getRevision());
+ }
+ }
+
+ @Test
+ void testTemplatedRouteSameChecksumWhenCreatedAgain() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
routeTemplate("orders-template").templateParameter("target")
+ .from("direct:orders")
+ .split(body())
+ .to("{{target}}");
+ }
+ });
+ context.start();
+
+ String first = addOrders(context);
+ assertEquals(1, context.getRoute("orders").getRevision());
+
+ // created again, as a reload does: the node prefix comes from a
counter, which is no part of the checksum
+ context.getRouteController().stopRoute("orders");
+ context.removeRoute("orders");
+ String second = addOrders(context);
+
+ assertNotNull(first);
+ assertEquals(first, second);
+ assertEquals(1, context.getRoute("orders").getRevision());
+ }
+ }
+
+ private static String addOrders(CamelContext context) {
+ TemplatedRouteBuilder.builder(context,
"orders-template").routeId("orders").parameter("target", "mock:out")
+ .add();
+ return context.getRoute("orders").getChecksum();
+ }
+
+ @Test
+ void testSecretsInUrisMasked() throws Exception {
+ // the checksum is printed beside the masked URIs: it must not tell
one literal password from another
+ String a = checksum("orders",
orders("stub:http://localhost/orders?password=secret1"));
+ String b = checksum("orders",
orders("stub:http://localhost/orders?password=secret2"));
+ String other = checksum("orders",
orders("stub:http://localhost/other?password=secret1"));
+
+ assertEquals(a, b);
+ assertNotEquals(a, other);
+ }
+
+ @Test
+ void testRevisionKnownWhenTheRouteIsAnnounced() throws Exception {
+ List<Integer> revisions = new ArrayList<>();
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.getManagementStrategy().addEventNotifier(new
EventNotifierSupport() {
+ @Override
+ public void notify(CamelEvent event) {
+ if (event instanceof RouteAddedEvent added) {
+ revisions.add(added.getRoute().getRevision());
+ }
+ }
+ });
+ context.addRoutes(orders("mock:out"));
+ context.start();
+ replace(context, orders("mock:other"));
+ }
+
+ // 0 would say the checksum is not known
+ assertEquals(List.of(1, 2), revisions);
+ }
+
+ private static void replace(CamelContext context, RoutesBuilder builder)
throws Exception {
+ context.getRouteController().stopRoute("orders");
+ context.removeRoute("orders");
+ context.addRoutes(builder);
+ }
+
+ private static Properties props(String target) {
+ Properties answer = new Properties();
+ answer.put("target", target);
+ return answer;
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadChangedRoutesTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadChangedRoutesTest.java
new file mode 100644
index 000000000000..ce3dd8d7fb9d
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadChangedRoutesTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.support;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Route;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.engine.DefaultRoutesLoader;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.spi.RoutesLoader;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25771: the summary of a reload says how many routes the reload
changed, and marks them: a route that came back
+ * with another checksum is changed, one that was not there before is new.
+ */
+class RouteWatcherReloadChangedRoutesTest extends ContextTestSupport {
+
+ @TempDir
+ Path dir;
+
+ private final List<String> lines = new CopyOnWriteArrayList<>();
+ private Appender appender;
+ private NextRoutesLoader loader;
+ private RouteWatcherReloadStrategy strategy;
+
+ private static RouteBuilder routes(String ordersTarget, boolean audit) {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders").to(ordersTarget);
+ from("direct:stock").routeId("stock").to("mock:stock");
+ if (audit) {
+ from("direct:audit").routeId("audit").to("mock:audit");
+ }
+ }
+ };
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return routes("mock:out", false);
+ }
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+ loader = new NextRoutesLoader();
+ loader.setCamelContext(context);
+
context.getCamelContextExtension().addContextPlugin(RoutesLoader.class, loader);
+ strategy = new RouteWatcherReloadStrategy();
+ strategy.setCamelContext(context);
+ strategy.start();
+
+ appender = new AbstractAppender("reload-summary", null, null, true,
Property.EMPTY_ARRAY) {
+ @Override
+ public void append(LogEvent event) {
+ if
(RouteWatcherReloadStrategy.class.getName().equals(event.getLoggerName())) {
+ lines.add(event.getMessage().getFormattedMessage());
+ }
+ }
+ };
+ appender.start();
+ LoggerContext logging = (LoggerContext) LogManager.getContext(false);
+ logging.getConfiguration().getRootLogger().addAppender(appender,
Level.ALL, null);
+ logging.updateLoggers();
+ }
+
+ @AfterEach
+ void stopCapture() {
+ strategy.stop();
+ LoggerContext logging = (LoggerContext) LogManager.getContext(false);
+
logging.getConfiguration().getRootLogger().removeAppender(appender.getName());
+ logging.updateLoggers();
+ appender.stop();
+ }
+
+ @Test
+ void theSummarySaysWhichRoutesTheReloadChanged() throws Exception {
+ // the file now sends the orders elsewhere and has a new route; the
stock route is as it was
+ loader.next = routes("mock:other", true);
+
+ strategy.onRouteReload(List.of(resource()), false);
+
+ assertTrue(lines.contains("Routes reloaded summary (total:3 started:3
changed:2)"), lines.toString());
+ assertTrue(line("orders").endsWith("(changed)"), line("orders"));
+ assertTrue(line("audit").endsWith("(new)"), line("audit"));
+ assertTrue(line("stock").endsWith(")") &&
!line("stock").contains("changed"), line("stock"));
+ assertEquals(2, context.getRoute("orders").getRevision());
+ assertEquals(1, context.getRoute("stock").getRevision());
+
+ // saved again as it is: nothing changed
+ lines.clear();
+ strategy.onRouteReload(List.of(resource()), false);
+ assertTrue(lines.contains("Routes reloaded summary (total:3 started:3
changed:0)"), lines.toString());
+ assertEquals(2, context.getRoute("orders").getRevision());
+ }
+
+ private String line(String routeId) {
+ return lines.stream().filter(l -> l.contains(" " + routeId + "
(")).findFirst().orElse("");
+ }
+
+ private Resource resource() throws Exception {
+ Path file = Files.writeString(dir.resolve("routes.yaml"), "- route:
{}\n");
+ return ResourceHelper.resolveMandatoryResource(context, "file:" +
file);
+ }
+
+ // loads the next version of the routes, whatever the file has
+ private static class NextRoutesLoader extends DefaultRoutesLoader {
+ private RoutesBuilder next;
+
+ @Override
+ public Set<String> updateRoutes(Collection<Resource> resources) throws
Exception {
+ for (Route route : List.copyOf(getCamelContext().getRoutes())) {
+
getCamelContext().getRouteController().stopRoute(route.getId());
+ getCamelContext().removeRoute(route.getId());
+ }
+ getCamelContext().addRoutes(next);
+ Set<String> ids = new LinkedHashSet<>();
+ getCamelContext().getRoutes().forEach(r -> ids.add(r.getId()));
+ return ids;
+ }
+ }
+}
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 d16ac9e8043f..a2d9cb08e47d 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 checksum of the route as written, the
same in every run: compare it to tell whether the route changed (null when
camel-xml-io is not on the classpath)")
+ String getChecksum();
+
+ @ManagedAttribute(description = "The revision of the route in this run: 1
when it is added, one more each time it is added again with another checksum,
such as by a reload (0 when the checksum is not known)")
+ int getRevision();
+
@ManagedAttribute(description = "The number of steps that have been
reached with more than one body type, across the messages (recorded while the
message history is on)")
int getVariedBodyTypes();
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 ccf087f26c87..cff094fc7858 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
@@ -205,6 +205,16 @@ public class ManagedRoute extends
ManagedPerformanceCounter implements ManagedRo
return sourceLocationShort;
}
+ @Override
+ public String getChecksum() {
+ return route.getChecksum();
+ }
+
+ @Override
+ public int getRevision() {
+ return route.getRevision();
+ }
+
@Override
public int getVariedBodyTypes() {
BodyTypeRegistry registry =
context.getCamelContextExtension().getContextPlugin(BodyTypeRegistry.class);
diff --git
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteChecksumTest.java
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteChecksumTest.java
new file mode 100644
index 000000000000..a3b0d87ceada
--- /dev/null
+++
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteChecksumTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25771: a managed route has the checksum of the route as written, and
its revision in this run.
+ */
+@DisabledOnOs(OS.AIX)
+class ManagedRouteChecksumTest extends ManagementTestSupport {
+
+ @Test
+ void testChecksumAndRevision() throws Exception {
+ MBeanServer mbeanServer = getMBeanServer();
+ ObjectName on = getCamelObjectName(TYPE_ROUTE, "myRoute");
+
+ String checksum = (String) mbeanServer.getAttribute(on, "Checksum");
+ assertNotNull(checksum);
+ assertTrue(checksum.matches("[0-9a-f]{12}"), checksum);
+ assertEquals(context.getRoute("myRoute").getChecksum(), checksum);
+ assertEquals(1, mbeanServer.getAttribute(on, "Revision"));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start").routeId("myRoute").to("mock:result");
+ }
+ };
+ }
+}
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 eb2027a783f4..1d97fec5da47 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
@@ -27,6 +27,7 @@ import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -578,6 +579,10 @@ public class RouteWatcherReloadStrategy extends
FileWatcherResourceReloadStrateg
});
}
+ // the revision of each route before the reload, to tell the routes it
changed (CAMEL-25771)
+ Map<String, Integer> revisions = new HashMap<>();
+ getCamelContext().getRoutes().forEach(r -> revisions.put(r.getId(),
r.getRevision()));
+
try {
// should all existing routes be stopped and removed first?
if (removeAllRoutes) {
@@ -666,6 +671,9 @@ public class RouteWatcherReloadStrategy extends
FileWatcherResourceReloadStrateg
List<String> lines = new ArrayList<>();
int total = 0;
int started = 0;
+ // the routes the reload changed, known when the routes have
checksums (camel-xml-io)
+ int changed = 0;
+ boolean checksums = true;
for (String id : ids) {
total++;
Route route = getCamelContext().getRoute(id);
@@ -686,10 +694,24 @@ public class RouteWatcherReloadStrategy extends
FileWatcherResourceReloadStrateg
if (loc == null) {
loc = "";
}
- lines.add(String.format(" %s %s (%s) (source: %s)",
status, id, uri, loc));
+ String change = "";
+ if (route.getRevision() == 0) {
+ checksums = false;
+ } else if (!revisions.containsKey(id)) {
+ change = " (new)";
+ changed++;
+ } else if (revisions.get(id) != route.getRevision()) {
+ change = " (changed)";
+ changed++;
+ }
+ lines.add(String.format(" %s %s (%s) (source:
%s)%s", status, id, uri, loc, change));
}
}
- LOG.info("Routes reloaded summary (total:{} started:{})",
total, started);
+ if (checksums) {
+ LOG.info("Routes reloaded summary (total:{} started:{}
changed:{})", total, started, changed);
+ } else {
+ LOG.info("Routes reloaded summary (total:{} started:{})",
total, started);
+ }
// if we are default/verbose then log each route line
if (getCamelContext().getStartupSummaryLevel() ==
StartupSummaryLevel.Default
|| getCamelContext().getStartupSummaryLevel() ==
StartupSummaryLevel.Verbose) {
diff --git
a/core/camel-xml-io/src/generated/resources/META-INF/services/org/apache/camel/model-checksum-generator
b/core/camel-xml-io/src/generated/resources/META-INF/services/org/apache/camel/model-checksum-generator
new file mode 100644
index 000000000000..4f7b3a5910dd
--- /dev/null
+++
b/core/camel-xml-io/src/generated/resources/META-INF/services/org/apache/camel/model-checksum-generator
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.xml.LwModelChecksumGenerator
diff --git
a/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelChecksumGenerator.java
b/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelChecksumGenerator.java
new file mode 100644
index 000000000000..208c3d956695
--- /dev/null
+++
b/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelChecksumGenerator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.xml;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.util.HexFormat;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.NamedNode;
+import org.apache.camel.spi.ModelChecksumGenerator;
+import org.apache.camel.spi.annotations.JdkService;
+import org.apache.camel.xml.out.ModelWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link ModelChecksumGenerator} based on the generated {@link ModelWriter},
which writes every option of the model:
+ * the SHA-256 of the definition as XML, with property placeholders as
written, without the ids Camel generates (also
+ * those a route template or Kamelet makes from a counter), without source
locations (also while debugging), and with
+ * the secrets in endpoint URIs masked. The first 12 hex characters of it are
the checksum.
+ */
+@JdkService(ModelChecksumGenerator.FACTORY)
+public class LwModelChecksumGenerator implements ModelChecksumGenerator {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(LwModelChecksumGenerator.class);
+
+ /** The bytes of the digest the checksum has: 12 hex characters. */
+ private static final int CHECKSUM_BYTES = 6;
+
+ @Override
+ public String generateChecksum(CamelContext context, NamedNode definition)
{
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ try (Writer out = new OutputStreamWriter(
+ new DigestOutputStream(OutputStream.nullOutputStream(),
digest), StandardCharsets.UTF_8)) {
+ LwModelToXMLDumper.write(context, definition, out, false,
false, false, true);
+ }
+ return HexFormat.of().formatHex(digest.digest(), 0,
CHECKSUM_BYTES);
+ } catch (Exception e) {
+ LOG.debug("Cannot compute the checksum of {} due to {}",
definition, e.getMessage(), e);
+ return null;
+ }
+ }
+}
diff --git
a/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java
b/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java
index 915e4cceb4a1..8dc9459b7c95 100644
---
a/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java
+++
b/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java
@@ -18,14 +18,21 @@ package org.apache.camel.xml;
import java.io.IOException;
import java.io.StringWriter;
+import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
@@ -54,6 +61,7 @@ import org.apache.camel.spi.NamespaceAware;
import org.apache.camel.spi.annotations.JdkService;
import org.apache.camel.util.KeyValueHolder;
import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.URISupport;
import org.apache.camel.xml.out.BaseWriter;
import org.apache.camel.xml.out.ModelWriter;
@@ -65,6 +73,10 @@ import static
org.apache.camel.model.ProcessorDefinitionHelper.filterTypeInOutpu
@JdkService(ModelToXMLDumper.FACTORY)
public class LwModelToXMLDumper implements ModelToXMLDumper {
+ // the separator before a query parameter, and its value; the & after it
is not part of the match
+ private static final Pattern HASH_PARAMETER =
Pattern.compile("([?&])hash=([0-9a-f]+)(?=&|$)");
+ private static final Pattern ROUTE_ID_PARAMETER =
Pattern.compile("([?&])routeId=([^&]*)(?=&|$)");
+
@Override
public String dumpModelAsXml(CamelContext context, NamedNode definition)
throws Exception {
return dumpModelAsXml(context, definition, false, true, false);
@@ -75,17 +87,42 @@ public class LwModelToXMLDumper implements ModelToXMLDumper
{
CamelContext context, NamedNode definition, boolean
resolvePlaceholders, boolean generatedIds,
boolean sourceLocation)
throws Exception {
+ StringWriter buffer = new StringWriter();
+ // while debugging the source locations are written too, for the
debugger to show where a step is
+ write(context, definition, buffer, resolvePlaceholders, generatedIds,
sourceLocation || context.isDebugging(),
+ false);
+ return buffer.toString();
+ }
+
+ /**
+ * Writes the definition as XML: the dump, and the text the checksum of a
route is computed from
+ * ({@link LwModelChecksumGenerator}), which never has the source
locations.
+ *
+ * @param checksum whether the text is for the checksum: then what Camel
makes from a counter or an identity hash is
+ * left out (the node prefix and the route id that a route
template or Kamelet makes, the hash of
+ * the endpoint DSL options that are objects), and the
secrets in endpoint URIs are masked
+ */
+ static void write(
+ CamelContext context, NamedNode definition, Writer out, boolean
resolvePlaceholders, boolean generatedIds,
+ boolean sourceLocation, boolean checksum)
+ throws Exception {
Properties properties = new Properties();
Map<String, String> namespaces = new LinkedHashMap<>();
Map<String, KeyValueHolder<Integer, String>> locations = new
HashMap<>();
List<Runnable> restorers = new ArrayList<>();
+ // the definitions whose id Camel makes from a counter, though it is
marked as custom, and those ids
+ Set<OptionalIdentifiedDefinition<?>> counterIds =
Collections.newSetFromMap(new IdentityHashMap<>());
+ Set<String> counterRouteIds = new HashSet<>();
Consumer<RouteDefinition> extractor = route -> {
extractNamespaces(route, namespaces);
- if (sourceLocation || context.isDebugging()) {
+ if (sourceLocation) {
extractSourceLocations(route, locations);
}
- restorers.add(resolveEndpointDslUris(route));
+ restorers.add(resolveEndpointDslUris(route, checksum));
+ if (checksum) {
+ restorers.add(leaveOutCounters(route, counterIds,
counterRouteIds));
+ }
if (Boolean.TRUE.equals(route.isTemplate())) {
Map<String, Object> parameters = route.getTemplateParameters();
if (parameters != null) {
@@ -94,13 +131,12 @@ public class LwModelToXMLDumper implements
ModelToXMLDumper {
}
};
- StringWriter buffer = new StringWriter();
- ModelWriter writer = new ModelWriter(buffer,
BaseWriter.DEFAULT_NAMESPACE) {
+ ModelWriter writer = new ModelWriter(out,
BaseWriter.DEFAULT_NAMESPACE) {
@Override
protected void
doWriteOptionalIdentifiedDefinitionAttributes(OptionalIdentifiedDefinition<?>
def)
throws IOException {
- if (generatedIds || Boolean.TRUE.equals(def.getCustomId())) {
+ if ((generatedIds || Boolean.TRUE.equals(def.getCustomId()))
&& !counterIds.contains(def)) {
// write id
doWriteAttribute("id", def.getId());
}
@@ -113,7 +149,7 @@ public class LwModelToXMLDumper implements ModelToXMLDumper
{
doWriteAttribute("note", def.getNote());
}
// write location information
- if (sourceLocation || context.isDebugging()) {
+ if (sourceLocation) {
int line = (def instanceof RouteDefinition ?
((RouteDefinition) def).getInput() : def).getLineNumber();
if (line != -1) {
writer.addAttribute("sourceLineNumber",
Integer.toString(line));
@@ -181,6 +217,15 @@ public class LwModelToXMLDumper implements
ModelToXMLDumper {
if (resolvePlaceholders && value != null) {
value = resolve(value.toString(), properties);
}
+ if (checksum && "uri".equals(name) && value != null) {
+ String uri = value.toString();
+ for (String id : counterRouteIds) {
+ // kamelet:source and kamelet:sink have the id of
their route
+ uri = withoutRouteId(uri, id);
+ }
+ // a literal password is no part of the checksum, which is
printed beside the masked URIs
+ value = URISupport.sanitizeUri(uri);
+ }
super.attribute(name, value);
}
@@ -212,8 +257,6 @@ public class LwModelToXMLDumper implements ModelToXMLDumper
{
} finally {
restorers.forEach(Runnable::run);
}
-
- return buffer.toString();
}
@Override
@@ -320,25 +363,78 @@ public class LwModelToXMLDumper implements
ModelToXMLDumper {
}
}
+ /**
+ * The URI of an endpoint DSL builder, for the checksum without the hash
of its options that are objects: an
+ * identity hash, another one in every run. The objects are beans, which
the checksum leaves out.
+ */
+ static String rawUri(String uri, boolean checksum) {
+ return checksum && uri != null ? withoutParameter(uri, HASH_PARAMETER,
null) : uri;
+ }
+
+ /** The URI without its routeId parameter of that id, which kamelet:source
and kamelet:sink have. */
+ static String withoutRouteId(String uri, String id) {
+ return withoutParameter(uri, ROUTE_ID_PARAMETER, id);
+ }
+
+ /**
+ * The URI without the first query parameter the pattern matches, whose
value is the given value, or any value when
+ * it is null.
+ */
+ static String withoutParameter(String uri, Pattern parameter, String
value) {
+ Matcher matcher = parameter.matcher(uri);
+ while (matcher.find()) {
+ if (value == null || value.equals(matcher.group(2))) {
+ if (matcher.end() < uri.length()) {
+ // more options follow: keep the separator before it, and
drop the & after it
+ return uri.substring(0, matcher.start() + 1) +
uri.substring(matcher.end() + 1);
+ }
+ return uri.substring(0, matcher.start());
+ }
+ }
+ return uri;
+ }
+
+ /**
+ * Leaves out of the checksum what a route template or a Kamelet makes
from a counter when it creates the route: the
+ * node prefix (RouteTemplateDefinition.asRouteDefinition and
Kamelet.templateToRoute), and the id of a Kamelet
+ * route that has none of its own, the template id and that prefix, which
its kamelet:source and kamelet:sink have
+ * too. The same route then has the same checksum each time it is created,
such as when a reload creates it again.
+ */
+ private static Runnable leaveOutCounters(
+ RouteDefinition route, Set<OptionalIdentifiedDefinition<?>>
counterIds, Set<String> counterRouteIds) {
+ if (!Boolean.TRUE.equals(route.isTemplate())) {
+ return () -> {
+ };
+ }
+ String prefix = route.getNodePrefixId();
+ if (prefix != null && Boolean.TRUE.equals(route.isKamelet()) &&
route.getId() != null
+ && route.getId().endsWith("-" + prefix)) {
+ counterIds.add(route);
+ counterRouteIds.add(route.getId());
+ }
+ route.setNodePrefixId(null);
+ return () -> route.setNodePrefixId(prefix);
+ }
+
/**
* If the route has been built with endpoint-dsl, then the model will not
have uri set which then cannot be included
* in the JAXB model dump. Returns a {@link Runnable} that restores the
original endpoint DSL builders after the XML
* has been written, so that the live {@link RouteDefinition} is not
permanently mutated by the dump.
*/
@SuppressWarnings("rawtypes")
- private static Runnable resolveEndpointDslUris(RouteDefinition route) {
+ private static Runnable resolveEndpointDslUris(RouteDefinition route,
boolean checksum) {
List<Runnable> restorers = new ArrayList<>();
FromDefinition from = route.getInput();
if (from != null && from.getEndpointConsumerBuilder() != null) {
EndpointConsumerBuilder builder =
from.getEndpointConsumerBuilder();
- from.setUri(builder.getRawUri());
+ from.setUri(rawUri(builder.getRawUri(), checksum));
restorers.add(() -> from.setEndpointConsumerBuilder(builder));
}
Collection<SendDefinition> col =
filterTypeInOutputs(route.getOutputs(), SendDefinition.class);
for (SendDefinition<?> to : col) {
if (to.getEndpointProducerBuilder() != null) {
EndpointProducerBuilder builder =
to.getEndpointProducerBuilder();
- to.setUri(builder.getRawUri());
+ to.setUri(rawUri(builder.getRawUri(), checksum));
restorers.add(() -> to.setEndpointProducerBuilder(builder));
}
}
@@ -346,7 +442,7 @@ public class LwModelToXMLDumper implements ModelToXMLDumper
{
for (ToDynamicDefinition to : col2) {
if (to.getEndpointProducerBuilder() != null) {
EndpointProducerBuilder builder =
to.getEndpointProducerBuilder();
- to.setUri(builder.getRawUri());
+ to.setUri(rawUri(builder.getRawUri(), checksum));
// ToDynamicDefinition.setUri does not call clear(), so the
builder is preserved;
// only the uri field needs to be reset.
restorers.add(() -> to.setUri(null));
diff --git
a/core/camel-xml-io/src/test/java/org/apache/camel/xml/LwModelToXMLDumperRawUriTest.java
b/core/camel-xml-io/src/test/java/org/apache/camel/xml/LwModelToXMLDumperRawUriTest.java
new file mode 100644
index 000000000000..3abc70cc01b9
--- /dev/null
+++
b/core/camel-xml-io/src/test/java/org/apache/camel/xml/LwModelToXMLDumperRawUriTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.xml;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * CAMEL-25771: the hash an endpoint DSL builder adds for its options that are
objects is an identity hash, another one
+ * in every run, so the checksum of a route leaves it out.
+ */
+class LwModelToXMLDumperRawUriTest {
+
+ @Test
+ void theHashOfTheObjectOptionsIsLeftOutOfTheChecksum() {
+ assertEquals("sql:select?a=1&b=2",
LwModelToXMLDumper.rawUri("sql:select?a=1&hash=1f2e3d&b=2", true));
+ assertEquals("sql:select?b=2",
LwModelToXMLDumper.rawUri("sql:select?hash=1f2e3d&b=2", true));
+ assertEquals("sql:select?a=1",
LwModelToXMLDumper.rawUri("sql:select?a=1&hash=1f2e3d", true));
+ assertEquals("sql:select",
LwModelToXMLDumper.rawUri("sql:select?hash=1f2e3d", true));
+ assertEquals("sql:select?ahash=1f2e3d",
LwModelToXMLDumper.rawUri("sql:select?ahash=1f2e3d", true));
+ }
+
+ @Test
+ void theRouteIdOfTheRouteIsLeftOut() {
+ assertEquals("kamelet:source?routeId=other",
+
LwModelToXMLDumper.withoutRouteId("kamelet:source?routeId=other&routeId=my-route",
"my-route"));
+ assertEquals("kamelet:source?a=1",
+
LwModelToXMLDumper.withoutRouteId("kamelet:source?routeId=my-route&a=1",
"my-route"));
+ assertEquals("kamelet:source?routeId=my-route2",
+
LwModelToXMLDumper.withoutRouteId("kamelet:source?routeId=my-route2",
"my-route"));
+ }
+
+ @Test
+ void theDumpKeepsTheHash() {
+ assertEquals("sql:select?a=1&hash=1f2e3d",
LwModelToXMLDumper.rawUri("sql:select?a=1&hash=1f2e3d", false));
+ }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
index 62747d7ff785..65bb5d422055 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
@@ -184,6 +184,19 @@ Give a source file instead of a running integration to see
the structure without
by the Java DSL parser, without compiling it; use `--compile` to compile it
instead.
`--brief` shortens each node, and `--description` shows the description of
each node instead of its code.
+Each route has its xref:routes.adoc#_route_checksum_and_revision[checksum]
under its source, from a running
+integration and from a source file alike: a route read from its file has the
same checksum as when it runs, until it is
+changed. Compare it with `camel get route --checksum` to tell whether the
routes that run are the ones in the file. A
+Java source file read without compiling it has the checksum only when the
parser read all of it (else use `--compile`).
+
+[source,text]
+----
+Source: orders.camel.yaml
+Checksum: aa4535cb1725
+--------------------------------------------------------------------------------
+ 4: route[orders]
+----
+
==== Body types
When the integration runs with the message history on, such as with `camel run
--dev`, each step also shows the body
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index 7c0310bfda1d..74d1b4cc13f4 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -154,6 +154,11 @@ exchanges, and a route such as `direct:audit` has the
remote exchanges of the ro
`camel get processor --remote` and `camel top route --remote`, and the JSON of
these commands has the remote counters
as `remoteTotal`, `remoteFailed` and `remoteInflight`.
+Use `--checksum` to show the
xref:routes.adoc#_route_checksum_and_revision[checksum and revision] of each
route. The
+checksum is the same in every run while the route is not changed, so comparing
it with an earlier run tells which routes
+changed; the revision counts the times a reload changed the route since the
integration started. The JSON output
+(`--json`) always has them, as `checksum` and `revision`.
+
TIP: Run `camel get --help` to see all available sub-commands.
== Top status
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
index 7e5f3770b2c4..2e5da04f11f1 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-route.adoc
@@ -19,6 +19,7 @@ camel get route [options]
[cols="2,5,1,2",options="header"]
|===
| Option | Description | Default | Type
+| `--checksum` | Include the checksum and revision of each route: the checksum
is the same in every run while the route is not changed, and the revision
counts the times a reload changed it | | boolean
| `--description` | Include description in the ID column (if available) | |
boolean
| `--duration` | How long to keep watching or following, in millis or as a
duration such as 30s (default: until Enter is pressed) | | long
| `--error` | Shows detailed information for routes that has error status | |
boolean
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
index 889a4ec5d2b0..6dd64fa4440f 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-top-route.adoc
@@ -19,6 +19,7 @@ camel top route [options]
[cols="2,5,1,2",options="header"]
|===
| Option | Description | Default | Type
+| `--checksum` | Include the checksum and revision of each route: the checksum
is the same in every run while the route is not changed, and the revision
counts the times a reload changed it | | boolean
| `--description` | Include description in the ID column (if available) | |
boolean
| `--duration` | How long to keep watching or following, in millis or as a
duration such as 30s (default: until Enter is pressed) | | long
| `--error` | Shows detailed information for routes that has error status | |
boolean
diff --git a/docs/user-manual/modules/ROOT/pages/route-reload.adoc
b/docs/user-manual/modules/ROOT/pages/route-reload.adoc
index 3ffcd96d5350..f1f394dc56ce 100644
--- a/docs/user-manual/modules/ROOT/pages/route-reload.adoc
+++ b/docs/user-manual/modules/ROOT/pages/route-reload.adoc
@@ -71,6 +71,22 @@ This is necessary because Apache Camel must stop the
existing routes from runnin
And adding new routes is therefore possible as they would have a new unique
route id specified.
+=== Which routes changed
+
+The summary that is logged after a reload says how many of the reloaded routes
changed, and marks each one: a route
+that came back with another
xref:routes.adoc#_route_checksum_and_revision[checksum] is `(changed)`, and a
route that
+was not there before is `(new)`. A route that is saved again as it was is not
marked.
+
+[source,text]
+----
+Routes reloaded summary (total:3 started:3 changed:2)
+ Started orders (direct://orders) (source: orders.yaml:3) (changed)
+ Started stock (direct://stock) (source: orders.yaml:9)
+ Started audit (direct://audit) (source: orders.yaml:14) (new)
+----
+
+The revision of a route counts the times a reload changed it in this run.
+
=== Live reload of resource files
In addition to reloading routes, when `routesReloadEnabled=true` Camel also
disables the
diff --git a/docs/user-manual/modules/ROOT/pages/routes.adoc
b/docs/user-manual/modules/ROOT/pages/routes.adoc
index eac3d85f8de7..799e585f1e9a 100644
--- a/docs/user-manual/modules/ROOT/pages/routes.adoc
+++ b/docs/user-manual/modules/ROOT/pages/routes.adoc
@@ -259,6 +259,45 @@ YAML::
----
====
+== Route Checksum and Revision
+
+Each route has a _checksum_ of the route as written, which tools such as the
Camel CLI, an IDE or an AI agent use to
+tell which routes changed between two runs of an application, or after a
reload. The same route gives the same checksum
+in every run.
+
+The checksum is a short hash (12 hex characters) of the whole route: its
steps, their options and expressions, with
+property placeholders as written. It leaves out what does not change the route:
+
+* the ids Camel generates (a route or step with an id of its own keeps it in
the checksum), also the node prefix that a
+route template or a Kamelet gives the routes it creates, and the id of a
Kamelet route without one of its own: the same
+templated route has the same checksum each time it is created;
+* where the route is in its source: moving the route in its file, or adding a
route above it, keeps its checksum;
+* the parts that route configurations add to every route (their `onException`,
`intercept` and `onCompletion`): a
+change in a shared route configuration does not change every route. The
`onException` of a route itself is part of it;
+* the secrets in endpoint URIs, such as a password written in the URI, which
are masked as in the logs.
+
+Each route also has a _revision_ in the running application: it is 1 when the
route is added, and one more each time
+the route is added again with another checksum, such as when
xref:route-reload.adoc[route reloading] changed it. A
+reload that brings the route back as it was keeps its revision. The revision
follows the route id, and starts over
+when the application restarts: to compare two runs, compare the checksums.
Camel keeps the revisions of the 10000 route
+ids used last.
+
+The checksum and revision are on the `Route` (`getChecksum()` and
`getRevision()`), on the route in JMX (the
+`Checksum` and `Revision` attributes), in the `route` developer console, and
in the Camel CLI
+(`camel get route --checksum`). The `route-structure` and `route-dump`
developer consoles have the checksum of each
+route, so the facts a tool keeps per line of a route know which version of the
route they belong to. The Camel CLI
+gives the checksum of the routes of a source file without running it (`camel
cmd route-structure orders.camel.yaml`):
+it is the one the route has when it runs.
+
+The checksum needs `camel-xml-io` on the classpath, which comes with
`camel-core`, `camel-management` and the Camel
+CLI connector. Without it the routes have no checksum, and no revision.
+
+NOTE: The checksum covers the route, not the code it calls: changing a Java
lambda in `.process(...)`, or a bean the route
+calls, does not change it. Neither does a property value the route uses, which
is configuration, not another route, nor
+an object given to an endpoint DSL option, such as a `DataSource`, which is a
bean. A
+route without an id of its own gets an id such as `route3`, which can move to
another route when routes are added to or
+removed from its file: match such routes by their checksum.
+
== More Information
Check the xref:dsl.adoc[list of supported languages] that you can use for
writing Camel routes.
diff --git
a/dsl/camel-endpointdsl/src/test/java/org/apache/camel/builder/endpoint/EndpointDslRouteChecksumTest.java
b/dsl/camel-endpointdsl/src/test/java/org/apache/camel/builder/endpoint/EndpointDslRouteChecksumTest.java
new file mode 100644
index 000000000000..c4e553f4b8fd
--- /dev/null
+++
b/dsl/camel-endpointdsl/src/test/java/org/apache/camel/builder/endpoint/EndpointDslRouteChecksumTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.builder.endpoint;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.processor.DefaultExchangeFormatter;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * CAMEL-25771: an endpoint DSL option that is an object, such as an
ExchangeFormatter, is a bean, which the checksum of
+ * a route leaves out: the same route has the same checksum with another
instance.
+ */
+class EndpointDslRouteChecksumTest {
+
+ @Test
+ void anOptionThatIsAnObjectKeepsTheChecksum() throws Exception {
+ String first = checksum();
+ String second = checksum();
+
+ assertNotNull(first);
+ assertEquals(first, second);
+ }
+
+ private static String checksum() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new EndpointRouteBuilder() {
+ @Override
+ public void configure() {
+ from(direct("orders")).routeId("orders")
+ .to(log("out").advanced().exchangeFormatter(new
DefaultExchangeFormatter()));
+ }
+ });
+ context.start();
+ return context.getRoute("orders").getChecksum();
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 27d9042058ec..42e987a3ac18 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -15,7 +15,7 @@
{ "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
{ "name": "explain", "fullName": "explain", "description": "Explain what a
Camel route does using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars",
"javaType": "java.lang.String", "type": "string" }, { "names": "--api-type",
"description": "API type: 'ollama', 'openai' (OpenAI-compatible), ' [...]
{ "name": "export", "fullName": "export", "description": "Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names":
"--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "ty [...]
- { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity",
"fullName": " [...]
+ { "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity",
"fullName": " [...]
{ "name": "harden", "fullName": "harden", "description": "Suggest security
hardening for Camel routes using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String",
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama'
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...]
{ "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio
web console", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ {
"names": "--host", "description": "Hostname to bind the Hawtio web console to",
"defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string"
}, { "names": "--open-url,--openUrl", "description": "To automatic open Hawtio
web console in the web browser (without an interactive terminal the default
[...]
{ "name": "infra", "fullName": "infra", "description": "List and Run
external services for testing and prototyping", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ {
"names": "--json", "description": "Output in JSON Format", "javaType":
"boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display
the help and sub-commands", "javaType": "boolean", "type": "boolean" } ],
"subcommands": [ { "name": "get", "fullName": "infra [...]
@@ -33,7 +33,7 @@
{ "name": "semantic", "fullName": "semantic", "description": "List
semantic definitions and expert contracts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ {
"names": "--expert", "description": "Show the operations and parameter contract
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names":
"--json", "description": "Output a single JSON document", "javaType":
"boolean", "type": "boolean" }, { "names": "--timeou [...]
{ "name": "shell", "fullName": "shell", "description": "Interactive Camel
CLI shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell",
"options": [ { "names": "-h,--help", "description": "Display the help and
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
{ "name": "stop", "fullName": "stop", "description": "Shuts down running
Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ {
"names": "--kill", "description": "To force killing the process (SIGKILL)",
"javaType": "boolean", "type": "boolean" }, { "names": "--no-wait",
"description": "Do not wait for the integration to stop", "javaType":
"boolean", "type": "boolean" }, { "names": "--timeout", "description": "How
long to wa [...]
- { "name": "top", "fullName": "top", "description": "Top status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context",
"fullName": "top [...]
+ { "name": "top", "fullName": "top", "description": "Top status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context",
"fullName": "top [...]
{ "name": "trace", "fullName": "trace", "description": "Tail message
traces from running Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction", "options":
[ { "names": "--action", "description": "Action to start, stop, clear, list
status, or dump traces", "defaultValue": "status", "javaType":
"java.lang.String", "type": "string" }, { "names": "--ago", "description": "Use
ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "b [...]
{ "name": "transform", "fullName": "transform", "description": "Transform
message or Camel routes", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"dataweave", "fullName": "transform dataweave", "description": "Convert
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl.
[...]
{ "name": "update", "fullName": "update", "description": "Update Camel
project", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"list", "fullName": "update list", "description": "List available update
versions for Camel and its runtime variants", "sourceClass":
"org.apache.camel.dsl.jbang.cor [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
index 331a9fc698c4..e88334d1b733 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
@@ -168,6 +168,15 @@ public final class TransformJavaRoutes {
context.addRouteDefinitions(r.routes().getRoutes());
}
if ("json".equals(format)) {
+ // the checksum of each route as written, which is the one the
route has when it runs: only when the
+ // parser read the whole source, as a part it could not read
is missing from the routes
+ if (read.unread().isEmpty()) {
+ for (RouteDefinition route :
context.getRouteDefinitions()) {
+ if (route.getChecksum() == null) {
+
route.setChecksum(RouteDefinitionHelper.checksum(context, route));
+ }
+ }
+ }
// the ids a start assigns: the route structure looks each
route up by its id, and shows the step ids
// (a route id that is a property placeholder is kept as it
is, as there are no properties)
context.getPropertiesComponent().setIgnoreMissingProperty(true);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
index eac486c2ed0c..7a95ca448bab 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
@@ -163,6 +163,7 @@ public class CamelRouteStructureAction extends
ActionBaseCommand {
Row row = new Row();
row.location = extractSourceName(o.getString("source"));
row.routeId = o.getString("routeId");
+ row.checksum = o.getString("checksum");
if (!rows.contains(row)) {
List<JsonObject> lines = o.getCollection("code");
if (lines != null) {
@@ -267,6 +268,10 @@ public class CamelRouteStructureAction extends
ActionBaseCommand {
printer().println();
if (!raw) {
printer().printf("Source: %s%n", row.location);
+ if (row.checksum != null) {
+ // the same as the checksum of the running route while the
route is not changed
+ printer().printf("Checksum: %s%n", row.checksum);
+ }
printer().println("--------------------------------------------------------------------------------");
}
// the body types each step was reached with, as the running app
has seen them, line up in a column after
@@ -369,6 +374,7 @@ public class CamelRouteStructureAction extends
ActionBaseCommand {
private static class Row {
String location;
String routeId;
+ String checksum;
List<Code> code = new ArrayList<>();
@Override
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
index 76af869d1fbd..22a41c321819 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
@@ -114,6 +114,10 @@ public class CamelRouteStatus extends ProcessWatchCommand {
description = "Include group column")
boolean showGroup;
+ @CommandLine.Option(names = { "--checksum" },
+ description = "Include the checksum and revision of
each route: the checksum is the same in every run while the route is not
changed, and the revision counts the times a reload changed it")
+ boolean checksum;
+
public CamelRouteStatus(CamelJBangMain main) {
super(main);
}
@@ -149,6 +153,8 @@ public class CamelRouteStatus extends ProcessWatchCommand {
row.remote = bool;
}
row.source = o.getString("source");
+ row.checksum = o.getString("checksum");
+ row.revision = o.getIntegerOrDefault("revision", 0);
row.state = o.getString("state");
row.age = o.getString("uptime");
row.uptime = row.age != null ?
TimeUtils.toMilliSeconds(row.age) : 0;
@@ -307,6 +313,11 @@ public class CamelRouteStatus extends ProcessWatchCommand {
new
Column().header("FROM").visible(wideUri).dataAlign(HorizontalAlign.LEFT)
.maxWidth(fromWideW, OverflowBehaviour.NEWLINE)
.with(r -> r.from),
+ new
Column().header("CHECKSUM").visible(checksum).dataAlign(HorizontalAlign.LEFT)
+ .with(r -> r.checksum != null ? r.checksum : ""),
+ new
Column().header("REV").visible(checksum).headerAlign(HorizontalAlign.CENTER)
+ .dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> r.revision > 0 ? String.valueOf(r.revision)
: ""),
new
Column().header("REMOTE").visible(remoteVisible).headerAlign(HorizontalAlign.CENTER)
.dataAlign(HorizontalAlign.CENTER)
.with(this::getRemote),
@@ -337,6 +348,9 @@ public class CamelRouteStatus extends ProcessWatchCommand {
jo.put("group", r.group);
jo.put("from", r.from);
jo.put("remote", r.remote);
+ // the same in every run while the route is not changed, and the times
a reload changed it (when known)
+ jo.put("checksum", r.checksum);
+ jo.put("revision", r.revision > 0 ? r.revision : null);
jo.put("status", getStatus(r));
jo.put("age", r.age);
jo.put("uptime", r.uptime);
@@ -558,6 +572,8 @@ public class CamelRouteStatus extends ProcessWatchCommand {
String from;
boolean remote;
String source;
+ String checksum;
+ int revision;
String state;
String age;
String coverage;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteTop.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteTop.java
index 667ab4f19cbb..7d6f5ef9c414 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteTop.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteTop.java
@@ -63,6 +63,11 @@ public class CamelRouteTop extends CamelRouteStatus {
.with(this::getId),
new
Column().header("FROM").dataAlign(HorizontalAlign.LEFT).maxWidth(fromW,
OverflowBehaviour.ELLIPSIS_RIGHT)
.with(this::getFrom),
+ new
Column().header("CHECKSUM").visible(checksum).dataAlign(HorizontalAlign.LEFT)
+ .with(r -> r.checksum != null ? r.checksum : ""),
+ new
Column().header("REV").visible(checksum).headerAlign(HorizontalAlign.CENTER)
+ .dataAlign(HorizontalAlign.RIGHT)
+ .with(r -> r.revision > 0 ? String.valueOf(r.revision)
: ""),
new
Column().header("REMOTE").visible(remoteVisible).headerAlign(HorizontalAlign.CENTER)
.dataAlign(HorizontalAlign.CENTER)
.with(this::getRemote),
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
index 24de5f0b77b5..e9d693bc2f7b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
@@ -20,12 +20,16 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.UsesWorkingDirectory;
import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.ModelCamelContext;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
@@ -212,6 +216,58 @@ class RouteActionJavaSourceTest {
.contains("direct:next");
}
+ @Test
+ void structureHasTheChecksumTheRouteHasWhenItRuns(@TempDir Path dir)
throws Exception {
+ Path orders = write(dir, "Orders.java", """
+ import org.apache.camel.builder.RouteBuilder;
+
+ public class Orders extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+ .choice()
+ .when(simple("${header.vip} ==
true")).to("seda:vip")
+ .otherwise().to("seda:orders")
+ .end()
+ .log("${body}");
+ }
+ }
+ """);
+ StringPrinter printer = new StringPrinter();
+ CamelRouteStructureAction command = new CamelRouteStructureAction(new
CamelJBangMain().withPrinter(printer));
+ command.name = orders.toString();
+ command.jsonOutput = true;
+
+ assertThat(command.doCall()).isZero();
+ JsonObject structure = Jsoner.deserialize(printer.getOutput(), new
JsonObject());
+ JsonObject route = ((JsonArray)
structure.getCollection("routes")).getMap(0);
+ String read = route.getString("checksum");
+
+ // the same route, compiled and added to a CamelContext as when it runs
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+ .choice()
+ .when(simple("${header.vip} ==
true")).to("seda:vip")
+ .otherwise().to("seda:orders")
+ .end()
+ .log("${body}");
+ }
+ });
+ String running = ((ModelCamelContext)
context).getRouteDefinition("orders").getChecksum();
+ assertThat(read).isNotNull().isEqualTo(running);
+ }
+
+ // the text shows it under the source
+ printer = new StringPrinter();
+ command = new CamelRouteStructureAction(new
CamelJBangMain().withPrinter(printer));
+ command.name = orders.toString();
+ assertThat(command.doCall()).isZero();
+ assertThat(printer.getOutput()).contains("Source: Orders.java",
"Checksum: " + read);
+ }
+
private static Path write(Path dir, String name, String content) throws
Exception {
Path file = dir.resolve(name);
Files.writeString(file, content);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
index 40baa36fd3b5..8d31ed805a84 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
@@ -91,6 +91,34 @@ class CamelRouteStatusTest extends ProcessCommandTestSupport
{
assertThat(route2.get("mean")).isNull();
}
+ @Test
+ void checksumShowsTheChecksumAndRevisionOfEachRoute() throws Exception {
+ JsonObject changed = route("route1", "timer:tick", "Started",
stats(12, 1, 2, 25));
+ changed.put("checksum", "0a1b2c3d4e5f");
+ changed.put("revision", 2);
+ writeStatusFile(TEST_PID, integration("myApp", changed,
+ route("route2", "timer:tock", "Started", stats(0, 0, 0, -1))));
+
+ assertThat(call(command())).isZero();
+ assertThat(printer.getOutput()).doesNotContain("CHECKSUM",
"0a1b2c3d4e5f");
+
+ printer = new StringPrinter();
+ assertThat(call(command("--checksum"))).isZero();
+ String[] lines = printer.getOutput().split(System.lineSeparator());
+ assertThat(lines[0]).contains("CHECKSUM", "REV");
+ assertThat(lines[1]).contains("route1", "0a1b2c3d4e5f", " 2 ");
+
+ // the JSON has them whether asked for or not; a route of an older
Camel has none
+ printer = new StringPrinter();
+ assertThat(call(command("--json"))).isZero();
+ JsonArray routes = Jsoner.deserialize(printer.getOutput(), (JsonArray)
null);
+ JsonObject first = routes.getMap(0);
+ JsonObject second = routes.getMap(1);
+ assertThat(first.getString("checksum")).isEqualTo("0a1b2c3d4e5f");
+ assertThat(first.getInteger("revision")).isEqualTo(2);
+ assertThat(second.get("revision")).isNull();
+ }
+
@Test
void jsonWithoutRoutesIsAnEmptyArray() throws Exception {
writeStatusFile(TEST_PID, integration("myApp"));
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 64f26f63cffc..55017b894a70 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -146,7 +146,10 @@ public class RuntimeTools {
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "List Camel routes with their state, uptime, messages
processed, last error, and throughput statistics."
+ " A route with mixedBodyKinds has steps reached with
bodies of more than one kind, which need"
- + " attention: camel_runtime_route_structure with
varied=true shows them.")
+ + " attention: camel_runtime_route_structure with
varied=true shows them. The checksum of a route"
+ + " is the same in every run while the route is not
changed: compare it with an earlier run to"
+ + " tell whether a route changed, and match routes
with generated ids by it; revision counts the"
+ + " times a reload changed the route in this run.")
public JsonObject camel_runtime_routes(
@ToolArg(description = NAME_OR_PID_DESC, required = false) String
nameOrPid) {
return delegateToRegistry("get_routes", nameOrPid, Map.of());
@@ -244,7 +247,8 @@ public class RuntimeTools {
+ " a processor has bodyTypes: the body types it has
been reached with, across the messages, and"
+ " bodyKinds: null, payload, data or collection. More
than one kind needs attention: a step that"
+ " writes fails for data, one that reads fields fails
for a payload. The routes tool counts them"
- + " per route (mixedBodyKinds).")
+ + " per route (mixedBodyKinds). The checksum of each
route says which version of the route the"
+ + " facts of its lines are of.")
public JsonObject camel_runtime_route_structure(
@ToolArg(description = NAME_OR_PID_DESC, required = false) String
nameOrPid,
@ToolArg(description = "Route ID or pattern such as orders* to
inspect (default * for all routes)",