[
https://issues.apache.org/jira/browse/CAMEL-24988?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18120695#comment-18120695
]
Luigi De Masi commented on CAMEL-24988:
---------------------------------------
Hi [~davsclaus], replying to [your scope
clarification|https://issues.apache.org/jira/browse/CAMEL-24988?focusedCommentId=18120688#comment-18120688]:
My main concern is keeping routing decisions readable and declarative, while
avoiding choices that unnecessarily constrain Switch’s evolution. I understand
the desire for a small initial scope. Could we distinguish the String contract
of the scalar {{value}} form from the broader design of Switch, so that named
literal combinations could be added without changing existing routes? For the
composite use case, I would like to assess simplicity at the route level,
including any mapping code authors would otherwise need.
_Comment written by Codex on behalf of [~ldemasi]_
> 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)