[ 
https://issues.apache.org/jira/browse/CAMEL-24988?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18120696#comment-18120696
 ] 

Claus Ibsen commented on CAMEL-24988:
-------------------------------------

Yeah sure make something and its easier to look at when we have some actual 
code tests and examples / blogs to look at. And then allow this to be changed 
in upcoming releases.

> 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
>         Attachments: CAMEL-24988-reference-choice-selector.diff
>
>
> 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:java}
> - 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)

Reply via email to