blob: 8c021b46e5ac3322c6d3333cee42fb8be4e4d521 [file]
---
title: Reading Java DSL routes without compiling them
authors:
- "@davsclaus"
reviewers: []
approvers: []
creation-date: 2026-09-30
last-updated: 2026-09-30
status: implementable
see-also:
- "CAMEL-25148"
- "CAMEL-25143"
- "CAMEL-25157"
- "CAMEL-25161"
- "CAMEL-25166"
replaces: []
superseded-by: []
---
== Summary
`LwJavaParser` in `camel-java-io` reads the routes of a Java DSL source (a `RouteBuilder`, a `LambdaRouteBuilder` or a
snippet as in the documentation) into the Camel model without compiling it, the reverse of `LwModelToJavaDumper`. It
reads the source as text, then replays each chain of calls against Camel's own DSL to build the model. Nothing of the
project it reads is compiled, loaded or run; it needs no class path of the project and takes milliseconds.
== Motivation
XML and YAML routes can be read into the model by tools; Java routes could only be read by compiling the
`RouteBuilder` and running `configure()` (`camel transform`), which needs the full class path of the project, may
download dependencies, takes seconds and runs the project's code. Tools that want to show or check Java routes
(the project overview and the TUI, AI prompts, documentation checks) fell back on pattern matching on string literals,
which misses constants, nesting, error paths and the endpoint DSL.
== Goals
* The model a compiled route has, for the ways people usually write routes.
* No code of the parsed project runs, and no class of it is loaded; a source nobody vetted is safe to read.
* Say clearly what could not be read, rather than guess: the result lists each unresolved part with its line.
* Pluggable knowledge a tool has and the parser does not: the catalog (endpoint DSL, component header constants and
enums) and the project's other source files.
== Non-Goals
* A Java compiler. Values computed at runtime, routes built in helper methods of other classes, generated code, and
lambdas or processors (they are placeholders) are out of reach.
* Replacing `camel transform`'s compiling mode, which stays the precise option.
== Proposal
=== Reading the source
A small tokenizer and parser read the package, the imports (static ones too), the constants of the class, the class
names it declares and extends, and the statements of each `configure()` (or `configuration()`), each builder lambda
(`rb -> rb.from(...)`), or the whole source when it is a snippet. Each statement starting with a DSL call is a chain of
calls with arguments: literals, text blocks, constants, `+` and arithmetic, class literals, nested calls, `new`,
lambdas. Control statements (`for`, `if`, `try`) are stepped into: a route built in a loop is read once, with the loop
variable unresolved.
=== Replay rather than a mapping table
Each chain is replayed by reflection against Camel's own DSL: `from("a").to("b")` calls `from("a")` on a route
builder and `to("b")` on the route it returns. `end()`, blocks, expression clauses (`setHeader("x").constant("y")`)
and ids behave as in a compiled route, for every EIP, with no table to keep in step with the model. The alternative,
a table of fluent method to model class generated from the model metadata, would need hand-kept overrides for the
argument mapping of hundreds of overloads and reimplement the DSL's semantics.
Afterwards the model is normalized to what the other DSLs have: expression clauses and value builders become their
language, a class kept as a `Class` option also gets its name in the String option next to it, and
`constant(value)` keeps its text.
Overloads are chosen as Java does: the one the arguments fit, preferring exact types over stand-ins, and of equal ones
the most specific; candidates are tried in a stable order.
=== Values the parser cannot see
* A class of the project (`onException(InvalidOrderException.class)`) becomes an empty abstract stub class with its
name, so the model keeps the name. Nested classes get their binary name.
* A lambda, an anonymous class or `new MyProcessor()` becomes a placeholder object of the interface the DSL method
takes; `new Foo()` prefers the overload whose parameter type `Foo` is named for.
* A value it cannot work out is kept in the model as `?{source text}` and reported.
* An exception of the JDK or of Camel (`new IllegalArgumentException("x")`) is created, from messages and causes only.
=== Plugs
* `EndpointDslResolver`: the endpoint DSL (`kafka("orders").brokers("b")`). The default reverses the rules the
endpoint DSL is generated with (the factory is the scheme in camel case, with four exceptions; each builder method is
the option of that name), for sources that use the endpoint DSL. A resolver with the catalog confirms components,
checks options and knows the prefix of multi-value options.
* `ConstantResolver`: constants of classes the parser cannot see. camel-jbang's resolver reads the project's other
source files, and the catalog's component header constants (`KafkaConstants.KEY`) and enum values
(`HazelcastOperation.PUT`).
=== Where it is used
camel-jbang reads Java routes with the parser only; the pattern matching on string literals it had before is gone
(CAMEL-25199):
* the project overview (`camel overview`, the `camel_project_overview` MCP tool, the TUI Architecture and Topology
views);
* the consumer check of `camel validate`: which `direct:` and `seda:` endpoints the Java routes of an application
consume, with constants, formats and the endpoint DSL resolved;
* the jump links between routes in the TUI Source tab.
=== Transforming Java routes
`camel transform route` reads Java routes with the parser by default and writes them with the dump strategy of a
running Camel, in a CamelContext that is never started (CAMEL-25202); it compiles the routes only when one cannot be
read completely, or with `--compile`.
`camel cmd route-diagram`, `route-topology` and `route-structure` (and the `camel_render_route_diagram` MCP tool) read
Java source files the same way, and dump the route structure and topology JSON from that CamelContext (CAMEL-25565). A
route read only partly is still shown: a lambda or a processor is the placeholder step, and the parts the parser could
not read are listed. They compile the routes only with `--compile`.
== Security
The parser reads sources nobody vetted: a cloned example opened in the TUI, documentation, a project an AI agent
points a tool at. What it guarantees, and how each is enforced:
* *No code of the project runs, and none of its classes is loaded.* Only Camel's DSL is called: methods declared by
Camel's model and builder types (`org.apache.camel.model.*`, `org.apache.camel.builder.*`,
`org.apache.camel.support.builder.*`), and the static DSL of `Builder`, `PredicateBuilder` and
`AggregationStrategies`. A class of the project is a stub with no code, in a class loader of its own for one parse.
* *No CamelContext.* The replay builder never has one: no method that takes or returns a `CamelContext`, none of the
builder's life cycle (populate, configure, prepare, update, initialize, set, check, add, remove, customize), no
registry or properties (`bindToRegistry`, `propertyInject`, `endpoint`). So nothing is resolved, looked up or
started, and property placeholders and vault references stay as written.
* *The reachable DSL is pinned.* The methods a source can call on the route builder and the static DSL are listed in
`replay-builder-methods.txt` in camel-java-io's tests; a method added to Camel fails the build until someone has
checked it only builds the model, then regenerated the list (`-Dcamel.java.in.updateSurface=true`). The model's
definitions are data objects: their methods are covered by the package and context rules.
* *Reading a constant initializes its class*: only public static final Strings, primitives and enums of Camel classes
and a few JDK classes (the boxed numbers, `TimeUnit`, `StandardCharsets`) are read.
* *Only exceptions are created*, through constructors taking messages and causes.
* *Bounded work.* At most 1000 stub classes per parse, arguments nested at most 100 deep, sources of at most 2 MB;
every loop of the parser makes progress, and `parse()` never throws. A fuzz test feeds cut and broken sources.
* *Reflection stays within Camel.* Public DSL methods are called as they are; only the route builder's protected
methods (as a `RouteBuilder` subclass calls them) are made accessible. This assumes Camel's builder and model
packages are open to camel-java-io, as they are on the class path and in Camel's own module layout; a strong module
boundary on `org.apache.camel.builder` or `org.apache.camel.model` would need them opened to camel-java-io.
* *Its output is data.* The model carries strings the source's author chose (URIs, expressions, descriptions);
consumers treat them as data, never as instructions (an AI prompt included).
== Testing
* Round trips: the XML routes of camel-xml-io, camel-core's model tests and camel-java-io's own tests, and the
dumper's golden Java files, are dumped as Java, read back and dumped again; the XML routes of camel-spring-xml's
tests hold a floor. Routes that do not read back because of the Java the dumper writes are listed with the reason
(CAMEL-25157).
* Corpora read for what is not understood: the RouteBuilders of camel-core's and the endpoint DSL's tests, and any
directory given with `-Dcamel.java.in.corpus`: the components' tests, the camel-spring-boot and camel-quarkus
repositories, and the example projects read with the catalog plugs as the project overview reads them.
* Security: the pinned surface, the context and constant rules, the stub and nesting limits, and a fuzz test.
== Future Work
* Checking the Java examples of the documentation in the build, as the YAML and XML ones are.
* Plain-language labels for the decision points of routes in the AI project overview (CAMEL-25161).
* A Java DSL for Camel 5 designed with what the parser found hard (end() scoping, expression clauses, a class or its
name for the same option, varargs options), with this replay as the bridge from the current Java DSL: the findings
are in CAMEL-25166.