This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit 639e282ef866c440446e4a4fdcb42a3e1f6afa91 Author: Luigi De Masi <[email protected]> AuthorDate: Wed Sep 30 18:34:47 2026 +0200 CAMEL-25178: Improve Switch EIP discoverability and DSL support Add Switch documentation, catalog aliases and intent mappings, and resource-based XML, YAML and Spring round-trip coverage. Support Endpoint DSL destinations while preserving builders through copying, JAXB dumping and reification. Show case values and destinations in route structure output while keeping the fallback processor type and identity. Co-authored-by: Codex <[email protected]> Signed-off-by: Luigi De Masi <[email protected]> --- .../org/apache/camel/catalog/docs/ai-patterns.adoc | 6 +- .../org/apache/camel/catalog/docs/choice-eip.adoc | 6 + .../docs/enterprise-integration-patterns.adoc | 4 + .../org/apache/camel/catalog/docs/switch-eip.adoc | 135 ++++++++++++++------- .../org/apache/camel/catalog/models/switch.json | 1 + .../camel/catalog/schemas/camelYamlDsl-model.json | 1 + .../camel/spring/processor/SpringSwitchTest.java | 51 ++++++++ .../camel/spring/processor/SpringSwitchTest.xml | 48 ++++++++ .../SwitchRouteStructureDevConsoleTest.java | 89 ++++++++++++++ .../main/docs/modules/eips/pages/ai-patterns.adoc | 6 +- .../main/docs/modules/eips/pages/choice-eip.adoc | 6 + .../pages/enterprise-integration-patterns.adoc | 4 + .../main/docs/modules/eips/pages/switch-eip.adoc | 135 ++++++++++++++------- .../camel/impl/DefaultModelToStructureDumper.java | 13 +- .../META-INF/org/apache/camel/model/switch.json | 1 + .../apache/camel/model/SwitchCaseDefinition.java | 31 ++++- .../org/apache/camel/model/SwitchDefinition.java | 33 ++++- .../camel/model/SwitchOtherwiseDefinition.java | 26 +++- .../apache/camel/model/SwitchDefinitionTest.java | 12 ++ .../org/apache/camel/reifier/SwitchReifier.java | 8 +- .../camel/processor/SwitchValidationTest.java | 2 +- .../org/apache/camel/java/in/LwJavaParserTest.java | 29 +++++ core/camel-xml-io/src/test/resources/switch.xml | 40 ++++++ .../java/org/apache/camel/xml/jaxb/JaxbHelper.java | 17 +++ .../camel/yaml/io/YamlPrinterRoundTripTest.java | 21 ++++ .../org/apache/camel/yaml/out/XmlToYamlTest.java | 31 +++++ .../apache/camel/yaml/out/YamlModelWriterTest.java | 16 +++ .../src/test/resources/yaml-route-switch.yaml | 38 ++++++ .../builder/endpoint/SwitchEndpointDslTest.java | 106 ++++++++++++++++ .../dsl/jbang/core/commands/ai/eip-samples.json | 4 +- .../dsl/jbang/core/commands/ai/CatalogSamples.java | 4 + .../dsl/jbang/core/commands/ai/ToolRegistry.java | 4 +- .../jbang/core/commands/ai/CatalogSamplesTest.java | 21 ++++ .../jbang/core/commands/mcp/PromptDefinitions.java | 2 +- .../camel/dsl/yaml/validator/SwitchSchemaTest.java | 9 ++ .../resources/schema/camelYamlDsl-model.json | 1 + .../java/org/apache/camel/dsl/yaml/SwitchTest.java | 47 +------ .../src/test/resources/switch.camel.yaml | 56 +++++++++ 38 files changed, 909 insertions(+), 155 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-patterns.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-patterns.adoc index a783c5882f69..d77c72900306 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-patterns.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-patterns.adoc @@ -48,7 +48,7 @@ See also the full xref:enterprise-integration-patterns.adoc[Enterprise Integrati |Discard messages that do not match a predicate. |Router / Dispatch -|xref:choice-eip.adoc[Content-Based Router], xref:dynamicRouter-eip.adoc[Dynamic Router] +|xref:choice-eip.adoc[Content-Based Router], xref:switch-eip.adoc[Switch], xref:dynamicRouter-eip.adoc[Dynamic Router] |Route messages to different destinations based on content or rules. |=== @@ -208,6 +208,10 @@ See also the full xref:enterprise-integration-patterns.adoc[Enterprise Integrati |=== |Term |Camel Component / EIP |Description +|Classify / Intent routing +|xref:languages:semantic-language.adoc[Semantic], xref:switch-eip.adoc[Switch] +|Evaluate a Semantic choice question once and dispatch its category string through literal Switch cases. + |Agent-to-Agent / A2A |xref:ROOT:a2a-component.adoc[A2A] |Google's Agent-to-Agent protocol for communication between AI agents. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/choice-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/choice-eip.adoc index 0e3d67c6ba1a..c9f0645698c2 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/choice-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/choice-eip.adoc @@ -13,6 +13,12 @@ on the contents of the message exchanges. image::eip/ContentBasedRouter.gif[image] +TIP: For a decision table based on the literal value of one expression, use +xref:switch-eip.adoc[Switch]. Each Switch case sends to one endpoint, with no inline +processing steps. Branches can be separate routes linked through +xref:ROOT:direct-component.adoc[Direct] or xref:ROOT:seda-component.adoc[SEDA]. +Choice supports predicates, ranges, and inline processing steps. + == Choice options // eip options: START diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/enterprise-integration-patterns.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/enterprise-integration-patterns.adoc index 819428ae457c..b20b1f8c243d 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/enterprise-integration-patterns.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/enterprise-integration-patterns.adoc @@ -118,6 +118,10 @@ a|image::eip/ContentBasedRouterIcon.gif[image] a situation where the implementation of a single logical function (e.g., inventory check) is spread across multiple physical systems? +a|image::eip/ContentBasedRouterIcon.gif[image] +|xref:switch-eip.adoc[Switch] |How do we route a message to a fixed endpoint by +the literal value of a single selector, using a decision table? + a|image::eip/MessageFilterIcon.gif[image] |xref:filter-eip.adoc[Message Filter] |How can a component avoid receiving uninteresting messages? diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc index 79b1b441406f..4fab8169deb6 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/switch-eip.adoc @@ -17,6 +17,11 @@ predicates, ranges, overlapping conditions, or nested processing steps. A case c send to a `direct:` route when more processing is needed. Use xref:toD-eip.adoc[To D] when the endpoint URI itself must be calculated dynamically. +TIP: Switch has no inline processing steps: each case sends to one endpoint. +Keep each branch in its own route, linked through xref:ROOT:direct-component.adoc[Direct] +or xref:ROOT:seda-component.adoc[SEDA], so it can be read and tested separately. +For predicates, ranges, or inline processing steps, use xref:choice-eip.adoc[Choice]. + == Options // eip options: START @@ -36,8 +41,9 @@ They follow normal Camel error handling and do not select `otherwise`. === Route from a header This route reads the `department` header. The body is passed unchanged to the -selected destination. The examples on this page are independent; load one at a -time and connect the `direct:` destinations to your application's handling routes. +selected destination. Each case has a separate `direct:` handling route. Replace +the logging steps with the application's processing. The examples on this page +are independent; load one at a time. [tabs] ==== @@ -51,20 +57,29 @@ from("direct:tickets") .doCase("technical", "direct:technical") .otherwise("direct:review") .end(); + +from("direct:billing").log("Billing: ${body}"); +from("direct:technical").log("Technical: ${body}"); +from("direct:review").log("Review: ${body}"); ---- XML:: + [source,xml] ---- -<route xmlns="http://camel.apache.org/schema/spring"> - <from uri="direct:tickets"/> - <switch> - <selector><header>department</header></selector> - <case value="billing" uri="direct:billing"/> - <case value="technical" uri="direct:technical"/> - <otherwise uri="direct:review"/> - </switch> -</route> +<routes xmlns="http://camel.apache.org/schema/xml-io"> + <route> + <from uri="direct:tickets"/> + <switch> + <selector><header>department</header></selector> + <case value="billing" uri="direct:billing"/> + <case value="technical" uri="direct:technical"/> + <otherwise uri="direct:review"/> + </switch> + </route> + <route><from uri="direct:billing"/><log message="Billing: ${body}"/></route> + <route><from uri="direct:technical"/><log message="Technical: ${body}"/></route> + <route><from uri="direct:review"/><log message="Review: ${body}"/></route> +</routes> ---- YAML:: + @@ -85,6 +100,22 @@ YAML:: uri: direct:technical otherwise: uri: direct:review + +- route: + from: + uri: direct:billing + steps: + - log: "Billing: ${body}" +- route: + from: + uri: direct:technical + steps: + - log: "Technical: ${body}" +- route: + from: + uri: direct:review + steps: + - log: "Review: ${body}" ---- ==== @@ -110,53 +141,59 @@ never select the fallback. Streams are reset after selector evaluation when stre caching is enabled. Loops and retries that enter Switch again evaluate the selector again; results are not cached across entries. -=== Connect the example destinations +=== Use the Endpoint DSL -For a local demonstration, the destination routes can simply log the message. -Load these routes alongside one of the examples above; replace the logging steps -with the application's processing when integrating the example. +With an xref:manual::Endpoint-dsl.adoc[EndpointRouteBuilder], cases and the fallback +also accept endpoint builders. Both case forms support endpoint options, including +object-valued options, just like `to(...)`: -[tabs] -==== -Java:: -+ [source,java] ---- -from("direct:billing").log("Billing: ${body}"); -from("direct:technical").log("Technical: ${body}"); -from("direct:review").log("Review: ${body}"); ----- -XML:: -+ -[source,xml] ----- -<routes xmlns="http://camel.apache.org/schema/xml-io"> - <route><from uri="direct:billing"/><log message="Billing: ${body}"/></route> - <route><from uri="direct:technical"/><log message="Technical: ${body}"/></route> - <route><from uri="direct:review"/><log message="Review: ${body}"/></route> -</routes> +from(direct("tickets")) + .doSwitch(header("department")) + .doCase("billing", direct("billing")) + .doCase("technical").to(direct("technical")) + .otherwise(direct("review")) + .end(); ---- -YAML:: -+ + +=== Classify and route with Semantic + +A xref:languages:semantic-language.adoc[Semantic] choice question returns a category +string that Switch can match against its literal cases. Configure `camel-semantic` +and a provider as described in the language documentation. This example uses one +reference, `ref:department`, to evaluate the message body once and route the result. +Load it alongside the `direct:billing`, `direct:technical`, and `direct:review` +handling routes shown above. + [source,yaml] ---- +- semantic: + question: + department: + type: choice + instructions: Which department should handle this message? + criteria: + billing: Invoices, payments and refunds + technical: Bugs, outages and technical problems + other: Any other request - route: from: - uri: direct:billing - steps: - - log: "Billing: ${body}" -- route: - from: - uri: direct:technical + uri: direct:classify steps: - - log: "Technical: ${body}" -- route: - from: - uri: direct:review - steps: - - log: "Review: ${body}" + - switch: + selector: + language: + language: semantic + expression: ref:department + case: + - value: billing + uri: direct:billing + - value: technical + uri: direct:technical + otherwise: + uri: direct:review ---- -==== == Destinations and management @@ -177,6 +214,10 @@ otherwise: name: review ---- +The `route-structure` developer console shows the destination of each case, such +as `case[billing -> direct:billing]`, and identifies the fallback as +`otherwise[direct:review]`. Sensitive URI parameters are masked in these labels. + Each case has an identity for tracing, debugging and management. The Switch MBean's `extendedInformation` table reports case IDs, literal values, destination URIs and selection counts. URIs are masked when management masking is enabled (the default). diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/switch.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/switch.json index 1da2d69acf5b..4befb27147a7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/switch.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/switch.json @@ -8,6 +8,7 @@ "firstVersion": "4.23.0", "label": "eip,routing", "javaType": "org.apache.camel.model.SwitchDefinition", + "aliases": [ "decision-table", "dispatch-table", "lookup-table", "case" ], "abstract": false, "input": true, "output": false diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json index 320e9d67ea9f..eacf7a4d52cd 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json @@ -8369,6 +8369,7 @@ "title" : "Switch", "description" : "Evaluates a selector once and dispatches to a fixed endpoint by literal scalar values", "label" : "eip,routing", + "aliases" : [ "decision-table", "dispatch-table", "lookup-table", "case" ], "input" : true, "isListItem" : true, "children" : [ { diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/processor/SpringSwitchTest.java b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/processor/SpringSwitchTest.java new file mode 100644 index 000000000000..93a3cd2a4518 --- /dev/null +++ b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/processor/SpringSwitchTest.java @@ -0,0 +1,51 @@ +/* + * 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.spring.processor; + +import org.apache.camel.spring.SpringTestSupport; +import org.junit.jupiter.api.Test; +import org.springframework.context.support.AbstractXmlApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class SpringSwitchTest extends SpringTestSupport { + @Test + public void caseAndFallbackUseSeparateRoutes() throws Exception { + getMockEndpoint("mock:billing").expectedBodiesReceived("invoice"); + getMockEndpoint("mock:technical").expectedBodiesReceived("outage"); + getMockEndpoint("mock:review").expectedBodiesReceived("unknown", "missing"); + getMockEndpoint("mock:after").expectedMessageCount(4); + template.sendBodyAndHeader("direct:start", "invoice", "department", "BILLING"); + template.sendBodyAndHeader("direct:start", "outage", "department", "technical"); + template.sendBodyAndHeader("direct:start", "unknown", "department", "other"); + template.sendBody("direct:start", "missing"); + assertMockEndpointsSatisfied(); + } + + @Test + public void xpathSelectorUsesSpringNamespaces() throws Exception { + String body = "<t:ticket xmlns:t='urn:tickets'><t:department>billing</t:department></t:ticket>"; + getMockEndpoint("mock:billing").expectedBodiesReceived(body); + getMockEndpoint("mock:review").expectedMessageCount(0); + template.sendBody("direct:xpath", body); + assertMockEndpointsSatisfied(); + } + + @Override + protected AbstractXmlApplicationContext createApplicationContext() { + return new ClassPathXmlApplicationContext("org/apache/camel/spring/processor/SpringSwitchTest.xml"); + } +} diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/SpringSwitchTest.xml b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/SpringSwitchTest.xml new file mode 100644 index 000000000000..20dc2edcf862 --- /dev/null +++ b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/SpringSwitchTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:t="urn:tickets" + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://camel.apache.org/schema/spring https://camel.apache.org/schema/spring/camel-spring.xsd"> + <camelContext xmlns="http://camel.apache.org/schema/spring"> + <route id="dispatch"> + <from uri="direct:start"/> + <switch id="byDepartment"> + <selector><header>department</header></selector> + <case id="billingCase" value="billing" uri="direct:billing"/> + <case id="technicalCase" value="technical" uri="direct:technical"/> + <otherwise uri="direct:review"/> + </switch> + <to uri="mock:after"/> + </route> + <route id="xpathDispatch"> + <from uri="direct:xpath"/> + <switch> + <selector><xpath resultType="java.lang.String">string(/t:ticket/t:department)</xpath></selector> + <case value="billing" uri="direct:billing"/> + <otherwise uri="direct:review"/> + </switch> + </route> + <route><from uri="direct:billing"/><to uri="mock:billing"/></route> + <route><from uri="direct:technical"/><to uri="mock:technical"/></route> + <route><from uri="direct:review"/><to uri="mock:review"/></route> + </camelContext> +</beans> diff --git a/core/camel-console/src/test/java/org/apache/camel/impl/console/SwitchRouteStructureDevConsoleTest.java b/core/camel-console/src/test/java/org/apache/camel/impl/console/SwitchRouteStructureDevConsoleTest.java new file mode 100644 index 000000000000..facbe152339f --- /dev/null +++ b/core/camel-console/src/test/java/org/apache/camel/impl/console/SwitchRouteStructureDevConsoleTest.java @@ -0,0 +1,89 @@ +/* + * 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 java.util.Map; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.console.DevConsole; +import org.apache.camel.model.SwitchDefinition; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.util.json.JsonObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SwitchRouteStructureDevConsoleTest extends ContextTestSupport { + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void labelsIdentifyCaseAndFallbackDestinations(boolean brief) { + DevConsole console = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-structure"); + String text = (String) console.call(DevConsole.MediaType.TEXT, Map.of("filter", "tickets", "brief", brief)); + assertTrue(text.contains("case[billing -> direct:billing]"), text); + assertTrue(text.contains("case[technical -> direct:technical]"), text); + assertTrue(text.contains("otherwise[direct:review]"), text); + + JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON, Map.of("filter", "tickets", "brief", brief)); + JsonObject route = (JsonObject) json.getCollection("routes").iterator().next(); + JsonObject fallback = route.getCollection("code").stream().map(JsonObject.class::cast) + .filter(line -> "dispatch-otherwise".equals(line.getString("id"))).findFirst().orElseThrow(); + assertEquals("to", fallback.getString("type")); + assertEquals("otherwise[direct:review]", fallback.getString("code")); + assertEquals("direct:review", fallback.getString("uri")); + } + + @Test + void labelsMaskSecretsAndBriefModeKeepsTheFallbackIdentity() { + SwitchDefinition sw = (SwitchDefinition) context.getRouteDefinition("tickets").getOutputs().get(0); + sw.getCases().get(0).setUri("direct:billing?password=caseSecret"); + sw.getOtherwise().setUri("direct:review?password=fallbackSecret"); + DevConsole console = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("route-structure"); + String text = (String) console.call(DevConsole.MediaType.TEXT, Map.of("filter", "tickets")); + assertTrue(text.contains("case[billing -> direct:billing?password="), text); + assertTrue(text.contains("otherwise[direct:review?password="), text); + assertFalse(text.contains("caseSecret"), text); + assertFalse(text.contains("fallbackSecret"), text); + + String brief = (String) console.call(DevConsole.MediaType.TEXT, Map.of("filter", "tickets", "brief", true)); + assertTrue(brief.contains("case[billing -> direct:billing]"), brief); + assertTrue(brief.contains("otherwise[direct:review]"), brief); + assertFalse(brief.contains("password"), brief); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:tickets").routeId("tickets") + .doSwitch(header("department")).id("dispatch") + .doCase("billing", "direct:billing") + .doCase("technical").to("direct:technical") + .otherwise("direct:review") + .end(); + from("direct:billing").to("mock:billing"); + from("direct:technical").to("mock:technical"); + from("direct:review").to("mock:review"); + } + }; + } +} diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/ai-patterns.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/ai-patterns.adoc index a783c5882f69..d77c72900306 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/ai-patterns.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/ai-patterns.adoc @@ -48,7 +48,7 @@ See also the full xref:enterprise-integration-patterns.adoc[Enterprise Integrati |Discard messages that do not match a predicate. |Router / Dispatch -|xref:choice-eip.adoc[Content-Based Router], xref:dynamicRouter-eip.adoc[Dynamic Router] +|xref:choice-eip.adoc[Content-Based Router], xref:switch-eip.adoc[Switch], xref:dynamicRouter-eip.adoc[Dynamic Router] |Route messages to different destinations based on content or rules. |=== @@ -208,6 +208,10 @@ See also the full xref:enterprise-integration-patterns.adoc[Enterprise Integrati |=== |Term |Camel Component / EIP |Description +|Classify / Intent routing +|xref:languages:semantic-language.adoc[Semantic], xref:switch-eip.adoc[Switch] +|Evaluate a Semantic choice question once and dispatch its category string through literal Switch cases. + |Agent-to-Agent / A2A |xref:ROOT:a2a-component.adoc[A2A] |Google's Agent-to-Agent protocol for communication between AI agents. diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/choice-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/choice-eip.adoc index 0e3d67c6ba1a..c9f0645698c2 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/choice-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/choice-eip.adoc @@ -13,6 +13,12 @@ on the contents of the message exchanges. image::eip/ContentBasedRouter.gif[image] +TIP: For a decision table based on the literal value of one expression, use +xref:switch-eip.adoc[Switch]. Each Switch case sends to one endpoint, with no inline +processing steps. Branches can be separate routes linked through +xref:ROOT:direct-component.adoc[Direct] or xref:ROOT:seda-component.adoc[SEDA]. +Choice supports predicates, ranges, and inline processing steps. + == Choice options // eip options: START diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/enterprise-integration-patterns.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/enterprise-integration-patterns.adoc index 819428ae457c..b20b1f8c243d 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/enterprise-integration-patterns.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/enterprise-integration-patterns.adoc @@ -118,6 +118,10 @@ a|image::eip/ContentBasedRouterIcon.gif[image] a situation where the implementation of a single logical function (e.g., inventory check) is spread across multiple physical systems? +a|image::eip/ContentBasedRouterIcon.gif[image] +|xref:switch-eip.adoc[Switch] |How do we route a message to a fixed endpoint by +the literal value of a single selector, using a decision table? + a|image::eip/MessageFilterIcon.gif[image] |xref:filter-eip.adoc[Message Filter] |How can a component avoid receiving uninteresting messages? diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc index 79b1b441406f..4fab8169deb6 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/switch-eip.adoc @@ -17,6 +17,11 @@ predicates, ranges, overlapping conditions, or nested processing steps. A case c send to a `direct:` route when more processing is needed. Use xref:toD-eip.adoc[To D] when the endpoint URI itself must be calculated dynamically. +TIP: Switch has no inline processing steps: each case sends to one endpoint. +Keep each branch in its own route, linked through xref:ROOT:direct-component.adoc[Direct] +or xref:ROOT:seda-component.adoc[SEDA], so it can be read and tested separately. +For predicates, ranges, or inline processing steps, use xref:choice-eip.adoc[Choice]. + == Options // eip options: START @@ -36,8 +41,9 @@ They follow normal Camel error handling and do not select `otherwise`. === Route from a header This route reads the `department` header. The body is passed unchanged to the -selected destination. The examples on this page are independent; load one at a -time and connect the `direct:` destinations to your application's handling routes. +selected destination. Each case has a separate `direct:` handling route. Replace +the logging steps with the application's processing. The examples on this page +are independent; load one at a time. [tabs] ==== @@ -51,20 +57,29 @@ from("direct:tickets") .doCase("technical", "direct:technical") .otherwise("direct:review") .end(); + +from("direct:billing").log("Billing: ${body}"); +from("direct:technical").log("Technical: ${body}"); +from("direct:review").log("Review: ${body}"); ---- XML:: + [source,xml] ---- -<route xmlns="http://camel.apache.org/schema/spring"> - <from uri="direct:tickets"/> - <switch> - <selector><header>department</header></selector> - <case value="billing" uri="direct:billing"/> - <case value="technical" uri="direct:technical"/> - <otherwise uri="direct:review"/> - </switch> -</route> +<routes xmlns="http://camel.apache.org/schema/xml-io"> + <route> + <from uri="direct:tickets"/> + <switch> + <selector><header>department</header></selector> + <case value="billing" uri="direct:billing"/> + <case value="technical" uri="direct:technical"/> + <otherwise uri="direct:review"/> + </switch> + </route> + <route><from uri="direct:billing"/><log message="Billing: ${body}"/></route> + <route><from uri="direct:technical"/><log message="Technical: ${body}"/></route> + <route><from uri="direct:review"/><log message="Review: ${body}"/></route> +</routes> ---- YAML:: + @@ -85,6 +100,22 @@ YAML:: uri: direct:technical otherwise: uri: direct:review + +- route: + from: + uri: direct:billing + steps: + - log: "Billing: ${body}" +- route: + from: + uri: direct:technical + steps: + - log: "Technical: ${body}" +- route: + from: + uri: direct:review + steps: + - log: "Review: ${body}" ---- ==== @@ -110,53 +141,59 @@ never select the fallback. Streams are reset after selector evaluation when stre caching is enabled. Loops and retries that enter Switch again evaluate the selector again; results are not cached across entries. -=== Connect the example destinations +=== Use the Endpoint DSL -For a local demonstration, the destination routes can simply log the message. -Load these routes alongside one of the examples above; replace the logging steps -with the application's processing when integrating the example. +With an xref:manual::Endpoint-dsl.adoc[EndpointRouteBuilder], cases and the fallback +also accept endpoint builders. Both case forms support endpoint options, including +object-valued options, just like `to(...)`: -[tabs] -==== -Java:: -+ [source,java] ---- -from("direct:billing").log("Billing: ${body}"); -from("direct:technical").log("Technical: ${body}"); -from("direct:review").log("Review: ${body}"); ----- -XML:: -+ -[source,xml] ----- -<routes xmlns="http://camel.apache.org/schema/xml-io"> - <route><from uri="direct:billing"/><log message="Billing: ${body}"/></route> - <route><from uri="direct:technical"/><log message="Technical: ${body}"/></route> - <route><from uri="direct:review"/><log message="Review: ${body}"/></route> -</routes> +from(direct("tickets")) + .doSwitch(header("department")) + .doCase("billing", direct("billing")) + .doCase("technical").to(direct("technical")) + .otherwise(direct("review")) + .end(); ---- -YAML:: -+ + +=== Classify and route with Semantic + +A xref:languages:semantic-language.adoc[Semantic] choice question returns a category +string that Switch can match against its literal cases. Configure `camel-semantic` +and a provider as described in the language documentation. This example uses one +reference, `ref:department`, to evaluate the message body once and route the result. +Load it alongside the `direct:billing`, `direct:technical`, and `direct:review` +handling routes shown above. + [source,yaml] ---- +- semantic: + question: + department: + type: choice + instructions: Which department should handle this message? + criteria: + billing: Invoices, payments and refunds + technical: Bugs, outages and technical problems + other: Any other request - route: from: - uri: direct:billing - steps: - - log: "Billing: ${body}" -- route: - from: - uri: direct:technical + uri: direct:classify steps: - - log: "Technical: ${body}" -- route: - from: - uri: direct:review - steps: - - log: "Review: ${body}" + - switch: + selector: + language: + language: semantic + expression: ref:department + case: + - value: billing + uri: direct:billing + - value: technical + uri: direct:technical + otherwise: + uri: direct:review ---- -==== == Destinations and management @@ -177,6 +214,10 @@ otherwise: name: review ---- +The `route-structure` developer console shows the destination of each case, such +as `case[billing -> direct:billing]`, and identifies the fallback as +`otherwise[direct:review]`. Sensitive URI parameters are masked in these labels. + Each case has an identity for tracing, debugging and management. The Switch MBean's `extendedInformation` table reports case IDs, literal values, destination URIs and selection counts. URIs are masked when management masking is enabled (the default). diff --git a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java index eca8d90ce6a3..be180734a76d 100644 --- a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java +++ b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModelToStructureDumper.java @@ -28,11 +28,14 @@ import org.apache.camel.model.EndpointRequiredDefinition; import org.apache.camel.model.Model; import org.apache.camel.model.OptionalIdentifiedDefinition; import org.apache.camel.model.RouteDefinition; +import org.apache.camel.model.SwitchCaseDefinition; +import org.apache.camel.model.SwitchDefinition; import org.apache.camel.spi.ModelDumpLine; import org.apache.camel.spi.ModelToStructureDumper; import org.apache.camel.spi.annotations.JdkService; import org.apache.camel.support.LoggerHelper; import org.apache.camel.util.StringHelper; +import org.apache.camel.util.URISupport; @JdkService(ModelToStructureDumper.FACTORY) public class DefaultModelToStructureDumper implements ModelToStructureDumper { @@ -87,9 +90,17 @@ public class DefaultModelToStructureDumper implements ModelToStructureDumper { remote = isRemoteUri(endpointUri, schemeRemoteMap); if (brief) { String uri = StringHelper.before(endpointUri, "?", endpointUri); - code = output.getShortName() + "[" + uri + "]"; + if (output instanceof SwitchCaseDefinition c) { + code = "case[" + c.getValue() + " -> " + URISupport.sanitizeUri(uri) + "]"; + } else { + code = output.getShortName() + "[" + uri + "]"; + } } } + if (parent instanceof SwitchDefinition sw && output == sw.getOtherwiseDefinition()) { + String uri = URISupport.sanitizeUri(endpointUri); + code = "otherwise[" + (brief ? StringHelper.before(uri, "?", uri) : uri) + "]"; + } answer.add(new ModelDumpLine(loc, kind, id, level, code, output.getDescription(), endpointUri, remote)); } dumpChildren(child, scheme, brief, level + 1, answer, schemeRemoteMap); diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/switch.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/switch.json index 1da2d69acf5b..4befb27147a7 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/switch.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/switch.json @@ -8,6 +8,7 @@ "firstVersion": "4.23.0", "label": "eip,routing", "javaType": "org.apache.camel.model.SwitchDefinition", + "aliases": [ "decision-table", "dispatch-table", "lookup-table", "case" ], "abstract": false, "input": true, "output": false diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchCaseDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchCaseDefinition.java index fbbabe0c9845..99de82bbc378 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchCaseDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchCaseDefinition.java @@ -22,7 +22,9 @@ import jakarta.xml.bind.annotation.XmlAttribute; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; +import org.apache.camel.builder.EndpointProducerBuilder; import org.apache.camel.spi.Metadata; +import org.apache.camel.util.URISupport; /** One literal value and one fixed destination. */ @Metadata(label = "configuration", description = "A literal switch case with one fixed endpoint destination") @@ -37,6 +39,8 @@ public class SwitchCaseDefinition extends OptionalIdentifiedDefinition<SwitchCas @Metadata(description = "The fixed destination URI. Supports property placeholders, but not Simple expressions.") private String uri; @XmlTransient + private EndpointProducerBuilder endpointProducerBuilder; + @XmlTransient private SwitchDefinition parent; @XmlTransient private ToDefinition toDefinition; @@ -53,6 +57,7 @@ public class SwitchCaseDefinition extends OptionalIdentifiedDefinition<SwitchCas super(source); this.value = source.value; this.uri = source.uri; + this.endpointProducerBuilder = source.endpointProducerBuilder; } public SwitchCaseDefinition copyDefinition() { @@ -68,10 +73,22 @@ public class SwitchCaseDefinition extends OptionalIdentifiedDefinition<SwitchCas } public String getUri() { - return uri; + return endpointProducerBuilder != null ? endpointProducerBuilder.getRawUri() : uri; + } + + @XmlTransient + public EndpointProducerBuilder getEndpointProducerBuilder() { + return endpointProducerBuilder; + } + + public void setEndpointProducerBuilder(EndpointProducerBuilder endpointProducerBuilder) { + this.endpointProducerBuilder = endpointProducerBuilder; + // JAXB reads this field; the dump helper refreshes it if the builder changes. + this.uri = endpointProducerBuilder != null ? endpointProducerBuilder.getRawUri() : null; } public void setUri(String uri) { + this.endpointProducerBuilder = null; this.uri = uri; } @@ -86,7 +103,11 @@ public class SwitchCaseDefinition extends OptionalIdentifiedDefinition<SwitchCas if (toDefinition == null) { toDefinition = new ToDefinition(); } - toDefinition.setUri(uri); + if (endpointProducerBuilder != null) { + toDefinition.setEndpointProducerBuilder(endpointProducerBuilder); + } else { + toDefinition.setUri(uri); + } toDefinition.setParent(parent); if (hasCustomIdAssigned()) { toDefinition.setId(getId()); @@ -101,7 +122,7 @@ public class SwitchCaseDefinition extends OptionalIdentifiedDefinition<SwitchCas @Override public String getEndpointUri() { - return uri; + return getUri(); } @Override @@ -120,11 +141,11 @@ public class SwitchCaseDefinition extends OptionalIdentifiedDefinition<SwitchCas @Override public String getLabel() { - return "case[" + value + "]"; + return "case[" + value + " -> " + URISupport.sanitizeUri(getEndpointUri()) + "]"; } @Override public String toString() { - return getLabel() + " -> " + uri; + return getLabel(); } } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchDefinition.java index 944d6e059e50..65eff9dd77d5 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchDefinition.java @@ -28,12 +28,15 @@ import jakarta.xml.bind.annotation.XmlType; import org.apache.camel.Expression; import org.apache.camel.NamedNode; +import org.apache.camel.builder.EndpointProducerBuilder; import org.apache.camel.builder.ExpressionClause; import org.apache.camel.model.language.ExpressionDefinition; +import org.apache.camel.spi.AsEndpointUri; import org.apache.camel.spi.Metadata; /** Routes a message to one fixed endpoint using a literal lookup table. */ @Metadata(firstVersion = "4.23.0", label = "eip,routing", + aliases = { "decision-table", "dispatch-table", "lookup-table", "case" }, description = "Evaluates a selector once and dispatches to a fixed endpoint by literal scalar values") @XmlRootElement(name = "switch") @XmlAccessorType(XmlAccessType.FIELD) @@ -132,7 +135,11 @@ public class SwitchDefinition extends NoOutputDefinition<SwitchDefinition> { return; } otherwiseDefinition.setParent(this); - otherwiseDefinition.setUri(otherwise.getUri()); + if (otherwise.getEndpointProducerBuilder() != null) { + otherwiseDefinition.setEndpointProducerBuilder(otherwise.getEndpointProducerBuilder()); + } else { + otherwiseDefinition.setUri(otherwise.getUri()); + } if (getId() != null) { if (hasCustomIdAssigned()) { otherwiseDefinition.setId(getId() + "-otherwise"); @@ -147,6 +154,14 @@ public class SwitchDefinition extends NoOutputDefinition<SwitchDefinition> { return doCase(new SwitchCaseDefinition(value, uri)); } + /** Add a scalar literal and its destination using the Endpoint DSL. */ + public SwitchDefinition doCase(String value, @AsEndpointUri EndpointProducerBuilder endpoint) { + SwitchCaseDefinition c = new SwitchCaseDefinition(); + c.setValue(value); + c.setEndpointProducerBuilder(endpoint); + return doCase(c); + } + /** Add a case definition. */ public SwitchDefinition doCase(SwitchCaseDefinition value) { value.setParent(this); @@ -175,6 +190,17 @@ public class SwitchDefinition extends NoOutputDefinition<SwitchDefinition> { return this; } + /** Set the fixed fallback destination using the Endpoint DSL. */ + public SwitchDefinition otherwise(@AsEndpointUri EndpointProducerBuilder endpoint) { + SwitchOtherwiseDefinition fallback = null; + if (endpoint != null) { + fallback = new SwitchOtherwiseDefinition(); + fallback.setEndpointProducerBuilder(endpoint); + } + setOtherwise(fallback); + return this; + } + @Override public List<NamedNode> getChildren() { List<NamedNode> children = new ArrayList<>(cases); @@ -225,6 +251,11 @@ public class SwitchDefinition extends NoOutputDefinition<SwitchDefinition> { return this; } + public SwitchDefinition to(@AsEndpointUri EndpointProducerBuilder endpoint) { + definition.setEndpointProducerBuilder(endpoint); + return parent.doCase(definition); + } + public SwitchDefinition to(String uri) { definition.setUri(uri); return parent.doCase(definition); diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchOtherwiseDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchOtherwiseDefinition.java index d5cd511b2089..4783a7e033c6 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchOtherwiseDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/SwitchOtherwiseDefinition.java @@ -23,6 +23,7 @@ import jakarta.xml.bind.annotation.XmlAttribute; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; +import org.apache.camel.builder.EndpointProducerBuilder; import org.apache.camel.spi.Metadata; /** The fixed fallback destination of a switch. */ @@ -34,19 +35,38 @@ public class SwitchOtherwiseDefinition implements EndpointRequiredDefinition { @Metadata(description = "The fixed destination URI. Supports property placeholders, but not Simple expressions.") private String uri; @XmlTransient + private EndpointProducerBuilder endpointProducerBuilder; + @XmlTransient private final ToDefinition toDefinition = new ToDefinition(); public SwitchOtherwiseDefinition copyDefinition() { SwitchOtherwiseDefinition copy = new SwitchOtherwiseDefinition(); - copy.setUri(uri); + if (endpointProducerBuilder != null) { + copy.setEndpointProducerBuilder(endpointProducerBuilder); + } else { + copy.setUri(uri); + } return copy; } public String getUri() { - return uri; + return endpointProducerBuilder != null ? endpointProducerBuilder.getRawUri() : uri; + } + + @XmlTransient + public EndpointProducerBuilder getEndpointProducerBuilder() { + return endpointProducerBuilder; + } + + public void setEndpointProducerBuilder(EndpointProducerBuilder endpointProducerBuilder) { + this.endpointProducerBuilder = endpointProducerBuilder; + // JAXB reads this field; the dump helper refreshes it if the builder changes. + this.uri = endpointProducerBuilder != null ? endpointProducerBuilder.getRawUri() : null; + toDefinition.setEndpointProducerBuilder(endpointProducerBuilder); } public void setUri(String uri) { + this.endpointProducerBuilder = null; this.uri = uri; toDefinition.setUri(uri); } @@ -68,6 +88,6 @@ public class SwitchOtherwiseDefinition implements EndpointRequiredDefinition { @Override @XmlTransient public String getEndpointUri() { - return uri; + return getUri(); } } diff --git a/core/camel-core-model/src/test/java/org/apache/camel/model/SwitchDefinitionTest.java b/core/camel-core-model/src/test/java/org/apache/camel/model/SwitchDefinitionTest.java index 715e1a31075d..451d30b5a215 100644 --- a/core/camel-core-model/src/test/java/org/apache/camel/model/SwitchDefinitionTest.java +++ b/core/camel-core-model/src/test/java/org/apache/camel/model/SwitchDefinitionTest.java @@ -19,9 +19,11 @@ package org.apache.camel.model; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; class SwitchDefinitionTest { @Test @@ -52,6 +54,16 @@ class SwitchDefinitionTest { assertEquals("prepared-fallback", fallbackSend.getId()); } + @Test + void caseLabelIncludesTheDestinationAndMasksSecrets() { + SwitchCaseDefinition c = new SwitchCaseDefinition("billing", "https://example.com?password=secret"); + assertTrue(c.getLabel().startsWith("case[billing -> https://example.com?password=")); + assertFalse(c.getLabel().contains("secret")); + assertEquals(c.getLabel(), c.toString()); + c.setUri("direct:updated"); + assertEquals("case[billing -> direct:updated]", c.getLabel()); + } + @Test void copiedFallbackHasIndependentDestinationAndParent() { SwitchDefinition original = new SwitchDefinition().otherwise("mock:original"); diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SwitchReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SwitchReifier.java index 77adf6751f4c..44ccda5e85e7 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SwitchReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SwitchReifier.java @@ -69,7 +69,9 @@ public class SwitchReifier extends ProcessorReifier<SwitchDefinition> { c.idOrCreate(ids); c.prepareToDefinition(); ToDefinition send = c.getToDefinition(); - send.setUri(uris.get(i)); + if (c.getEndpointProducerBuilder() == null) { + send.setUri(uris.get(i)); + } cases.put(caseKeys.get(i), createSend(send)); } Processor otherwise = null; @@ -77,7 +79,9 @@ public class SwitchReifier extends ProcessorReifier<SwitchDefinition> { definition.idOrCreate(ids); definition.prepareOtherwiseDefinition(); ToDefinition send = definition.getOtherwiseDefinition(); - send.setUri(otherwiseUri); + if (definition.getOtherwise().getEndpointProducerBuilder() == null) { + send.setUri(otherwiseUri); + } otherwise = createSend(send); } SwitchProcessor answer = new SwitchProcessor(camelContext, selector, cases, otherwise); diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/SwitchValidationTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/SwitchValidationTest.java index 7c6eca2798dd..962d45f43973 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/SwitchValidationTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/SwitchValidationTest.java @@ -43,7 +43,7 @@ class SwitchValidationTest { @Test void rejectsMissingUri() { - rejects(s -> s.doCase("billing", null), "nonblank uri"); + rejects(s -> s.doCase("billing", (String) null), "nonblank uri"); } @Test diff --git a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java index d582aeb265f6..a83302815478 100644 --- a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java +++ b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java @@ -441,6 +441,35 @@ class LwJavaParserTest { assertThat(java).contains("routingSlip(constant(\"mock://m2,direct://c\"))", "com.acme.Errors$Invalid"); } + @Test + void switchDestinationsAcceptEndpointDslSyntax() { + JavaParseResult result = new LwJavaParser().parse(""" + import org.apache.camel.builder.endpoint.EndpointRouteBuilder; + + public class Tickets extends EndpointRouteBuilder { + public void configure() { + from(direct("tickets")).routeId("tickets") + .doSwitch(header("department")) + .doCase("billing", direct("billing")) + .doCase("technical").id("technicalCase").to(direct("technical")) + .otherwise(direct("review")) + .end() + .to(mock("after")); + } + } + """); + assertThat(result.isComplete()).as("%s", result.unresolved()).isTrue(); + RouteDefinition route = result.routes().getRoutes().get(0); + assertThat(route.getInput().getUri()).isEqualTo("direct://tickets"); + SwitchDefinition sw = (SwitchDefinition) route.getOutputs().get(0); + assertThat(sw.getCases()).extracting(SwitchCaseDefinition::getValue).containsExactly("billing", "technical"); + assertThat(sw.getCases()).extracting(SwitchCaseDefinition::getUri) + .containsExactly("direct://billing", "direct://technical"); + assertThat(sw.getCases().get(1).getId()).isEqualTo("technicalCase"); + assertThat(sw.getOtherwise().getUri()).isEqualTo("direct://review"); + assertThat(((ToDefinition) route.getOutputs().get(1)).getUri()).isEqualTo("mock://after"); + } + @Test void classNamesFormatsAndArithmetic() { JavaParseResult result = new LwJavaParser().parse(""" diff --git a/core/camel-xml-io/src/test/resources/switch.xml b/core/camel-xml-io/src/test/resources/switch.xml new file mode 100644 index 000000000000..73b2b016c94d --- /dev/null +++ b/core/camel-xml-io/src/test/resources/switch.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<routes xmlns="http://camel.apache.org/schema/xml-io" xmlns:t="urn:tickets"> + <route id="headerSwitch"> + <from uri="direct:header"/> + <switch id="byDepartment"> + <selector><header>department</header></selector> + <case id="billingCase" value="billing" uri="direct:billing"/> + <case id="technicalCase" value="technical" uri="direct:technical"/> + <case id="numericCase" value="001" uri="direct:numeric"/> + <otherwise uri="direct:review"/> + </switch> + <to uri="mock:after"/> + </route> + <route id="xpathSwitch"> + <from uri="direct:xpath"/> + <switch id="byXPath"> + <selector><xpath resultType="java.lang.String">string(/t:ticket/t:department)</xpath></selector> + <case id="xpathBillingCase" value="billing" uri="direct:billing"/> + <otherwise uri="direct:review"/> + </switch> + </route> +</routes> diff --git a/core/camel-xml-jaxb/src/main/java/org/apache/camel/xml/jaxb/JaxbHelper.java b/core/camel-xml-jaxb/src/main/java/org/apache/camel/xml/jaxb/JaxbHelper.java index 62c746934a2f..ac1a081a7565 100644 --- a/core/camel-xml-jaxb/src/main/java/org/apache/camel/xml/jaxb/JaxbHelper.java +++ b/core/camel-xml-jaxb/src/main/java/org/apache/camel/xml/jaxb/JaxbHelper.java @@ -56,7 +56,9 @@ import org.apache.camel.model.RouteTemplateDefinition; import org.apache.camel.model.RouteTemplatesDefinition; import org.apache.camel.model.RoutesDefinition; import org.apache.camel.model.SendDefinition; +import org.apache.camel.model.SwitchCaseDefinition; import org.apache.camel.model.SwitchDefinition; +import org.apache.camel.model.SwitchOtherwiseDefinition; import org.apache.camel.model.TemplatedRouteDefinition; import org.apache.camel.model.TemplatedRoutesDefinition; import org.apache.camel.model.ToDynamicDefinition; @@ -170,6 +172,21 @@ public final class JaxbHelper { restorers.add(() -> to.setUri(null)); } } + for (SwitchDefinition sw : filterTypeInOutputs(route.getOutputs(), SwitchDefinition.class)) { + for (SwitchCaseDefinition c : sw.getCases()) { + EndpointProducerBuilder builder = c.getEndpointProducerBuilder(); + if (builder != null) { + c.setUri(builder.getRawUri()); + restorers.add(() -> c.setEndpointProducerBuilder(builder)); + } + } + SwitchOtherwiseDefinition fallback = sw.getOtherwise(); + if (fallback != null && fallback.getEndpointProducerBuilder() != null) { + EndpointProducerBuilder builder = fallback.getEndpointProducerBuilder(); + fallback.setUri(builder.getRawUri()); + restorers.add(() -> fallback.setEndpointProducerBuilder(builder)); + } + } return () -> restorers.forEach(Runnable::run); } diff --git a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/io/YamlPrinterRoundTripTest.java b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/io/YamlPrinterRoundTripTest.java index 0290567b8c6d..45df27b02b94 100644 --- a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/io/YamlPrinterRoundTripTest.java +++ b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/io/YamlPrinterRoundTripTest.java @@ -21,7 +21,10 @@ import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.apache.camel.model.RouteDefinition; +import org.apache.camel.model.SwitchDefinition; import org.apache.camel.util.json.JsonObject; +import org.apache.camel.yaml.out.YamlModelWriter; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -29,6 +32,24 @@ import static org.assertj.core.api.Assertions.assertThat; public class YamlPrinterRoundTripTest { + @ParameterizedTest + @ValueSource(strings = { "001", "true", "null", "", "*", "otherwise", "1e3" }) + public void testSwitchLiteralValue(String value) throws Exception { + RouteDefinition route = new RouteDefinition("direct:start"); + SwitchDefinition sw = route.doSwitch().header("department"); + sw.doCase(value, "direct:matched").otherwise("direct:review"); + sw.preCreateProcessor(); + YamlModelWriter writer = new YamlModelWriter(); + + String yaml = writer.printAsYaml(List.of(writer.writeRouteDefinition(route))); + JsonNode node = new YAMLMapper().readTree(yaml).get(0).path("route").path("from").path("steps").get(0).path("switch"); + JsonNode literal = node.path("case").get(0).path("value"); + assertThat(literal.isTextual()).as(yaml).isTrue(); + assertThat(literal.asText()).isEqualTo(value); + assertThat(node.path("selector").path("header").path("expression").asText()).isEqualTo("department"); + assertThat(node.path("otherwise").path("uri").asText()).isEqualTo("direct:review"); + } + @ParameterizedTest @ValueSource(strings = { "Processing:", ",leading comma", "]x", "}x", "a\rb", " x\ny", " lead\nnext", "x\n\n", "007", "+5", "1.", diff --git a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/XmlToYamlTest.java b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/XmlToYamlTest.java index 20ce09adc10a..6228f6746dbb 100644 --- a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/XmlToYamlTest.java +++ b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/XmlToYamlTest.java @@ -26,19 +26,24 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; import org.apache.camel.util.json.JsonObject; import org.apache.camel.xml.in.ModelParser; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class XmlToYamlTest { @@ -66,6 +71,32 @@ class XmlToYamlTest { } } + @Test + void switchPreservesSelectorNamespacesAndCaseDetails() throws Exception { + try (InputStream is = new FileInputStream("../camel-xml-io/src/test/resources/switch.xml")) { + RoutesDefinition routes = new ModelParser(is, NAMESPACE).parseRoutesDefinition().get(); + YamlModelWriter writer = new YamlModelWriter(); + List<JsonObject> roots = new ArrayList<>(); + for (RouteDefinition route : routes.getRoutes()) { + roots.add(writer.writeRouteDefinition(route)); + } + JsonNode yaml = new YAMLMapper().readTree(writer.printAsYaml(roots)); + JsonNode sw = yaml.get(0).path("route").path("from").path("steps").get(0).path("switch"); + assertEquals("department", sw.path("selector").path("header").path("expression").asText()); + assertEquals("billingCase", sw.path("case").get(0).path("id").asText()); + assertEquals("direct:billing", sw.path("case").get(0).path("uri").asText()); + assertTrue(sw.path("case").get(2).path("value").isTextual()); + assertEquals("001", sw.path("case").get(2).path("value").asText()); + assertEquals("direct:review", sw.path("otherwise").path("uri").asText()); + JsonNode xpath = yaml.get(1).path("route").path("from").path("steps").get(0) + .path("switch").path("selector").path("xpath"); + assertEquals("string(/t:ticket/t:department)", xpath.path("expression").asText()); + assertEquals("java.lang.String", xpath.path("resultType").asText()); + assertEquals("urn:tickets", xpath.path("namespace").get(0).path("value").asText()); + assertEquals("t", xpath.path("namespace").get(0).path("key").asText()); + } + } + private static Stream<Arguments> routes() { return definitions("routes"); } diff --git a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlModelWriterTest.java b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlModelWriterTest.java index d974171434b3..9b34b518b958 100644 --- a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlModelWriterTest.java +++ b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlModelWriterTest.java @@ -75,6 +75,7 @@ import org.apache.camel.model.SortDefinition; import org.apache.camel.model.SplitDefinition; import org.apache.camel.model.StepDefinition; import org.apache.camel.model.StopDefinition; +import org.apache.camel.model.SwitchDefinition; import org.apache.camel.model.ThreadsDefinition; import org.apache.camel.model.ThrottleDefinition; import org.apache.camel.model.ThrowExceptionDefinition; @@ -157,6 +158,21 @@ public class YamlModelWriterTest { Assertions.assertEquals(expected, out); } + @Test + public void testSwitch() throws Exception { + RouteDefinition route = new RouteDefinition("direct:start").routeId("myRoute"); + SwitchDefinition sw = route.doSwitch().header("department"); + sw.doCase("billing", "direct:billing").doCase("001", "direct:numeric").otherwise("direct:review"); + sw.getCases().get(0).setId("billingCase"); + sw.preCreateProcessor(); + route.to("mock:after"); + + YamlModelWriter writer = new YamlModelWriter(); + String out = writer.printAsYaml(List.of(writer.writeRouteDefinition(route))); + String expected = stripLineComments(Paths.get("src/test/resources/yaml-route-switch.yaml"), "#", true); + Assertions.assertEquals(expected, out); + } + @Test public void testChoice() throws Exception { YamlModelWriter writer = new YamlModelWriter(); diff --git a/core/camel-yaml-io/src/test/resources/yaml-route-switch.yaml b/core/camel-yaml-io/src/test/resources/yaml-route-switch.yaml new file mode 100644 index 000000000000..612377add949 --- /dev/null +++ b/core/camel-yaml-io/src/test/resources/yaml-route-switch.yaml @@ -0,0 +1,38 @@ +# +# 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. +# + +- route: + customId: true + id: myRoute + from: + uri: direct:start + steps: + - switch: + selector: + header: + expression: department + case: + - customId: true + id: billingCase + value: billing + uri: direct:billing + - value: "001" + uri: direct:numeric + otherwise: + uri: direct:review + - to: + uri: mock:after diff --git a/dsl/camel-endpointdsl/src/test/java/org/apache/camel/builder/endpoint/SwitchEndpointDslTest.java b/dsl/camel-endpointdsl/src/test/java/org/apache/camel/builder/endpoint/SwitchEndpointDslTest.java new file mode 100644 index 000000000000..057ec3e308cd --- /dev/null +++ b/dsl/camel-endpointdsl/src/test/java/org/apache/camel/builder/endpoint/SwitchEndpointDslTest.java @@ -0,0 +1,106 @@ +/* + * 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 java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.model.RouteDefinition; +import org.apache.camel.model.SwitchDefinition; +import org.apache.camel.xml.jaxb.JaxbHelper; +import org.apache.camel.xml.jaxb.JaxbModelToXMLDumper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SwitchEndpointDslTest extends BaseEndpointDslTest { + private final BlockingQueue<Exchange> billing = new LinkedBlockingQueue<>(); + private final BlockingQueue<Exchange> technical = new LinkedBlockingQueue<>(); + private final BlockingQueue<Exchange> review = new LinkedBlockingQueue<>(); + + @Override + public boolean isUseAdviceWith() { + return true; + } + + @Test + public void buildersSurviveCopyAndJaxbDumpAndResolveObjectOptions() throws Exception { + RouteDefinition route = context.getRouteDefinition("tickets"); + SwitchDefinition sw = (SwitchDefinition) route.getOutputs().get(0); + SwitchDefinition copy = sw.copyDefinition(); + assertNotSame(sw.getCases().get(0), copy.getCases().get(0)); + assertNotSame(sw.getOtherwiseDefinition(), copy.getOtherwiseDefinition()); + assertSame(sw.getCases().get(0).getEndpointProducerBuilder(), copy.getCases().get(0).getEndpointProducerBuilder()); + assertSame(sw.getCases().get(1).getEndpointProducerBuilder(), copy.getCases().get(1).getEndpointProducerBuilder()); + assertSame(sw.getOtherwise().getEndpointProducerBuilder(), copy.getOtherwise().getEndpointProducerBuilder()); + copy.getCases().get(0).setUri("mock:copy"); + copy.getOtherwise().setUri("mock:copyFallback"); + assertNull(copy.getCases().get(0).getEndpointProducerBuilder()); + assertNull(copy.getOtherwise().getEndpointProducerBuilder()); + + // Options added after the Switch was configured must also be visible to JAXB. + for (var c : sw.getCases()) { + c.getEndpointProducerBuilder().doSetProperty("blockWhenFull", true); + } + sw.getOtherwise().getEndpointProducerBuilder().doSetProperty("blockWhenFull", true); + String xml = new JaxbModelToXMLDumper().dumpModelAsXml(context, route); + try (var input = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) { + RouteDefinition restored = JaxbHelper.loadRoutesDefinition(context, input).getRoutes().get(0); + SwitchDefinition dumped = (SwitchDefinition) restored.getOutputs().get(0); + assertTrue(dumped.getCases().get(0).getUri().contains("blockWhenFull=true")); + assertTrue(dumped.getCases().get(1).getUri().contains("blockWhenFull=true")); + assertTrue(dumped.getOtherwise().getUri().contains("blockWhenFull=true")); + } + assertEquals("seda://billing", sw.getCases().get(0).getUri().split("\\?")[0]); + context.start(); + getMockEndpoint("mock:billing").expectedBodiesReceived("invoice"); + getMockEndpoint("mock:technical").expectedBodiesReceived("outage"); + getMockEndpoint("mock:review").expectedBodiesReceived("other", "missing"); + template.sendBodyAndHeader("direct:tickets", "invoice", "department", "BILLING"); + template.sendBodyAndHeader("direct:tickets", "outage", "department", "technical"); + template.sendBodyAndHeader("direct:tickets", "other", "department", "other"); + template.sendBody("direct:tickets", "missing"); + MockEndpoint.assertIsSatisfied(context); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new EndpointRouteBuilder() { + @Override + public void configure() { + from(direct("tickets")).routeId("tickets") + .doSwitch(header("department")) + .doCase("billing", seda("billing").advanced().queue(billing)) + .doCase("technical").to(seda("technical").advanced().queue(technical)) + .otherwise(seda("review").advanced().queue(review)) + .end(); + from(seda("billing").advanced().queue(billing)).to(mock("billing")); + from(seda("technical").advanced().queue(technical)).to(mock("technical")); + from(seda("review").advanced().queue(review)).to(mock("review")); + } + }; + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json index 583da9fce285..71af218d0aae 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json @@ -1406,11 +1406,11 @@ "switch": [ { "source": "switch-eip.adoc", - "yaml": "- route:\n from:\n uri: direct:tickets\n steps:\n - switch:\n selector:\n header:\n expression: department\n case:\n - value: billing\n uri: direct:billing\n - value: technical\n uri: direct:technical\n otherwise:\n uri: direct:review\n" + "yaml": "- route:\n from:\n uri: direct:tickets\n steps:\n - switch:\n selector:\n header:\n expression: department\n case:\n - value: billing\n uri: direct:billing\n - value: technical\n uri: direct:technical\n otherwise:\n uri: direct:review\n\n- route:\n from:\n uri: direct:billing\n steps:\n - log: \"Billing: $ [...] }, { "source": "switch-eip.adoc", - "yaml": "- route:\n from:\n uri: direct:billing\n steps:\n - log: \"Billing: ${body}\"\n- route:\n from:\n uri: direct:technical\n steps:\n - log: \"Technical: ${body}\"\n- route:\n from:\n uri: direct:review\n steps:\n - log: \"Review: ${body}\"\n" + "yaml": "- semantic:\n question:\n department:\n type: choice\n instructions: Which department should handle this message?\n criteria:\n billing: Invoices, payments and refunds\n technical: Bugs, outages and technical problems\n other: Any other request\n- route:\n from:\n uri: direct:classify\n steps:\n - switch:\n selector:\n language:\n language: semantic\n [...] } ], "threads": [ diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java index baeaffe09c3d..bd1b71e6e402 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java @@ -81,6 +81,8 @@ public final class CatalogSamples { Map.entry("onFallback", "circuitBreaker"), Map.entry("doCatch", "doTry"), Map.entry("doFinally", "doTry"), + Map.entry("case", "switch"), + Map.entry("switchOtherwise", "switch"), Map.entry("when", "choice"), Map.entry("otherwise", "choice"), Map.entry("onWhen", "onException"), @@ -107,6 +109,8 @@ public final class CatalogSamples { Map.entry("retry", "onException"), Map.entry("error handling", "onException"), Map.entry("errorhandling", "onException"), Map.entry("exception", "onException"), Map.entry("route by content", "choice"), Map.entry("if", "choice"), + Map.entry("route by value", "switch"), Map.entry("switch case", "switch"), + Map.entry("decision table", "switch"), Map.entry("parallel", "multicast"), Map.entry("resilience", "circuitBreaker"), Map.entry("rest api", "rest"), Map.entry("http server", "rest"), Map.entry("endpoint", "rest"), Map.entry("convert", "convertBodyTo"), Map.entry("json", "marshal"), diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java index b4674d69cdae..0d493a50d94e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java @@ -750,7 +750,7 @@ public final class ToolRegistry { })); register(tool("catalog_eips", - "Search EIPs (Enterprise Integration Patterns) like split, aggregate, filter, choice, multicast.") + "Search EIPs (Enterprise Integration Patterns) like split, aggregate, filter, choice, switch, multicast.") .param("filter", "string", "Filter by name, title, or description (case-insensitive substring)", false) .executor((ctx, args) -> { @@ -909,7 +909,7 @@ public final class ToolRegistry { register(tool("catalog_eip_doc", "Get detailed documentation for a Camel EIP (Enterprise Integration Pattern).") .param("eip", "string", - "EIP name (e.g., split, aggregate, filter, choice)", true) + "EIP name (e.g., split, aggregate, filter, choice, switch)", true) .executor((ctx, args) -> { String name = args.get("eip"); if (name == null || name.isBlank()) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java index 669663a3f1b1..091b463f93d8 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java @@ -89,6 +89,27 @@ public class CatalogSamplesTest { assertThat(CatalogSamples.INTENTS).doesNotContainKeys("fan out", "broadcast", "router", "fallback", "split"); } + @Test + void switchIntentsAliasesAndPartsReturnTheDispatchSample() { + CamelCatalog catalog = new DefaultCamelCatalog(); + for (String term : List.of("route by value", "switch case", "decision table", + "decision-table", "dispatch-table", "lookup-table", "case", "switchOtherwise", "switch-otherwise")) { + JsonObject sample = CatalogSamples.sample(catalog, "eip", term, 1); + assertThat(sample.getString("name")).as(term).isEqualTo("switch"); + assertThat(yaml(sample, 0)).contains("switch:", "uri: direct:billing", "uri: direct:technical", + "uri: direct:review"); + } + for (String term : List.of("case", "switchOtherwise")) { + assertThat(CatalogSamples.sample(catalog, term, 1).getString("partOf")).isEqualTo("switch"); + } + for (String alias : List.of("decision-table", "dispatch-table", "lookup-table")) { + assertThat(catalog.suggestEipNames(alias, 1)).containsExactly("switch"); + } + assertThat(CatalogSamples.sample(catalog, "route by content", 1).getString("name")).isEqualTo("choice"); + assertThat(CatalogSamples.sample(catalog, "if", 1).getString("name")).isEqualTo("choice"); + assertThat(CatalogSamples.sample(catalog, "lookup", 1).getString("name")).isEqualTo("enrich"); + } + @Test void unknownNameGetsSuggestions() { JsonObject o = CatalogSamples.sample("aggregat", 2); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java index 21f954d34b30..e3d5c36922a2 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java @@ -63,7 +63,7 @@ public class PromptDefinitions { Call `camel_catalog_find` with the protocol or product (kind="component") to find matching components. ### Step 2: Identify EIPs - Determine which Enterprise Integration Patterns are needed (e.g., split, aggregate, filter, choice). + Determine which Enterprise Integration Patterns are needed (e.g., split, aggregate, filter, choice, switch). Call `camel_catalog_find` with kind="eip" and a term such as fan-out, dedup or retry to find \ matching patterns; `camel_catalog_sample` gives a validated YAML sample of one. diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SwitchSchemaTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SwitchSchemaTest.java index 9d853b194343..e40382ee5c2d 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SwitchSchemaTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SwitchSchemaTest.java @@ -42,6 +42,15 @@ class SwitchSchemaTest { uri: direct:review """; assertThat(validator.validate(route)).isEmpty(); + assertThat(validator.validate(route.replace("value: billing", "id: billingCase\n value: \"001\""))) + .isEmpty(); + assertThat(validator.validate(route.replace("uri: direct:billing", + "uri: direct\n parameters:\n name: billing") + .replace("uri: direct:review", "uri: direct\n parameters:\n name: review"))) + .isEmpty(); + assertThat(validator.validate(route.replace(" uri: direct:billing\n", ""))).isNotEmpty(); + assertThat(validator.validate(route.replace("uri: direct:billing", "steps: [{to: {uri: direct:billing}}]"))) + .isNotEmpty(); assertThat(validator.validate(route.replace("case:", "keys: [department, urgent]\n case:"))) .isNotEmpty(); assertThat(validator.validate(route.replace("value: billing", "values: [department: billing]"))) diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json index 320e9d67ea9f..eacf7a4d52cd 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json @@ -8369,6 +8369,7 @@ "title" : "Switch", "description" : "Evaluates a selector once and dispatches to a fixed endpoint by literal scalar values", "label" : "eip,routing", + "aliases" : [ "decision-table", "dispatch-table", "lookup-table", "case" ], "input" : true, "isListItem" : true, "children" : [ { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SwitchTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SwitchTest.java index 6150f2182916..57aa88df7242 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SwitchTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SwitchTest.java @@ -30,49 +30,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; class SwitchTest extends YamlTestSupport { @Test - void scalarValuesAndEndpointParametersRoundTrip() throws Exception { - loadRoutes(""" - - route: - id: decision - from: - uri: direct:start - steps: - - switch: - selector: - header: - expression: decision - case: - - value: urgent - uri: direct - parameters: - name: urgent - - value: billing - uri: direct:billing - otherwise: - uri: direct - parameters: - name: review - - route: - from: - uri: direct:urgent - steps: - - setBody: - constant: urgent - - route: - from: - uri: direct:billing - steps: - - setBody: - constant: billing - - route: - from: - uri: direct:review - steps: - - setBody: - constant: review - """); + void resourceWithScalarValuesAndEndpointParametersRoundTrips() throws Exception { + loadRoutes(ResourceHelper.resolveResource(context, "classpath:switch.camel.yaml")); SwitchDefinition sw = (SwitchDefinition) context.getRouteDefinition("decision").getOutputs().get(0); assertThat(sw.getCases().get(0).getUri()).isEqualTo("direct:urgent"); + assertThat(sw.getCases().get(0).getId()).isEqualTo("urgentCase"); assertThat(sw.getCases().get(0).getValue()).isEqualTo("urgent"); assertThat(sw.getOtherwise().getUri()).isEqualTo("direct:review"); try (var restored = new DefaultCamelContext()) { @@ -80,6 +42,8 @@ class SwitchTest extends YamlTestSupport { String yaml = new LwModelToYAMLDumper().dumpModelAsYaml(context, route); PluginHelper.getRoutesLoader(restored).loadRoutes(ResourceHelper.fromString(route.getId() + ".yaml", yaml)); } + SwitchDefinition restoredSwitch = (SwitchDefinition) restored.getRouteDefinition("decision").getOutputs().get(0); + assertThat(restoredSwitch.getCases().get(0).getId()).isEqualTo("urgentCase"); restored.start(); try (var template = restored.createProducerTemplate()) { assertThat(template.requestBodyAndHeader("direct:start", "original", "decision", @@ -88,6 +52,7 @@ class SwitchTest extends YamlTestSupport { "BILLING")).isEqualTo("billing"); assertThat(template.requestBodyAndHeader("direct:start", "original", "decision", "other")).isEqualTo("review"); + assertThat(template.requestBody("direct:start", "original")).isEqualTo("review"); } } } diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/resources/switch.camel.yaml b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/resources/switch.camel.yaml new file mode 100644 index 000000000000..f5351393d026 --- /dev/null +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/resources/switch.camel.yaml @@ -0,0 +1,56 @@ +# +# 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. +# + +- route: + id: decision + from: + uri: direct:start + steps: + - switch: + selector: + header: + expression: decision + case: + - id: urgentCase + value: urgent + uri: direct + parameters: + name: urgent + - value: billing + uri: direct:billing + otherwise: + uri: direct + parameters: + name: review +- route: + from: + uri: direct:urgent + steps: + - setBody: + constant: urgent +- route: + from: + uri: direct:billing + steps: + - setBody: + constant: billing +- route: + from: + uri: direct:review + steps: + - setBody: + constant: review
