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 d83971d15e63 CAMEL-23666: Support optional endpoint URIs in route
templates
d83971d15e63 is described below
commit d83971d15e63c46deeb66dddf01025f29addae96
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()
+ }
+
}