[
https://issues.apache.org/jira/browse/CAMEL-24988?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Luigi De Masi reassigned CAMEL-24988:
-------------------------------------
Assignee: Luigi De Masi
> 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: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._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)