This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-23564 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 4b181de099a9688beb9dcb2c8c6dae5f4dd30cc2 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jun 8 09:53:46 2026 +0200 CAMEL-23666: Support optional endpoint URIs in route templates Extend the existing {{?param}} optional syntax to support full endpoint URIs via {{?uri}}. When used inside a route template and the parameter is not provided, the step is silently skipped instead of throwing NoSuchEndpointException. Implemented in SendReifier, ToDynamicReifier, WireTapReifier, EnrichReifier, PollEnrichReifier, and PollReifier. Closes #23816 --- .../org/apache/camel/reifier/EnrichReifier.java | 4 + .../apache/camel/reifier/PollEnrichReifier.java | 4 + .../java/org/apache/camel/reifier/PollReifier.java | 4 + .../org/apache/camel/reifier/ProcessorReifier.java | 16 +++ .../java/org/apache/camel/reifier/SendReifier.java | 11 ++ .../org/apache/camel/reifier/ToDynamicReifier.java | 4 + .../org/apache/camel/reifier/WireTapReifier.java | 4 + .../RouteTemplateOptionalEndpointUriTest.java | 112 +++++++++++++++++++++ .../modules/ROOT/pages/route-template.adoc | 61 +++++++++++ .../apache/camel/dsl/yaml/RouteTemplateTest.groovy | 55 ++++++++++ 10 files changed, 275 insertions(+) diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/EnrichReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/EnrichReifier.java index a66a2b88185f..47414d32c11d 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/EnrichReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/EnrichReifier.java @@ -52,7 +52,11 @@ public class EnrichReifier extends ExpressionReifier<EnrichDefinition> { // route templates should pre parse uri as they have dynamic values as part of their template parameters RouteDefinition rd = ProcessorDefinitionHelper.getRoute(definition); if (rd != null && rd.isTemplate() != null && rd.isTemplate()) { + String rawUri = uri; uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, uri); + if (isOptionalUriAndNotResolved(camelContext, rawUri)) { + return null; + } } Enricher enricher = new Enricher(exp, uri); diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollEnrichReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollEnrichReifier.java index fd2325066bea..79a245998974 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollEnrichReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollEnrichReifier.java @@ -59,7 +59,11 @@ public class PollEnrichReifier extends ProcessorReifier<PollEnrichDefinition> { // route templates should pre parse uri as they have dynamic values as part of their template parameters RouteDefinition rd = ProcessorDefinitionHelper.getRoute(definition); if (rd != null && rd.isTemplate() != null && rd.isTemplate()) { + String rawUri = uri; uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, uri); + if (isOptionalUriAndNotResolved(camelContext, rawUri)) { + return null; + } } // if no timeout then we should block, and there use a negative timeout diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollReifier.java index e9fd1f752892..7f2acad6bf84 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/PollReifier.java @@ -50,7 +50,11 @@ public class PollReifier extends ProcessorReifier<PollDefinition> { // route templates should pre parse uri as they have dynamic values as part of their template parameters RouteDefinition rd = ProcessorDefinitionHelper.getRoute(definition); if (rd != null && rd.isTemplate() != null && rd.isTemplate()) { + String rawUri = uri; uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, uri); + if (isOptionalUriAndNotResolved(camelContext, rawUri)) { + return null; + } } long timeout = parseDuration(definition.getTimeout(), 20000); diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java index a8a145ffa767..71d4fc01f449 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java @@ -123,9 +123,11 @@ import org.apache.camel.spi.IdAware; import org.apache.camel.spi.InterceptStrategy; import org.apache.camel.spi.NodeIdFactory; import org.apache.camel.spi.ProcessorFactory; +import org.apache.camel.spi.PropertiesComponent; import org.apache.camel.spi.RouteIdAware; import org.apache.camel.spi.StepIdAware; import org.apache.camel.support.CamelContextHelper; +import org.apache.camel.support.EndpointHelper; import org.apache.camel.support.PluginHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; @@ -966,6 +968,20 @@ public abstract class ProcessorReifier<T extends ProcessorDefinition<?>> extends return strategy; } + /** + * Checks if a URI is an optional property placeholder that resolved to null/empty, meaning the endpoint is not + * needed and the processor should be skipped. + */ + protected static boolean isOptionalUriAndNotResolved(CamelContext camelContext, String rawUri) { + if (rawUri == null || !rawUri.contains(PropertiesComponent.PREFIX_OPTIONAL_TOKEN)) { + return false; + } + String resolved = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, rawUri); + // if resolved is null/empty, or still contains unresolved optional placeholders, then skip + return resolved == null || resolved.isEmpty() + || resolved.contains(PropertiesComponent.PREFIX_OPTIONAL_TOKEN); + } + /** * Is the given node marked as disabled */ diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SendReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SendReifier.java index b1965274c624..f724016d9623 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SendReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SendReifier.java @@ -22,6 +22,8 @@ import org.apache.camel.LineNumberAware; import org.apache.camel.Processor; import org.apache.camel.Route; import org.apache.camel.model.ProcessorDefinition; +import org.apache.camel.model.ProcessorDefinitionHelper; +import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.ToDefinition; import org.apache.camel.processor.SendProcessor; import org.apache.camel.support.CamelContextHelper; @@ -34,6 +36,15 @@ public class SendReifier extends ProcessorReifier<ToDefinition> { @Override public Processor createProcessor() throws Exception { + // route templates with optional URI should skip the processor if the URI is not provided + RouteDefinition rd = ProcessorDefinitionHelper.getRoute(definition); + if (rd != null && rd.isTemplate() != null && rd.isTemplate()) { + String rawUri = definition.getEndpointUri(); + if (isOptionalUriAndNotResolved(camelContext, rawUri)) { + return null; + } + } + SendProcessor answer = new SendProcessor(resolveEndpoint(), parse(ExchangePattern.class, definition.getPattern())); answer.setDisabled(isDisabled(camelContext, definition)); answer.setVariableSend(parseString(definition.getVariableSend())); diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ToDynamicReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ToDynamicReifier.java index bce54b2a307d..291c0936430d 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ToDynamicReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ToDynamicReifier.java @@ -51,7 +51,11 @@ public class ToDynamicReifier<T extends ToDynamicDefinition> extends ProcessorRe // route templates should pre parse uri as they have dynamic values as part of their template parameters RouteDefinition rd = ProcessorDefinitionHelper.getRoute(definition); if (rd != null && rd.isTemplate() != null && rd.isTemplate()) { + String rawUri = uri; uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, uri); + if (isOptionalUriAndNotResolved(camelContext, rawUri)) { + return null; + } } SendDynamicProcessor processor = new SendDynamicProcessor(uri, exp); diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/WireTapReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/WireTapReifier.java index 57cd5604e1ff..963b98e38323 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/WireTapReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/WireTapReifier.java @@ -65,7 +65,11 @@ public class WireTapReifier extends ToDynamicReifier<WireTapDefinition<?>> { // route templates should pre parse uri as they have dynamic values as part of their template parameters RouteDefinition rd = ProcessorDefinitionHelper.getRoute(definition); if (rd != null && rd.isTemplate() != null && rd.isTemplate()) { + String rawUri = uri; uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, uri); + if (isOptionalUriAndNotResolved(camelContext, rawUri)) { + return null; + } } SendDynamicProcessor dynamicSendProcessor = null; diff --git a/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateOptionalEndpointUriTest.java b/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateOptionalEndpointUriTest.java new file mode 100644 index 000000000000..b5b61d0f6b42 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateOptionalEndpointUriTest.java @@ -0,0 +1,112 @@ +/* + * 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; + +import org.apache.camel.ContextTestSupport; +import org.junit.jupiter.api.Test; + +class RouteTemplateOptionalEndpointUriTest extends ContextTestSupport { + + @Test + void testToWithOptionalUriNotProvided() throws Exception { + TemplatedRouteBuilder.builder(context, "myTemplate") + .parameter("name", "test1") + .routeId("myRoute1") + .add(); + + getMockEndpoint("mock:end").expectedMessageCount(1); + getMockEndpoint("mock:end").expectedBodiesReceived("Hello World"); + + template.sendBody("direct:test1", "Hello World"); + + assertMockEndpointsSatisfied(); + } + + @Test + void testToWithOptionalUriProvided() throws Exception { + TemplatedRouteBuilder.builder(context, "myTemplate") + .parameter("name", "test2") + .parameter("optionalUri", "mock:middle") + .routeId("myRoute2") + .add(); + + getMockEndpoint("mock:middle").expectedMessageCount(1); + getMockEndpoint("mock:end").expectedMessageCount(1); + + template.sendBody("direct:test2", "Hello World"); + + assertMockEndpointsSatisfied(); + } + + @Test + void testMultipleOptionalUris() throws Exception { + TemplatedRouteBuilder.builder(context, "myMultiTemplate") + .parameter("name", "test3") + .parameter("optionalUri2", "mock:second") + .routeId("myRoute3") + .add(); + + // optionalUri1 is not provided, so that step is skipped + // optionalUri2 is provided, so it should receive the message + getMockEndpoint("mock:second").expectedMessageCount(1); + getMockEndpoint("mock:end").expectedMessageCount(1); + + template.sendBody("direct:test3", "Hello World"); + + assertMockEndpointsSatisfied(); + } + + @Test + void testAllOptionalUrisNotProvided() throws Exception { + TemplatedRouteBuilder.builder(context, "myMultiTemplate") + .parameter("name", "test4") + .routeId("myRoute4") + .add(); + + // both optional URIs not provided, message goes straight to mock:end + getMockEndpoint("mock:end").expectedMessageCount(1); + getMockEndpoint("mock:end").expectedBodiesReceived("Hello World"); + + template.sendBody("direct:test4", "Hello World"); + + assertMockEndpointsSatisfied(); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + routeTemplate("myTemplate") + .templateParameter("name") + .templateOptionalParameter("optionalUri") + .from("direct:{{name}}") + .to("{{?optionalUri}}") + .to("mock:end"); + + routeTemplate("myMultiTemplate") + .templateParameter("name") + .templateOptionalParameter("optionalUri1") + .templateOptionalParameter("optionalUri2") + .from("direct:{{name}}") + .to("{{?optionalUri1}}") + .to("{{?optionalUri2}}") + .to("mock:end"); + } + }; + } +} diff --git a/docs/user-manual/modules/ROOT/pages/route-template.adoc b/docs/user-manual/modules/ROOT/pages/route-template.adoc index 3d479f67ab45..9913ce9592ab 100644 --- a/docs/user-manual/modules/ROOT/pages/route-template.adoc +++ b/docs/user-manual/modules/ROOT/pages/route-template.adoc @@ -178,6 +178,67 @@ Notice how we use `?` in the replyTo option below: IMPORTANT: In case no replyToQueue property is provided when creating the template the option replyTo is just ignored. +==== Optional endpoint URIs + +The `{{?}}` syntax can also be used for the entire endpoint URI. +When the template parameter is not provided, the step is silently skipped (removed from the route). + +[tabs] +==== + +Java:: ++ +[source,java] +---- +routeTemplate("myTemplate") + .templateParameter("name") + .templateOptionalParameter("optionalUri") + .from("direct:{{name}}") + .to("{{?optionalUri}}") + .to("mock:end"); +---- + +XML:: ++ +[source,xml] +---- +<routeTemplate id="myTemplate"> + <templateParameter name="name"/> + <templateParameter name="optionalUri" required="false"/> + <route> + <from uri="direct:{{name}}"/> + <to uri="{{?optionalUri}}"/> + <to uri="mock:end"/> + </route> +</routeTemplate> +---- + +YAML:: ++ +[source,yaml] +---- +- routeTemplate: + id: "myTemplate" + parameters: + - name: "name" + - name: "optionalUri" + required: false + from: + uri: "direct:{{name}}" + steps: + - to: + uri: "{{?optionalUri}}" + - to: + uri: "mock:end" +---- +==== + +When creating a route from this template without providing the `optionalUri` parameter, +the `to("{\{?optionalUri}}")` step is omitted and messages flow directly to `mock:end`. +When the parameter is provided, the step is included as usual. + +This works with `to`, `toD`, `wireTap`, `enrich`, `pollEnrich`, and `poll` EIPs. + A property can also have a logical negation using the exclamation mark (`!`): [source,text] diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteTemplateTest.groovy b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteTemplateTest.groovy index bbba239e0de8..61fbcfc3c9ca 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteTemplateTest.groovy +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteTemplateTest.groovy @@ -431,4 +431,59 @@ class RouteTemplateTest extends YamlTestSupport { Assertions.assertEquals(3, context.getRoute("second").filter("bbb*").size()) } + def "create template with optional endpoint uri not provided"() { + when: + loadRoutes """ + - routeTemplate: + id: "myTemplate" + parameters: + - name: "foo" + - name: "optionalUri" + required: false + from: + uri: "direct:{{foo}}" + steps: + - to: "{{?optionalUri}}" + - to: "mock:result" + """ + + context.addRouteFromTemplate("myRoute1", "myTemplate", [foo: "start"]) + context.start() + + then: + MockEndpoint mock = context.getEndpoint("mock:result", MockEndpoint) + mock.expectedBodiesReceived("Hello World") + context.createProducerTemplate().sendBody("direct:start", "Hello World") + mock.assertIsSatisfied() + } + + def "create template with optional endpoint uri provided"() { + when: + loadRoutes """ + - routeTemplate: + id: "myTemplate" + parameters: + - name: "foo" + - name: "optionalUri" + required: false + from: + uri: "direct:{{foo}}" + steps: + - to: "{{?optionalUri}}" + - to: "mock:result" + """ + + context.addRouteFromTemplate("myRoute1", "myTemplate", [foo: "start", optionalUri: "mock:middle"]) + context.start() + + then: + MockEndpoint middle = context.getEndpoint("mock:middle", MockEndpoint) + middle.expectedBodiesReceived("Hello World") + MockEndpoint result = context.getEndpoint("mock:result", MockEndpoint) + result.expectedBodiesReceived("Hello World") + context.createProducerTemplate().sendBody("direct:start", "Hello World") + middle.assertIsSatisfied() + result.assertIsSatisfied() + } + }
