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

Reply via email to