[
https://issues.apache.org/jira/browse/CAMEL-24988?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Luigi De Masi updated CAMEL-24988:
----------------------------------
Description:
h2. Problem
Camel has no concise, safe way to dispatch a message to one of a fixed set of
endpoints based on a value carried in the message.
The short form today is:
{code:java}
.toD("direct:${header.department}")
{code}
which takes the endpoint name straight out of message data. Camel's threat
model treats external message senders as untrusted, so this hands endpoint
selection to the sender. The safe form is Choice:
{code:java}
.choice()
.when(simple("${header.department} == 'billing'")).to("direct:billing")
.when(simple("${header.department} == 'technical'")).to("direct:technical")
.otherwise().to("direct:general")
.end();
{code}
Correct, but verbose, it repeats the selector in every branch, and it costs an
O(n) predicate scan per message.
h2. Proposal
A new {{switch}} EIP: a decision table mapping literal values to endpoint URIs.
* A {{selector}} expression is evaluated once per entry and produces the value
to match.
* Each {{case}} carries a literal {{value}} and a target {{uri}}, both as
attributes. Matching is *case-insensitive*: real-world data spells the same
thing {{customerID}} and {{customerId}}, and a router that silently drops a
message over letter case is a poor trade for strictness.
* {{otherwise}} is a fallback uri. Null and unmatched selector results use it.
With no {{otherwise}}, processing simply continues.
* Dispatch is a map lookup rather than a scan. Case-insensitivity costs nothing
here: the map is keyed on the normalised value and looked up with the
normalised selector result, so it stays O(1).
The identity of this EIP is *the safe alternative to toD*, not "Choice with
less". The set of destinations is closed and declared in the route; message
data selects among them but cannot name a new one.
h2. Non-goals
Choice is a foundational EIP and is *not modified by this issue*. It keeps
every advanced capability: nested steps per branch, arbitrary predicates,
precondition mode, disabled branches.
{{switch}} deliberately has none of those. One value, one destination, no
nested steps. The moment a branch needs more than a single destination, the
route goes back to Choice. That cliff is intentional and is what keeps
{{switch}} readable as a table.
Specifically out of scope: nested {{steps}}, {{to}}/{{toD}} sub-elements,
predicates, precondition mode. Simple expressions in {{uri}} are not supported
either; {{uri}} takes property placeholders resolved at build time, and
anything genuinely dynamic remains toD's job.
h2. DSL
{{switch}} and {{case}} are both reserved words in Java, so the Java DSL
follows the existing doTry/doCatch/doFinally precedent.
Java:
{code:java}
from("direct:tickets")
.doSwitch(header("department"))
.doCase("billing", "direct:billing")
.doCase("technical", "direct:technical")
.otherwise("direct:general")
.end();
{code}
XML:
{code:xml}
<switch otherwise="direct:general">
<selector><header>department</header></selector>
<case value="billing" uri="direct:billing"/>
<case value="technical" uri="direct:technical"/>
</switch>
{code}
YAML:
{code}
- switch:
selector:
header:
expression: department
case:
- value: billing
uri: direct:billing
- value: orders
uri: kafka
parameters:
topic: cheese
otherwise: direct:general
{code}
h2. Model design
Two new definitions in {{camel-core-model}}:
{code:java}
@XmlRootElement(name = "switch")
@XmlType(propOrder = { "selector", "cases" })
public class SwitchDefinition extends NoOutputDefinition<SwitchDefinition> {
@XmlElement
private ExpressionSubElementDefinition selector;
@XmlElement(name = "case")
private List<SwitchCaseDefinition> cases = new ArrayList<>();
@XmlAttribute
private String otherwise;
}
@XmlRootElement(name = "case")
public class SwitchCaseDefinition extends
OptionalIdentifiedDefinition<SwitchCaseDefinition>
implements EndpointRequiredDefinition {
@XmlAttribute(required = true) private String value;
@XmlAttribute(required = true) private String uri;
}
{code}
Notes:
* The Java field is {{cases}} with {{@XmlElement(name = "case")}}, mirroring
how {{ChoiceDefinition.whenClauses}} already maps to the {{when}} element. This
is the established way the model works around a Java keyword.
* {{cases}} is a *List*, not a Map. Two reasons: each case needs an identity to
hang an id on (see Management below), and declaration order becomes significant
if pattern matching is added later (see Follow-up).
* {{OtherwiseDefinition}} is *not* reusable here. It holds a nested
{{List<ProcessorDefinition<?>>}} outputs list, i.e. it is a steps block. Hence
{{otherwise}} as a plain uri attribute on {{switch}}.
* Do *not* extract a shared base class from {{ChoiceDefinition}}. Inserting an
abstract parent between it and {{NoOutputDefinition}} changes choice's
{{<xs:extension base=...>}} in the generated XSD, which is itself a change to
Choice.
h2. Runtime design
* New {{SwitchProcessor}} in {{camel-core-processor}}, backed by a
{{Map<String, Processor>}} built at reify time. Evaluate the selector once as a
String, look up, dispatch. Null or unmatched goes to the otherwise processor if
configured, otherwise processing continues.
* Selector evaluation follows normal Camel error handling. A selector that
throws does *not* fall through to otherwise.
* Reset the stream cache once after selector evaluation, not per branch.
* New {{SwitchReifier}} in {{camel-core-reifier}}. Registration in
{{ProcessorReifier}} is an additive branch; it does not touch the Choice branch.
Validation at reify time:
* duplicate {{value}} across cases is rejected, *compared case-insensitively*.
{{value="Billing"}} and {{value="billing"}} are the same case and must be an
error rather than one silently shadowing the other.
* a case with no {{value}} or no {{uri}} is rejected
* an empty {{<selector/>}} with no nested expression is rejected with a clear
message rather than an NPE
Normalisation must use {{Locale.ENGLISH}} explicitly, never the default locale.
Lowercasing with the platform locale breaks on a JVM running in a Turkish
locale, where {{"I".toLowerCase()}} yields a dotless i and route dispatch would
silently stop matching. {{PatternHelper}} already lowercases with
{{Locale.ENGLISH}} for exactly this reason.
h2. YAML uri and parameters
{{case}} should accept the canonical form that visual tooling such as Karavan
and Kaoto emits, rather than requiring a single assembled URI string:
{code:yaml}
- value: orders
uri: kafka
parameters:
topic: cheese
{code}
This is a YAML-DSL parse-time affordance only.
{{YamlDeserializerEndpointAwareBase}} collapses uri plus parameters into one
URI via {{YamlSupport.createEndpointUri(...)}} before the model is built, so
the model only ever holds a single uri String. XML keeps the assembled form.
The opt-in currently lives in {{GenerateYamlDeserializersMojo}} (around line
591) as a hard-coded check on three class names: {{SendDefinition}},
{{ToDynamicDefinition}}, {{PollDefinition}}.
*Preferred approach:* replace that condition with the existing
{{EndpointRequiredDefinition}} marker interface in {{camel-core-model}}, which
already declares {{getEndpointUri()}} and is already implemented by
{{SendDefinition}}, {{PollDefinition}} and {{FromDefinition}}.
{{SwitchCaseDefinition}} then implements it and gets uri plus parameters for
free, with no new hard-coded model names in the generator. There is precedent
in the very next branch of the same method, which tests the
{{HasExpressionType}} interface rather than class names.
Caveat to verify during implementation: {{ToDynamicDefinition}} does not
implement {{EndpointRequiredDefinition}} today. It either gains the interface,
or stays as an extra term in the condition.
Rejected alternatives: making {{SwitchCaseDefinition}} extend
{{SendDefinition}} (drags in pattern and variableSend, too heavy for a table
row), and adding a fourth hard-coded class name to the generator.
h2. Management and observability
* New {{ManagedSwitch}} MBean, registered through an additive branch in
{{DefaultManagementObjectStrategy}}. {{ManagedChoice}} is not modified.
* Each case gets an id via {{idOrCreate(...)}} at reify time, the way
{{ChoiceReifier}} already does for {{when}}, so individual cases appear in JMX,
tracing and the debugger.
* {{extendedInformation()}} reports the table: value, uri and per-case hit
count.
h2. Testing
* {{camel-core}}: exact match, no match with and without otherwise, null
selector result, empty string value, nested switch, repeated entry via loop,
selector throwing an exception, duplicate value rejection, missing uri
rejection.
* {{camel-xml-io-dsl}} and {{camel-spring-xml}}: XML round trip including JAXB
namespace handling for an xpath selector.
* {{camel-yaml-dsl}}: YAML round trip, plus the uri-with-parameters form.
* {{camel-java-io}}: Java DSL export.
* {{camel-management}}: MBean table contents and per-case counts.
h2. Documentation
* New {{switch-eip.adoc}} EIP page with all three DSLs.
* Explicitly document the relationship to Choice and to toD, including when
*not* to use switch.
* Document that {{value}} is literal and matched case-insensitively, and that
{{uri}} supports property placeholders but not simple expressions.
h2. Possible follow-up, not in scope here
Pattern matching on cases, so a case could match {{billing*}}.
Design constraints to preserve now so this stays possible later:
* Do *not* extend {{value}} to mean a pattern. A route written today with a
literal asterisk in {{value}} would silently change meaning on upgrade. A
separate {{pattern}} attribute alongside {{value}} avoids any reinterpretation.
* Keep {{cases}} an ordered List. Exact matching is order-independent, but
first-match-wins pattern matching is not.
* The fast path survives: exact values stay in the map for O(1), pattern cases
are scanned in declaration order only after a map miss, so routes without
patterns pay nothing.
Camel's house matcher is {{PatternHelper.matchPattern(name, pattern)}} in
{{camel-support}}, used by Camel* header filtering and intercept. Its
documented rule order is exact match, then wildcard (pattern ends with {{*}}
and name starts with the prefix), then regex.
{{PatternHelper}} being case-insensitive throughout, in both the exact branch
({{equalsIgnoreCase}}) and the wildcard branch, now lines up with this EIP
rather than fighting it, since {{value}} is case-insensitive too. No change is
needed there for the base feature.
One thing to decide deliberately at that point: {{PatternHelper}} also accepts
regular expressions, which may be more than a decision table should offer. If
wildcard-only matching is wanted, that is a small wildcard-only variant in
{{camel-support}} rather than a change to the existing helper, which other call
sites depend on.
h2. Origin
Split out of the review of CAMEL-24977 and PR #26813, where this capability was
originally proposed as a modification to the Choice EIP. That approach changed
{{ChoiceDefinition}}, {{WhenDefinition}}, {{ChoiceProcessor}},
{{ChoiceReifier}}, {{ManagedChoice}}, the generated choice and when metadata,
both XSDs and the YAML schemas, and relaxed {{BasicExpressionNode}} so a
{{<when>}} no longer required a predicate. Choice has been stable for twenty
years and should stay that way, so the capability is being proposed here as its
own EIP instead.
----
_Filed by Claude Code on behalf of [~davsclaus]. cc [~ldemasi], whose work on
CAMEL-24977 this design is derived from._
was:
h2. Problem
Camel has no concise, safe way to dispatch a message to one of a fixed set of
endpoints based on a value carried in the message.
The short form today is:
{code:java}
.toD("direct:${header.department}")
{code}
which takes the endpoint name straight out of message data. Camel's threat
model treats external message senders as untrusted, so this hands endpoint
selection to the sender. The safe form is Choice:
{code:java}
.choice()
.when(simple("${header.department} == 'billing'")).to("direct:billing")
.when(simple("${header.department} == 'technical'")).to("direct:technical")
.otherwise().to("direct:general")
.end();
{code}
Correct, but verbose, it repeats the selector in every branch, and it costs an
O(n) predicate scan per message.
h2. Proposal
A new {{switch}} EIP: a decision table mapping literal values to endpoint URIs.
* A {{selector}} expression is evaluated once per entry and produces the value
to match.
* Each {{case}} carries a literal {{value}} and a target {{uri}}, both as
attributes. Matching is *case-insensitive*: real-world data spells the same
thing {{customerID}} and {{customerId}}, and a router that silently drops a
message over letter case is a poor trade for strictness.
* {{otherwise}} is a fallback uri. Null and unmatched selector results use it.
With no {{otherwise}}, processing simply continues.
* Dispatch is a map lookup rather than a scan. Case-insensitivity costs nothing
here: the map is keyed on the normalised value and looked up with the
normalised selector result, so it stays O(1).
The identity of this EIP is *the safe alternative to toD*, not "Choice with
less". The set of destinations is closed and declared in the route; message
data selects among them but cannot name a new one.
h2. Non-goals
Choice is a foundational EIP and is *not modified by this issue*. It keeps
every advanced capability: nested steps per branch, arbitrary predicates,
precondition mode, disabled branches.
{{switch}} deliberately has none of those. One value, one destination, no
nested steps. The moment a branch needs more than a single destination, the
route goes back to Choice. That cliff is intentional and is what keeps
{{switch}} readable as a table.
Specifically out of scope: nested {{steps}}, {{to}}/{{toD}} sub-elements,
predicates, precondition mode. Simple expressions in {{uri}} are not supported
either; {{uri}} takes property placeholders resolved at build time, and
anything genuinely dynamic remains toD's job.
h2. DSL
{{switch}} and {{case}} are both reserved words in Java, so the Java DSL
follows the existing doTry/doCatch/doFinally precedent.
Java:
{code:java}
from("direct:tickets")
.doSwitch(header("department"))
.doCase("billing", "direct:billing")
.doCase("technical", "direct:technical")
.otherwise("direct:general")
.end();
{code}
XML:
{code:xml}
<switch otherwise="direct:general">
<selector><header>department</header></selector>
<case value="billing" uri="direct:billing"/>
<case value="technical" uri="direct:technical"/>
</switch>
{code}
YAML:
{code:yaml}
- switch:
selector:
header:
expression: department
case:
- value: billing
uri: direct:billing
- value: orders
uri: kafka
parameters:
topic: cheese
otherwise: direct:general
{code}
h2. Model design
Two new definitions in {{camel-core-model}}:
{code:java}
@XmlRootElement(name = "switch")
@XmlType(propOrder = { "selector", "cases" })
public class SwitchDefinition extends NoOutputDefinition<SwitchDefinition> {
@XmlElement
private ExpressionSubElementDefinition selector;
@XmlElement(name = "case")
private List<SwitchCaseDefinition> cases = new ArrayList<>();
@XmlAttribute
private String otherwise;
}
@XmlRootElement(name = "case")
public class SwitchCaseDefinition extends
OptionalIdentifiedDefinition<SwitchCaseDefinition>
implements EndpointRequiredDefinition {
@XmlAttribute(required = true) private String value;
@XmlAttribute(required = true) private String uri;
}
{code}
Notes:
* The Java field is {{cases}} with {{@XmlElement(name = "case")}}, mirroring
how {{ChoiceDefinition.whenClauses}} already maps to the {{when}} element. This
is the established way the model works around a Java keyword.
* {{cases}} is a *List*, not a Map. Two reasons: each case needs an identity to
hang an id on (see Management below), and declaration order becomes significant
if pattern matching is added later (see Follow-up).
* {{OtherwiseDefinition}} is *not* reusable here. It holds a nested
{{List<ProcessorDefinition<?>>}} outputs list, i.e. it is a steps block. Hence
{{otherwise}} as a plain uri attribute on {{switch}}.
* Do *not* extract a shared base class from {{ChoiceDefinition}}. Inserting an
abstract parent between it and {{NoOutputDefinition}} changes choice's
{{<xs:extension base=...>}} in the generated XSD, which is itself a change to
Choice.
h2. Runtime design
* New {{SwitchProcessor}} in {{camel-core-processor}}, backed by a
{{Map<String, Processor>}} built at reify time. Evaluate the selector once as a
String, look up, dispatch. Null or unmatched goes to the otherwise processor if
configured, otherwise processing continues.
* Selector evaluation follows normal Camel error handling. A selector that
throws does *not* fall through to otherwise.
* Reset the stream cache once after selector evaluation, not per branch.
* New {{SwitchReifier}} in {{camel-core-reifier}}. Registration in
{{ProcessorReifier}} is an additive branch; it does not touch the Choice branch.
Validation at reify time:
* duplicate {{value}} across cases is rejected, *compared case-insensitively*.
{{value="Billing"}} and {{value="billing"}} are the same case and must be an
error rather than one silently shadowing the other.
* a case with no {{value}} or no {{uri}} is rejected
* an empty {{<selector/>}} with no nested expression is rejected with a clear
message rather than an NPE
Normalisation must use {{Locale.ENGLISH}} explicitly, never the default locale.
Lowercasing with the platform locale breaks on a JVM running in a Turkish
locale, where {{"I".toLowerCase()}} yields a dotless i and route dispatch would
silently stop matching. {{PatternHelper}} already lowercases with
{{Locale.ENGLISH}} for exactly this reason.
h2. YAML uri and parameters
{{case}} should accept the canonical form that visual tooling such as Karavan
and Kaoto emits, rather than requiring a single assembled URI string:
{code:yaml}
- value: orders
uri: kafka
parameters:
topic: cheese
{code}
This is a YAML-DSL parse-time affordance only.
{{YamlDeserializerEndpointAwareBase}} collapses uri plus parameters into one
URI via {{YamlSupport.createEndpointUri(...)}} before the model is built, so
the model only ever holds a single uri String. XML keeps the assembled form.
The opt-in currently lives in {{GenerateYamlDeserializersMojo}} (around line
591) as a hard-coded check on three class names: {{SendDefinition}},
{{ToDynamicDefinition}}, {{PollDefinition}}.
*Preferred approach:* replace that condition with the existing
{{EndpointRequiredDefinition}} marker interface in {{camel-core-model}}, which
already declares {{getEndpointUri()}} and is already implemented by
{{SendDefinition}}, {{PollDefinition}} and {{FromDefinition}}.
{{SwitchCaseDefinition}} then implements it and gets uri plus parameters for
free, with no new hard-coded model names in the generator. There is precedent
in the very next branch of the same method, which tests the
{{HasExpressionType}} interface rather than class names.
Caveat to verify during implementation: {{ToDynamicDefinition}} does not
implement {{EndpointRequiredDefinition}} today. It either gains the interface,
or stays as an extra term in the condition.
Rejected alternatives: making {{SwitchCaseDefinition}} extend
{{SendDefinition}} (drags in pattern and variableSend, too heavy for a table
row), and adding a fourth hard-coded class name to the generator.
h2. Management and observability
* New {{ManagedSwitch}} MBean, registered through an additive branch in
{{DefaultManagementObjectStrategy}}. {{ManagedChoice}} is not modified.
* Each case gets an id via {{idOrCreate(...)}} at reify time, the way
{{ChoiceReifier}} already does for {{when}}, so individual cases appear in JMX,
tracing and the debugger.
* {{extendedInformation()}} reports the table: value, uri and per-case hit
count.
h2. Testing
* {{camel-core}}: exact match, no match with and without otherwise, null
selector result, empty string value, nested switch, repeated entry via loop,
selector throwing an exception, duplicate value rejection, missing uri
rejection.
* {{camel-xml-io-dsl}} and {{camel-spring-xml}}: XML round trip including JAXB
namespace handling for an xpath selector.
* {{camel-yaml-dsl}}: YAML round trip, plus the uri-with-parameters form.
* {{camel-java-io}}: Java DSL export.
* {{camel-management}}: MBean table contents and per-case counts.
h2. Documentation
* New {{switch-eip.adoc}} EIP page with all three DSLs.
* Explicitly document the relationship to Choice and to toD, including when
*not* to use switch.
* Document that {{value}} is literal and matched case-insensitively, and that
{{uri}} supports property placeholders but not simple expressions.
h2. Possible follow-up, not in scope here
Pattern matching on cases, so a case could match {{billing*}}.
Design constraints to preserve now so this stays possible later:
* Do *not* extend {{value}} to mean a pattern. A route written today with a
literal asterisk in {{value}} would silently change meaning on upgrade. A
separate {{pattern}} attribute alongside {{value}} avoids any reinterpretation.
* Keep {{cases}} an ordered List. Exact matching is order-independent, but
first-match-wins pattern matching is not.
* The fast path survives: exact values stay in the map for O(1), pattern cases
are scanned in declaration order only after a map miss, so routes without
patterns pay nothing.
Camel's house matcher is {{PatternHelper.matchPattern(name, pattern)}} in
{{camel-support}}, used by Camel* header filtering and intercept. Its
documented rule order is exact match, then wildcard (pattern ends with {{*}}
and name starts with the prefix), then regex.
{{PatternHelper}} being case-insensitive throughout, in both the exact branch
({{equalsIgnoreCase}}) and the wildcard branch, now lines up with this EIP
rather than fighting it, since {{value}} is case-insensitive too. No change is
needed there for the base feature.
One thing to decide deliberately at that point: {{PatternHelper}} also accepts
regular expressions, which may be more than a decision table should offer. If
wildcard-only matching is wanted, that is a small wildcard-only variant in
{{camel-support}} rather than a change to the existing helper, which other call
sites depend on.
h2. Origin
Split out of the review of CAMEL-24977 and PR #26813, where this capability was
originally proposed as a modification to the Choice EIP. That approach changed
{{ChoiceDefinition}}, {{WhenDefinition}}, {{ChoiceProcessor}},
{{ChoiceReifier}}, {{ManagedChoice}}, the generated choice and when metadata,
both XSDs and the YAML schemas, and relaxed {{BasicExpressionNode}} so a
{{<when>}} no longer required a predicate. Choice has been stable for twenty
years and should stay that way, so the capability is being proposed here as its
own EIP instead.
----
_Filed by Claude Code on behalf of [~davsclaus]. cc [~ldemasi], whose work on
CAMEL-24977 this design is derived from._
> camel-core: Add switch EIP for literal value-based endpoint dispatch
> --------------------------------------------------------------------
>
> Key: CAMEL-24988
> URL: https://issues.apache.org/jira/browse/CAMEL-24988
> Project: Camel
> Issue Type: New Feature
> Reporter: Claus Ibsen
> Assignee: Luigi De Masi
> Priority: Minor
>
> h2. Problem
> Camel has no concise, safe way to dispatch a message to one of a fixed set of
> endpoints based on a value carried in the message.
> The short form today is:
> {code:java}
> .toD("direct:${header.department}")
> {code}
> which takes the endpoint name straight out of message data. Camel's threat
> model treats external message senders as untrusted, so this hands endpoint
> selection to the sender. The safe form is Choice:
> {code:java}
> .choice()
> .when(simple("${header.department} == 'billing'")).to("direct:billing")
> .when(simple("${header.department} ==
> 'technical'")).to("direct:technical")
> .otherwise().to("direct:general")
> .end();
> {code}
> Correct, but verbose, it repeats the selector in every branch, and it costs
> an O(n) predicate scan per message.
> h2. Proposal
> A new {{switch}} EIP: a decision table mapping literal values to endpoint
> URIs.
> * A {{selector}} expression is evaluated once per entry and produces the
> value to match.
> * Each {{case}} carries a literal {{value}} and a target {{uri}}, both as
> attributes. Matching is *case-insensitive*: real-world data spells the same
> thing {{customerID}} and {{customerId}}, and a router that silently drops a
> message over letter case is a poor trade for strictness.
> * {{otherwise}} is a fallback uri. Null and unmatched selector results use
> it. With no {{otherwise}}, processing simply continues.
> * Dispatch is a map lookup rather than a scan. Case-insensitivity costs
> nothing here: the map is keyed on the normalised value and looked up with the
> normalised selector result, so it stays O(1).
> The identity of this EIP is *the safe alternative to toD*, not "Choice with
> less". The set of destinations is closed and declared in the route; message
> data selects among them but cannot name a new one.
> h2. Non-goals
> Choice is a foundational EIP and is *not modified by this issue*. It keeps
> every advanced capability: nested steps per branch, arbitrary predicates,
> precondition mode, disabled branches.
> {{switch}} deliberately has none of those. One value, one destination, no
> nested steps. The moment a branch needs more than a single destination, the
> route goes back to Choice. That cliff is intentional and is what keeps
> {{switch}} readable as a table.
> Specifically out of scope: nested {{steps}}, {{to}}/{{toD}} sub-elements,
> predicates, precondition mode. Simple expressions in {{uri}} are not
> supported either; {{uri}} takes property placeholders resolved at build time,
> and anything genuinely dynamic remains toD's job.
> h2. DSL
> {{switch}} and {{case}} are both reserved words in Java, so the Java DSL
> follows the existing doTry/doCatch/doFinally precedent.
> Java:
> {code:java}
> from("direct:tickets")
> .doSwitch(header("department"))
> .doCase("billing", "direct:billing")
> .doCase("technical", "direct:technical")
> .otherwise("direct:general")
> .end();
> {code}
> XML:
> {code:xml}
> <switch otherwise="direct:general">
> <selector><header>department</header></selector>
> <case value="billing" uri="direct:billing"/>
> <case value="technical" uri="direct:technical"/>
> </switch>
> {code}
> YAML:
> {code}
> - switch:
> selector:
> header:
> expression: department
> case:
> - value: billing
> uri: direct:billing
> - value: orders
> uri: kafka
> parameters:
> topic: cheese
> otherwise: direct:general
> {code}
> h2. Model design
> Two new definitions in {{camel-core-model}}:
> {code:java}
> @XmlRootElement(name = "switch")
> @XmlType(propOrder = { "selector", "cases" })
> public class SwitchDefinition extends NoOutputDefinition<SwitchDefinition> {
> @XmlElement
> private ExpressionSubElementDefinition selector;
> @XmlElement(name = "case")
> private List<SwitchCaseDefinition> cases = new ArrayList<>();
> @XmlAttribute
> private String otherwise;
> }
> @XmlRootElement(name = "case")
> public class SwitchCaseDefinition extends
> OptionalIdentifiedDefinition<SwitchCaseDefinition>
> implements EndpointRequiredDefinition {
> @XmlAttribute(required = true) private String value;
> @XmlAttribute(required = true) private String uri;
> }
> {code}
> Notes:
> * The Java field is {{cases}} with {{@XmlElement(name = "case")}}, mirroring
> how {{ChoiceDefinition.whenClauses}} already maps to the {{when}} element.
> This is the established way the model works around a Java keyword.
> * {{cases}} is a *List*, not a Map. Two reasons: each case needs an identity
> to hang an id on (see Management below), and declaration order becomes
> significant if pattern matching is added later (see Follow-up).
> * {{OtherwiseDefinition}} is *not* reusable here. It holds a nested
> {{List<ProcessorDefinition<?>>}} outputs list, i.e. it is a steps block.
> Hence {{otherwise}} as a plain uri attribute on {{switch}}.
> * Do *not* extract a shared base class from {{ChoiceDefinition}}. Inserting
> an abstract parent between it and {{NoOutputDefinition}} changes choice's
> {{<xs:extension base=...>}} in the generated XSD, which is itself a change to
> Choice.
> h2. Runtime design
> * New {{SwitchProcessor}} in {{camel-core-processor}}, backed by a
> {{Map<String, Processor>}} built at reify time. Evaluate the selector once as
> a String, look up, dispatch. Null or unmatched goes to the otherwise
> processor if configured, otherwise processing continues.
> * Selector evaluation follows normal Camel error handling. A selector that
> throws does *not* fall through to otherwise.
> * Reset the stream cache once after selector evaluation, not per branch.
> * New {{SwitchReifier}} in {{camel-core-reifier}}. Registration in
> {{ProcessorReifier}} is an additive branch; it does not touch the Choice
> branch.
> Validation at reify time:
> * duplicate {{value}} across cases is rejected, *compared
> case-insensitively*. {{value="Billing"}} and {{value="billing"}} are the same
> case and must be an error rather than one silently shadowing the other.
> * a case with no {{value}} or no {{uri}} is rejected
> * an empty {{<selector/>}} with no nested expression is rejected with a clear
> message rather than an NPE
> Normalisation must use {{Locale.ENGLISH}} explicitly, never the default
> locale. Lowercasing with the platform locale breaks on a JVM running in a
> Turkish locale, where {{"I".toLowerCase()}} yields a dotless i and route
> dispatch would silently stop matching. {{PatternHelper}} already lowercases
> with {{Locale.ENGLISH}} for exactly this reason.
> h2. YAML uri and parameters
> {{case}} should accept the canonical form that visual tooling such as Karavan
> and Kaoto emits, rather than requiring a single assembled URI string:
> {code:yaml}
> - value: orders
> uri: kafka
> parameters:
> topic: cheese
> {code}
> This is a YAML-DSL parse-time affordance only.
> {{YamlDeserializerEndpointAwareBase}} collapses uri plus parameters into one
> URI via {{YamlSupport.createEndpointUri(...)}} before the model is built, so
> the model only ever holds a single uri String. XML keeps the assembled form.
> The opt-in currently lives in {{GenerateYamlDeserializersMojo}} (around line
> 591) as a hard-coded check on three class names: {{SendDefinition}},
> {{ToDynamicDefinition}}, {{PollDefinition}}.
> *Preferred approach:* replace that condition with the existing
> {{EndpointRequiredDefinition}} marker interface in {{camel-core-model}},
> which already declares {{getEndpointUri()}} and is already implemented by
> {{SendDefinition}}, {{PollDefinition}} and {{FromDefinition}}.
> {{SwitchCaseDefinition}} then implements it and gets uri plus parameters for
> free, with no new hard-coded model names in the generator. There is precedent
> in the very next branch of the same method, which tests the
> {{HasExpressionType}} interface rather than class names.
> Caveat to verify during implementation: {{ToDynamicDefinition}} does not
> implement {{EndpointRequiredDefinition}} today. It either gains the
> interface, or stays as an extra term in the condition.
> Rejected alternatives: making {{SwitchCaseDefinition}} extend
> {{SendDefinition}} (drags in pattern and variableSend, too heavy for a table
> row), and adding a fourth hard-coded class name to the generator.
> h2. Management and observability
> * New {{ManagedSwitch}} MBean, registered through an additive branch in
> {{DefaultManagementObjectStrategy}}. {{ManagedChoice}} is not modified.
> * Each case gets an id via {{idOrCreate(...)}} at reify time, the way
> {{ChoiceReifier}} already does for {{when}}, so individual cases appear in
> JMX, tracing and the debugger.
> * {{extendedInformation()}} reports the table: value, uri and per-case hit
> count.
> h2. Testing
> * {{camel-core}}: exact match, no match with and without otherwise, null
> selector result, empty string value, nested switch, repeated entry via loop,
> selector throwing an exception, duplicate value rejection, missing uri
> rejection.
> * {{camel-xml-io-dsl}} and {{camel-spring-xml}}: XML round trip including
> JAXB namespace handling for an xpath selector.
> * {{camel-yaml-dsl}}: YAML round trip, plus the uri-with-parameters form.
> * {{camel-java-io}}: Java DSL export.
> * {{camel-management}}: MBean table contents and per-case counts.
> h2. Documentation
> * New {{switch-eip.adoc}} EIP page with all three DSLs.
> * Explicitly document the relationship to Choice and to toD, including when
> *not* to use switch.
> * Document that {{value}} is literal and matched case-insensitively, and that
> {{uri}} supports property placeholders but not simple expressions.
> h2. Possible follow-up, not in scope here
> Pattern matching on cases, so a case could match {{billing*}}.
> Design constraints to preserve now so this stays possible later:
> * Do *not* extend {{value}} to mean a pattern. A route written today with a
> literal asterisk in {{value}} would silently change meaning on upgrade. A
> separate {{pattern}} attribute alongside {{value}} avoids any
> reinterpretation.
> * Keep {{cases}} an ordered List. Exact matching is order-independent, but
> first-match-wins pattern matching is not.
> * The fast path survives: exact values stay in the map for O(1), pattern
> cases are scanned in declaration order only after a map miss, so routes
> without patterns pay nothing.
> Camel's house matcher is {{PatternHelper.matchPattern(name, pattern)}} in
> {{camel-support}}, used by Camel* header filtering and intercept. Its
> documented rule order is exact match, then wildcard (pattern ends with {{*}}
> and name starts with the prefix), then regex.
> {{PatternHelper}} being case-insensitive throughout, in both the exact branch
> ({{equalsIgnoreCase}}) and the wildcard branch, now lines up with this EIP
> rather than fighting it, since {{value}} is case-insensitive too. No change
> is needed there for the base feature.
> One thing to decide deliberately at that point: {{PatternHelper}} also
> accepts regular expressions, which may be more than a decision table should
> offer. If wildcard-only matching is wanted, that is a small wildcard-only
> variant in {{camel-support}} rather than a change to the existing helper,
> which other call sites depend on.
> h2. Origin
> Split out of the review of CAMEL-24977 and PR #26813, where this capability
> was originally proposed as a modification to the Choice EIP. That approach
> changed {{ChoiceDefinition}}, {{WhenDefinition}}, {{ChoiceProcessor}},
> {{ChoiceReifier}}, {{ManagedChoice}}, the generated choice and when metadata,
> both XSDs and the YAML schemas, and relaxed {{BasicExpressionNode}} so a
> {{<when>}} no longer required a predicate. Choice has been stable for twenty
> years and should stay that way, so the capability is being proposed here as
> its own EIP instead.
> ----
> _Filed by Claude Code on behalf of [~davsclaus]. cc [~ldemasi], whose work on
> CAMEL-24977 this design is derived from._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)