This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26823-topology-edge-reply in repository https://gitbox.apache.org/repos/asf/camel.git
commit c1ea3046394bf73634f1f947efd98dfc23e8e950 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Nov 11 08:10:52 2026 +0100 CAMEL-26823: camel-core - the route topology says whether a route waits for the answer of the route it sends to Each edge of the route topology gets reply: the route waits for the answer, as the send processor decides the exchange pattern (the exchangePattern of the endpoint uri, else the pattern of the send, else the pattern of the message the route consumed), or the component always runs the target first (direct, kamelet). A request-reply over a broker is then told from a hand-off. An error path never waits. A wireTap or a toD to an endpoint the route names, not one it computes, is an edge too (a wire tap never waits), and an external endpoint when no route consumes from it. TopologyEdge has the new reply component and keeps its seven-argument constructor; the route-topology console and the topology JSON of the dump routes strategy write it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/catalog/dev-consoles-openapi.json | 9 +- .../camel/catalog/dev-consoles/route-topology.json | 9 +- .../org/apache/camel/spi/RouteTopologyDumper.java | 16 +++- .../apache/camel/dev-console/route-topology.json | 9 +- .../impl/console/RouteTopologyDevConsole.java | 14 +++- .../impl/console/RouteTopologyDevConsoleTest.java | 26 +++++- .../camel/impl/DefaultDumpRoutesStrategy.java | 3 + .../camel/impl/DefaultRouteTopologyDumper.java | 88 +++++++++++++++++-- .../DefaultDumpRoutesStrategyTopologyJsonTest.java | 2 + .../impl/DefaultRouteTopologyDumperReplyTest.java | 98 ++++++++++++++++++++++ .../org/apache/camel/main/components.properties | 10 --- .../org/apache/camel/main/dataformats.properties | 2 - .../java/org/apache/camel/util/ArtifactUtils.java | 12 --- .../java/org/apache/camel/util/SecurityUtils.java | 1 - .../java/org/apache/camel/util/SensitiveUtils.java | 18 +--- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 +++ 16 files changed, 271 insertions(+), 58 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 d87ddb449973..14a36d69d974 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 @@ -7394,8 +7394,15 @@ "handling": { "type": "string", "description": "For an error path, what happens to the failure: handled (the message ends there), continued (the route goes on after it) or notHandled (the failure goes on to the caller); absent when a predicate decides it" + }, + "reply": { + "type": "boolean", + "description": "Whether the source route waits for the answer of the target route: a call (direct) or a request-reply over a broker, rather than a hand-off" } - } + }, + "required": [ + "reply" + ] }, "description": "The connections between nodes (only present when a route topology dumper is available)" }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-topology.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-topology.json index 842512d8c341..976200187dbe 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-topology.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/route-topology.json @@ -146,8 +146,15 @@ "handling": { "type": "string", "description": "For an error path, what happens to the failure: handled (the message ends there), continued (the route goes on after it) or notHandled (the failure goes on to the caller); absent when a predicate decides it" + }, + "reply": { + "type": "boolean", + "description": "Whether the source route waits for the answer of the target route: a call (direct) or a request-reply over a broker, rather than a hand-off" } - } + }, + "required": [ + "reply" + ] }, "description": "The connections between nodes (only present when a route topology dumper is available)" }, diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/RouteTopologyDumper.java b/core/camel-api/src/main/java/org/apache/camel/spi/RouteTopologyDumper.java index eb4a37340000..6ef8b42e6ed2 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/RouteTopologyDumper.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/RouteTopologyDumper.java @@ -107,16 +107,28 @@ public interface RouteTopologyDumper { * @param handling for an error path, what happens to the failure: {@link #HANDLING_HANDLED}, * {@link #HANDLING_CONTINUED} or {@link #HANDLING_NOT_HANDLED}; null for a call, and when a * predicate decides it at runtime (since 4.23) + * @param reply whether the source route waits for the answer of the target route: the send is InOut (the + * exchange pattern of the endpoint uri, else of the send, else of the message the route + * consumed), or the component always waits, as direct does: a request-reply over a broker + * rather than a hand-off (since 4.24) * @since 4.21 */ record TopologyEdge(String fromRouteId, String toRouteId, String endpoint, String connectionType, String kind, - String via, String handling) { + String via, String handling, boolean reply) { /** * An edge of kind {@link #EDGE_CALL}. */ public TopologyEdge(String fromRouteId, String toRouteId, String endpoint, String connectionType) { - this(fromRouteId, toRouteId, endpoint, connectionType, EDGE_CALL, null, null); + this(fromRouteId, toRouteId, endpoint, connectionType, EDGE_CALL, null, null, false); + } + + /** + * An edge without the answer of its target known. + */ + public TopologyEdge(String fromRouteId, String toRouteId, String endpoint, String connectionType, String kind, + String via, String handling) { + this(fromRouteId, toRouteId, endpoint, connectionType, kind, via, handling, false); } /** diff --git a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-topology.json b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-topology.json index 842512d8c341..976200187dbe 100644 --- a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-topology.json +++ b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/route-topology.json @@ -146,8 +146,15 @@ "handling": { "type": "string", "description": "For an error path, what happens to the failure: handled (the message ends there), continued (the route goes on after it) or notHandled (the failure goes on to the caller); absent when a predicate decides it" + }, + "reply": { + "type": "boolean", + "description": "Whether the source route waits for the answer of the target route: a call (direct) or a request-reply over a broker, rather than a hand-off" } - } + }, + "required": [ + "reply" + ] }, "description": "The connections between nodes (only present when a route topology dumper is available)" }, diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java index 6d57c50284cb..b179ba34413f 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteTopologyDevConsole.java @@ -66,7 +66,9 @@ public class RouteTopologyDevConsole extends AbstractDevConsole { @Metadata(description = "For an error path, what sends: errorHandler or onException") String via, @Metadata(description = "For an error path, what happens to the failure: handled (the message ends there)," + " continued (the route goes on after it) or notHandled (the failure goes on to the" - + " caller); absent when a predicate decides it") String handling) { + + " caller); absent when a predicate decides it") String handling, + @Metadata(description = "Whether the source route waits for the answer of the target route: a call (direct)" + + " or a request-reply over a broker, rather than a hand-off") boolean reply) { } public record ExternalEndpointEntry( @@ -134,8 +136,12 @@ public class RouteTopologyDevConsole extends AbstractDevConsole { edge.toRouteId(), edge.endpoint(), edge.connectionType(), edge.via(), edge.handling() != null ? ", " + edge.handling() : "")); } else { - sb.append(String.format(" --> %s via %s [%s]%n", - edge.toRouteId(), edge.endpoint(), edge.connectionType())); + // a request-reply the component does not imply (direct always waits) + boolean requestReply = edge.reply() && !edge.endpoint().startsWith("direct:") + && !edge.endpoint().startsWith("kamelet:"); + sb.append(String.format(" --> %s via %s [%s%s]%n", + edge.toRouteId(), edge.endpoint(), edge.connectionType(), + requestReply ? ", request-reply" : "")); } } } @@ -186,7 +192,7 @@ public class RouteTopologyDevConsole extends AbstractDevConsole { for (TopologyEdge edge : result.edges()) { edges.add(new EdgeEntry( edge.fromRouteId(), edge.toRouteId(), edge.endpoint(), edge.connectionType(), edge.kind(), - edge.via(), edge.handling())); + edge.via(), edge.handling(), edge.reply())); } List<ExternalEndpointEntry> externalEndpoints = null; diff --git a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteTopologyDevConsoleTest.java b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteTopologyDevConsoleTest.java index f97c87fdd974..3be9d984f420 100644 --- a/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteTopologyDevConsoleTest.java +++ b/core/camel-console/src/test/java/org/apache/camel/impl/console/RouteTopologyDevConsoleTest.java @@ -54,6 +54,13 @@ class RouteTopologyDevConsoleTest extends ContextTestSupport { from("direct:isolated").routeId("isolated") .to("mock:nowhere"); + + // request-reply over a queue: the asker waits for the answer of the quoter + from("direct:ask").routeId("asker") + .to("seda:quote?exchangePattern=InOut"); + + from("seda:quote").routeId("quoter") + .to("mock:quoted"); } }; } @@ -75,7 +82,7 @@ class RouteTopologyDevConsoleTest extends ContextTestSupport { JsonArray nodes = (JsonArray) json.get("nodes"); assertNotNull(nodes); - assertEquals(5, nodes.size()); + assertEquals(7, nodes.size()); JsonArray edges = (JsonArray) json.get("edges"); assertNotNull(edges); @@ -110,6 +117,23 @@ class RouteTopologyDevConsoleTest extends ContextTestSupport { assertEquals("internal", edge.getString("connectionType")); } + @Test + void testAnEdgeSaysWhetherTheRouteWaitsForTheAnswer() { + DevConsole console = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology"); + JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON); + JsonArray edges = (JsonArray) json.get("edges"); + + // CAMEL-26823: a direct route runs before the sender goes on, a request-reply waits for its answer + assertEquals(Boolean.TRUE, findEdge(edges, "trigger", "processor").get("reply")); + assertEquals(Boolean.TRUE, findEdge(edges, "asker", "quoter").get("reply")); + // a hand-off over a queue does not + assertEquals(Boolean.FALSE, findEdge(edges, "looper", "looper").get("reply")); + + String text = (String) console.call(DevConsole.MediaType.TEXT); + assertTrue(text.contains("--> quoter via seda:quote [internal, request-reply]"), text); + assertTrue(text.contains("--> processor via direct:process [internal]"), text); + } + @Test void testChainedConnections() { DevConsole console = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-topology"); 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 460928cd7603..110b3f594884 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 @@ -341,6 +341,9 @@ public class DefaultDumpRoutesStrategy extends ServiceSupport implements DumpRou if (edge.handling() != null) { jo.put("handling", edge.handling()); } + if (edge.reply()) { + jo.put("reply", true); + } edgesArr.add(jo); } root.put("edges", edgesArr); diff --git a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultRouteTopologyDumper.java b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultRouteTopologyDumper.java index 444c295f8bac..c8c7e4c41209 100644 --- a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultRouteTopologyDumper.java +++ b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultRouteTopologyDumper.java @@ -28,6 +28,8 @@ import java.util.TreeMap; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; import org.apache.camel.ErrorHandlerFactory; +import org.apache.camel.ExchangePattern; +import org.apache.camel.Route; import org.apache.camel.model.EndpointRequiredDefinition; import org.apache.camel.model.ExpressionSubElementDefinition; import org.apache.camel.model.Model; @@ -35,12 +37,16 @@ import org.apache.camel.model.OnExceptionDefinition; import org.apache.camel.model.ProcessorDefinition; import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; +import org.apache.camel.model.SendDefinition; +import org.apache.camel.model.ToDynamicDefinition; +import org.apache.camel.model.WireTapDefinition; import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition; import org.apache.camel.model.errorhandler.NoErrorHandlerDefinition; import org.apache.camel.model.language.ConstantExpression; import org.apache.camel.spi.EndpointUriFactory; import org.apache.camel.spi.RouteTopologyDumper; import org.apache.camel.spi.annotations.JdkService; +import org.apache.camel.support.EndpointHelper; import org.apache.camel.util.URISupport; @JdkService(RouteTopologyDumper.FACTORY) @@ -48,6 +54,8 @@ public class DefaultRouteTopologyDumper implements RouteTopologyDumper { private static final Set<String> TRIGGER_SCHEMES = Set.of("timer", "quartz", "cron", "scheduler"); private static final Set<String> INTERNAL_SCHEMES = Set.of("direct", "seda", "vertx", "disruptor", "disruptor-vm"); + // the schemes whose target runs before the route goes on, whatever the exchange pattern + private static final Set<String> WAITING_SCHEMES = Set.of("direct", "kamelet"); @Override public TopologyResult dumpTopology(CamelContext context) { @@ -88,14 +96,23 @@ public class DefaultRouteTopologyDumper implements RouteTopologyDumper { Collection<EndpointRequiredDefinition> sends = ProcessorDefinitionHelper.filterTypeInOutputs(List.of(output), EndpointRequiredDefinition.class); for (EndpointRequiredDefinition erd : sends) { + // an error path ends there: what it sends to is no answer the route waits for + boolean reply = !onException && waitsForAnswer(context, rd, (ProcessorDefinition<?>) erd, + erd.getEndpointUri()); addEdges(context, edges, inputUriToRouteIds, rd.getRouteId(), erd.getEndpointUri(), kind, via, - handling); + handling, reply); + } + // a toD or a wire tap to an endpoint the route names, not one it computes + for (ToDynamicDefinition td : staticDynamicSends(output)) { + boolean reply = !onException && waitsForAnswer(context, rd, td, td.getUri()); + addEdges(context, edges, inputUriToRouteIds, rd.getRouteId(), td.getUri(), kind, via, handling, + reply); } } if (handlesFailures && eh instanceof DeadLetterChannelDefinition dlc && dlc.getDeadLetterUri() != null) { // a dead letter channel handles the failure: the message ends at the dead letter uri addEdges(context, edges, inputUriToRouteIds, rd.getRouteId(), dlc.getDeadLetterUri(), EDGE_ERROR, - VIA_ERROR_HANDLER, HANDLING_HANDLED); + VIA_ERROR_HANDLER, HANDLING_HANDLED, false); } } @@ -107,16 +124,67 @@ public class DefaultRouteTopologyDumper implements RouteTopologyDumper { private static void addEdges( CamelContext context, List<TopologyEdge> edges, Map<String, List<String>> inputUriToRouteIds, - String fromRouteId, String uri, String kind, String via, String handling) { + String fromRouteId, String uri, String kind, String via, String handling, boolean reply) { String outputUri = normalizeUri(context, uri); List<String> targetRouteIds = inputUriToRouteIds.get(outputUri); if (targetRouteIds != null) { String scheme = extractScheme(outputUri); String connType = INTERNAL_SCHEMES.contains(scheme) ? "internal" : "external"; for (String targetId : targetRouteIds) { - edges.add(new TopologyEdge(fromRouteId, targetId, outputUri, connType, kind, via, handling)); + edges.add(new TopologyEdge(fromRouteId, targetId, outputUri, connType, kind, via, handling, reply)); + } + } + } + + /** + * Whether the route waits for the answer of what it sends to, as the send processor decides the exchange pattern: + * the exchangePattern of the endpoint uri, else the pattern of the send (to with pattern InOut, a wire tap is + * InOnly), else the pattern of the message the route consumed (InOut from platform-http, InOnly from a timer). + * Direct always runs the target before the route goes on. + */ + static boolean waitsForAnswer(CamelContext context, RouteDefinition route, ProcessorDefinition<?> send, String uri) { + if (uri == null || send instanceof WireTapDefinition) { + // a wire tap sends a copy on another thread: the route goes on without its answer + return false; + } + if (WAITING_SCHEMES.contains(extractScheme(uri))) { + return true; + } + ExchangePattern pattern = null; + try { + pattern = EndpointHelper.resolveExchangePatternFromUrl(context.resolvePropertyPlaceholders(uri)); + } catch (Exception e) { + // a uri that cannot be resolved says nothing + } + if (pattern == null) { + String sendPattern = send instanceof SendDefinition<?> sd ? sd.getPattern() + : send instanceof ToDynamicDefinition td ? td.getPattern() : null; + if (sendPattern != null) { + pattern = context.getTypeConverter().tryConvertTo(ExchangePattern.class, + context.resolvePropertyPlaceholders(sendPattern)); } } + if (pattern == null) { + Route running = route.getRouteId() != null ? context.getRoute(route.getRouteId()) : null; + pattern = running != null && running.getEndpoint() != null ? running.getEndpoint().getExchangePattern() : null; + } + return pattern == ExchangePattern.InOut; + } + + /** + * The toD and wire tap sends below a processor whose uri has no expression: the route names the endpoint (a wire + * tap to seda:audit), as a to does. + */ + private static List<ToDynamicDefinition> staticDynamicSends(ProcessorDefinition<?> output) { + List<ToDynamicDefinition> answer = new ArrayList<>(); + for (ToDynamicDefinition td : ProcessorDefinitionHelper.filterTypeInOutputs(List.of(output), + ToDynamicDefinition.class)) { + String uri = td.getUri(); + if (uri != null && !uri.contains("${") && !uri.contains("$simple{")) { + answer.add(td); + } + } + return answer; } /** @@ -172,6 +240,9 @@ public class DefaultRouteTopologyDumper implements RouteTopologyDumper { for (EndpointRequiredDefinition erd : outputs) { allOutputUris.add(URISupport.stripQuery(erd.getEndpointUri())); } + for (ProcessorDefinition<?> output : rd.getOutputs()) { + staticDynamicSends(output).forEach(td -> allOutputUris.add(URISupport.stripQuery(td.getUri()))); + } } List<TopologyExternalEndpoint> externalEndpoints = new ArrayList<>(); @@ -193,9 +264,14 @@ public class DefaultRouteTopologyDumper implements RouteTopologyDumper { = ProcessorDefinitionHelper.filterTypeInOutputs( rd.getOutputs(), EndpointRequiredDefinition.class); + List<String> outputUris = new ArrayList<>(); + outputs.forEach(erd -> outputUris.add(erd.getEndpointUri())); + for (ProcessorDefinition<?> output : rd.getOutputs()) { + staticDynamicSends(output).forEach(td -> outputUris.add(td.getUri())); + } int outIdx = 0; - for (EndpointRequiredDefinition erd : outputs) { - String outputUri = URISupport.stripQuery(erd.getEndpointUri()); + for (String uri : outputUris) { + String outputUri = URISupport.stripQuery(uri); String outputScheme = extractScheme(outputUri); if (isRemoteScheme(outputScheme, schemeRemoteMap) && !inputUriToRouteIds.containsKey(outputUri)) { String dedupeKey = routeId + "|" + outputUri; diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultDumpRoutesStrategyTopologyJsonTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultDumpRoutesStrategyTopologyJsonTest.java index a60ceccade36..7e08639cbe80 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultDumpRoutesStrategyTopologyJsonTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultDumpRoutesStrategyTopologyJsonTest.java @@ -80,6 +80,8 @@ public class DefaultDumpRoutesStrategyTopologyJsonTest extends ContextTestSuppor assertThat(edge.getString("toRouteId")).isEqualTo("consumer"); assertThat(edge.getString("endpoint")).isEqualTo("direct:linked"); assertThat(edge.getString("connectionType")).isEqualTo("internal"); + // CAMEL-26823: the producer waits for the consumer, as direct runs it before the producer goes on + assertThat(edge.get("reply")).isEqualTo(Boolean.TRUE); } @Test diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultRouteTopologyDumperReplyTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultRouteTopologyDumperReplyTest.java new file mode 100644 index 000000000000..9971ea4af590 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultRouteTopologyDumperReplyTest.java @@ -0,0 +1,98 @@ +/* + * 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.List; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.ExchangePattern; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.spi.RouteTopologyDumper; +import org.apache.camel.spi.RouteTopologyDumper.TopologyEdge; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CAMEL-26823: an edge says whether the route waits for the answer of the route it sends to, as the send processor + * decides the exchange pattern: the exchangePattern of the endpoint uri, else the pattern of the send, else the pattern + * of the message the route consumed; a direct route always runs before the sender goes on. The shape of + * connect-service/jms-request-reply, with seda in the place of the JMS broker. + */ +class DefaultRouteTopologyDumperReplyTest extends ContextTestSupport { + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("timer:orders?repeatCount=1").routeId("order-intake") + // a hand-off: the order goes on a queue + .to("seda:orders"); + + from("seda:orders").routeId("reserve-order") + .errorHandler(deadLetterChannel("seda:orders-dead")) + // request-reply: the endpoint asks for the answer + .to("seda:stock?exchangePattern=InOut") + // the send asks for it + .to(ExchangePattern.InOut, "seda:pricing") + // a call + .to("direct:audit") + // a copy, never an answer + .wireTap("seda:metrics") + // a hand-off to the subscribers + .to("seda:reserved"); + + // the request of a client: what it sends on is InOut too + from("seda:quote?exchangePattern=InOut").routeId("quote") + .to("seda:pricing"); + + from("seda:stock").routeId("stock-service").log("stock"); + from("seda:pricing").routeId("pricing").log("pricing"); + from("direct:audit").routeId("audit").log("audit"); + from("seda:metrics").routeId("metrics").log("metrics"); + from("seda:reserved").routeId("shipping").log("shipping"); + from("seda:orders-dead").routeId("dead-orders").log("dead"); + } + }; + } + + @Test + void anEdgeSaysWhetherTheRouteWaitsForTheAnswer() { + List<TopologyEdge> edges = new DefaultRouteTopologyDumper().dumpTopology(context).edges(); + + assertThat(edges).extracting(e -> e.fromRouteId() + "->" + e.toRouteId() + (e.reply() ? " waits" : "")) + .contains( + "order-intake->reserve-order", + "reserve-order->stock-service waits", + "reserve-order->pricing waits", + "reserve-order->audit waits", + "reserve-order->metrics", + "reserve-order->shipping", + "quote->pricing waits"); + // the dead letter channel: the message ends there + assertThat(edges).filteredOn(e -> "dead-orders".equals(e.toRouteId())) + .isNotEmpty().allMatch(e -> e.isErrorPath() && !e.reply()); + } + + @Test + void anEdgeOfBeforeWaitsForNoAnswer() { + assertThat(new TopologyEdge("a", "b", "seda:b", "internal").reply()).isFalse(); + assertThat(new TopologyEdge("a", "b", "seda:b", "internal", RouteTopologyDumper.EDGE_CALL, null, null).reply()) + .isFalse(); + } +} diff --git a/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties b/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties index 7dd709b62764..e67614622a09 100644 --- a/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties +++ b/core/camel-main/src/generated/resources/org/apache/camel/main/components.properties @@ -15,11 +15,9 @@ alibaba-sms amqp apicurio-registry arangodb -as2 asterisk atmosphere-websocket atom -avro aws-bedrock aws-bedrock-agent aws-bedrock-agent-runtime @@ -71,7 +69,6 @@ azure-storage-queue bean bean-validator bonita -box braintree browse caffeine-cache @@ -111,7 +108,6 @@ debezium-oracle debezium-postgres debezium-sqlserver dfdl -dhis2 direct disruptor disruptor-vm @@ -129,7 +125,6 @@ elasticsearch elasticsearch-rest-client event exec -fhir file file-watch flatpack @@ -229,7 +224,6 @@ kafka kafka-share kamelet keycloak -knative kserve kubernetes-config-maps kubernetes-cronjob @@ -289,8 +283,6 @@ netty netty-http oaipmh odata -olingo2 -olingo4 once opa openai @@ -335,13 +327,11 @@ robotframework rocketmq rss saga -salesforce sap-netweaver scheduler schematron scp seda -servicenow servlet sftp shell diff --git a/core/camel-main/src/generated/resources/org/apache/camel/main/dataformats.properties b/core/camel-main/src/generated/resources/org/apache/camel/main/dataformats.properties index a6f67b25e6fd..0e7bff2ae6da 100644 --- a/core/camel-main/src/generated/resources/org/apache/camel/main/dataformats.properties +++ b/core/camel-main/src/generated/resources/org/apache/camel/main/dataformats.properties @@ -12,8 +12,6 @@ crypto csv dfdl fastjson -fhirJson -fhirXml flatpack fory grok diff --git a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java index 1a40f60a7320..1ea02fba1592 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java @@ -51,11 +51,9 @@ public final class ArtifactUtils { Map.<String, String> entry("amqp", "camel-amqp"), Map.<String, String> entry("apicurio-registry", "camel-apicurio-registry"), Map.<String, String> entry("arangodb", "camel-arangodb"), - Map.<String, String> entry("as2", "camel-as2"), Map.<String, String> entry("asterisk", "camel-asterisk"), Map.<String, String> entry("atmosphere-websocket", "camel-atmosphere-websocket"), Map.<String, String> entry("atom", "camel-atom"), - Map.<String, String> entry("avro", "camel-avro-rpc"), Map.<String, String> entry("aws-bedrock", "camel-aws-bedrock"), Map.<String, String> entry("aws-bedrock-agent", "camel-aws-bedrock"), Map.<String, String> entry("aws-bedrock-agent-runtime", "camel-aws-bedrock"), @@ -107,7 +105,6 @@ public final class ArtifactUtils { Map.<String, String> entry("bean", "camel-bean"), Map.<String, String> entry("bean-validator", "camel-bean-validator"), Map.<String, String> entry("bonita", "camel-bonita"), - Map.<String, String> entry("box", "camel-box"), Map.<String, String> entry("braintree", "camel-braintree"), Map.<String, String> entry("browse", "camel-browse"), Map.<String, String> entry("caffeine-cache", "camel-caffeine"), @@ -147,7 +144,6 @@ public final class ArtifactUtils { Map.<String, String> entry("debezium-postgres", "camel-debezium-postgres"), Map.<String, String> entry("debezium-sqlserver", "camel-debezium-sqlserver"), Map.<String, String> entry("dfdl", "camel-dfdl"), - Map.<String, String> entry("dhis2", "camel-dhis2"), Map.<String, String> entry("direct", "camel-direct"), Map.<String, String> entry("disruptor", "camel-disruptor"), Map.<String, String> entry("disruptor-vm", "camel-disruptor"), @@ -165,7 +161,6 @@ public final class ArtifactUtils { Map.<String, String> entry("elasticsearch-rest-client", "camel-elasticsearch-rest-client"), Map.<String, String> entry("event", "camel-event"), Map.<String, String> entry("exec", "camel-exec"), - Map.<String, String> entry("fhir", "camel-fhir"), Map.<String, String> entry("file", "camel-file"), Map.<String, String> entry("file-watch", "camel-file-watch"), Map.<String, String> entry("flatpack", "camel-flatpack"), @@ -265,7 +260,6 @@ public final class ArtifactUtils { Map.<String, String> entry("kafka-share", "camel-kafka-share"), Map.<String, String> entry("kamelet", "camel-kamelet"), Map.<String, String> entry("keycloak", "camel-keycloak"), - Map.<String, String> entry("knative", "camel-knative"), Map.<String, String> entry("kserve", "camel-kserve"), Map.<String, String> entry("kubernetes-config-maps", "camel-kubernetes"), Map.<String, String> entry("kubernetes-cronjob", "camel-kubernetes"), @@ -325,8 +319,6 @@ public final class ArtifactUtils { Map.<String, String> entry("netty-http", "camel-netty-http"), Map.<String, String> entry("oaipmh", "camel-oaipmh"), Map.<String, String> entry("odata", "camel-odata"), - Map.<String, String> entry("olingo2", "camel-olingo2"), - Map.<String, String> entry("olingo4", "camel-olingo4"), Map.<String, String> entry("once", "camel-once"), Map.<String, String> entry("opa", "camel-opa"), Map.<String, String> entry("openai", "camel-openai"), @@ -371,13 +363,11 @@ public final class ArtifactUtils { Map.<String, String> entry("rocketmq", "camel-rocketmq"), Map.<String, String> entry("rss", "camel-rss"), Map.<String, String> entry("saga", "camel-saga"), - Map.<String, String> entry("salesforce", "camel-salesforce"), Map.<String, String> entry("sap-netweaver", "camel-sap-netweaver"), Map.<String, String> entry("scheduler", "camel-scheduler"), Map.<String, String> entry("schematron", "camel-schematron"), Map.<String, String> entry("scp", "camel-jsch"), Map.<String, String> entry("seda", "camel-seda"), - Map.<String, String> entry("servicenow", "camel-servicenow"), Map.<String, String> entry("servlet", "camel-servlet"), Map.<String, String> entry("sftp", "camel-ftp"), Map.<String, String> entry("shell", "camel-shell"), @@ -511,8 +501,6 @@ public final class ArtifactUtils { Map.<String, String> entry("csv", "camel-csv"), Map.<String, String> entry("dfdl", "camel-dfdl"), Map.<String, String> entry("fastjson", "camel-fastjson"), - Map.<String, String> entry("fhirJson", "camel-fhir"), - Map.<String, String> entry("fhirXml", "camel-fhir"), Map.<String, String> entry("flatpack", "camel-flatpack"), Map.<String, String> entry("fory", "camel-fory"), Map.<String, String> entry("grok", "camel-grok"), diff --git a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java index 511d1600f11c..694805268390 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java @@ -220,7 +220,6 @@ public final class SecurityUtils { "component:xj", "component:xsltsaxon")); owners.put("serializablepackages", Set.of( - "component:avro", "dataformat:avro")); owners.put("skiptlsverify", Set.of( "component:splunkhec")); diff --git a/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java b/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java index ab94ce7cb3ab..10c5c3a26d26 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java @@ -91,26 +91,19 @@ public final class SensitiveUtils { "knownhostsfile", "knownhostsuri", "mongodbpassword", - "oauthclientid", "oauthclientsecret", "oauthtoken", - "oauthtokenurl", "p12filename", "passphrase", "passphrasebyte", "password", - "personalaccesstoken", "postmanapikey", "privatekey", - "privatekeyfile", "privatekeyname", - "privatekeypassword", "proxyauthpassword", "proxyauthusername", "proxypassword", - "proxyuser", "proxyusername", - "publickeyid", "publishkey", "queueownerawsaccountid", "realm", @@ -143,7 +136,6 @@ public final class SensitiveUtils { "user", "userauthenticationcredentials", "username", - "userpassword", "verificationcode", "webhookverifytoken", "zookeeperpassword" @@ -206,26 +198,19 @@ public final class SensitiveUtils { + "|\\Qknownhostsfile\\E" + "|\\Qknownhostsuri\\E" + "|\\Qmongodbpassword\\E" - + "|\\Qoauthclientid\\E" + "|\\Qoauthclientsecret\\E" + "|\\Qoauthtoken\\E" - + "|\\Qoauthtokenurl\\E" + "|\\Qp12filename\\E" + "|\\Qpassphrase\\E" + "|\\Qpassphrasebyte\\E" + "|\\Qpassword\\E" - + "|\\Qpersonalaccesstoken\\E" + "|\\Qpostmanapikey\\E" + "|\\Qprivatekey\\E" - + "|\\Qprivatekeyfile\\E" + "|\\Qprivatekeyname\\E" - + "|\\Qprivatekeypassword\\E" + "|\\Qproxyauthpassword\\E" + "|\\Qproxyauthusername\\E" + "|\\Qproxypassword\\E" - + "|\\Qproxyuser\\E" + "|\\Qproxyusername\\E" - + "|\\Qpublickeyid\\E" + "|\\Qpublishkey\\E" + "|\\Qqueueownerawsaccountid\\E" + "|\\Qrealm\\E" @@ -258,11 +243,10 @@ public final class SensitiveUtils { + "|\\Quser\\E" + "|\\Quserauthenticationcredentials\\E" + "|\\Qusername\\E" - + "|\\Quserpassword\\E" + "|\\Qverificationcode\\E" + "|\\Qwebhookverifytoken\\E" + "|\\Qzookeeperpassword\\E" - // SENSITIVE-PATTERN: END + // SENSITIVE-PATTERN: END ; private SensitiveUtils() { diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index fbb57a05ed5f..bde89e116481 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -256,6 +256,18 @@ in its model, so clearing them on the route saved little memory at the cost of m Applications that called `Route.clearRouteModel()` directly can remove the call. Implementations of the `Route` interface that override `clearRouteModel()` can remove the override. +=== camel-core - the route topology says whether a route waits for the answer, and has the edges of a wire tap + +Each edge of the route topology (the `route-topology` developer console, the topology JSON of the dump routes strategy +and `RouteTopologyDumper`) now says whether the route waits for the answer of the route it sends to: `reply` is `true` +for a send to `direct`, and for a send that is InOut, as a request-reply over a JMS queue is +(`exchangePattern=InOut`). The topology JSON writes `reply` only when it is `true`, and the text output of the console +marks a request-reply with `request-reply`. The `RouteTopologyDumper.TopologyEdge` record has the new `reply` +component; its seven-argument constructor is kept and creates an edge without it. + +A `wireTap` or a `toD` to an endpoint the route names, rather than one it computes with an expression, is now an edge +too (a wire tap never waits for the answer), and an external endpoint when no route consumes from it. + === camel-management - LastExchangeCompletedExchangeId is updated at most once per millisecond The JMX attribute `LastExchangeCompletedExchangeId`, of the `CamelContext`, route and processor MBeans, is now updated
