This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 1e8f70977f83 CAMEL-25928: camel-tooling-model - one rule to read a
body type, and a Message Body Types page (#28437)
1e8f70977f83 is described below
commit 1e8f70977f8398b1ee0bdda3f22cf55477c46b34
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 12:49:54 2026 +0100
CAMEL-25928: camel-tooling-model - one rule to read a body type, and a
Message Body Types page (#28437)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../modules/ROOT/partials/body-types.adoc | 2 +-
.../ROOT/images/body-types-route-diagram.svg | 104 ++++++
.../jbang/camel-tui-source-body-type-problem.png | Bin 0 -> 255685 bytes
docs/user-manual/modules/ROOT/nav.adoc | 1 +
.../user-manual/modules/ROOT/pages/body-types.adoc | 409 +++++++++++++++++++++
.../modules/ROOT/pages/camel-catalog.adoc | 106 +-----
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 2 +-
.../modules/ROOT/pages/camel-jbang-validation.adoc | 2 +-
.../modules/ROOT/pages/endpoint-annotations.adoc | 4 +-
.../camel/dsl/yaml/validator/CatalogBodyTypes.java | 85 +----
.../camel/tooling/model/BodyTypeResolver.java | 177 +++++++++
.../camel/tooling/model/BodyTypeResolverTest.java | 160 ++++++++
.../camel/maven/packaging/BodyTypesHelper.java | 18 +-
13 files changed, 872 insertions(+), 198 deletions(-)
diff --git a/docs/components/modules/ROOT/partials/body-types.adoc
b/docs/components/modules/ROOT/partials/body-types.adoc
index 8df01700d940..a79f7abeeb2b 100644
--- a/docs/components/modules/ROOT/partials/body-types.adoc
+++ b/docs/components/modules/ROOT/partials/body-types.adoc
@@ -16,6 +16,6 @@ ifeval::[{bodytypecount} != 0]
jsonpathTable::{body-types-module}:example$json/{shortname}.json['nodes$.bodyTypes.*','camel.bodySide(path[2])|camel.bodyCarriers(value)|camel.bodyDescription(value)','camel=xref:ROOT:js/camel.js']
The bodies are the ones with the default options. A side that is not listed is
not known, and tooling does not guess
-it. The carriers are described in the xref:manual::camel-catalog.adoc[Camel
Catalog].
+it. The carriers, the sides and the tools that use them are described in
xref:manual::body-types.adoc[Message Body Types].
endif::[]
// body types: END
diff --git a/docs/user-manual/modules/ROOT/images/body-types-route-diagram.svg
b/docs/user-manual/modules/ROOT/images/body-types-route-diagram.svg
new file mode 100644
index 000000000000..d1e70fd74699
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/images/body-types-route-diagram.svg
@@ -0,0 +1,104 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="760" height="540" viewBox="0 0
760 540" font-family="'Red Hat Text', 'Helvetica Neue', Helvetica, Arial,
sans-serif">
+ <title>A route diagram with the body types on the arrows and a warning on
the step that cannot take the body</title>
+ <defs>
+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7"
markerHeight="7" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#8a8d90"/>
+ </marker>
+ <marker id="arrow-warn" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#c58c00"/>
+ </marker>
+ <filter id="shadow" x="-10%" y="-10%" width="120%" height="140%">
+ <feDropShadow dx="0" dy="1" stdDeviation="1.5" flood-color="#000"
flood-opacity="0.12"/>
+ </filter>
+ </defs>
+
+ <!-- canvas -->
+ <rect x="0" y="0" width="760" height="540" rx="8" fill="#f5f6f7"/>
+ <rect x="0" y="0" width="760" height="36" rx="8" fill="#ffffff"/>
+ <rect x="0" y="28" width="760" height="8" fill="#ffffff"/>
+ <line x1="0" y1="36" x2="760" y2="36" stroke="#d2d2d2"/>
+ <text x="16" y="23" font-size="13" font-weight="600"
fill="#151515">orders.camel.yaml</text>
+ <text x="160" y="23" font-size="13" fill="#6a6e73">route: orders</text>
+ <g transform="translate(626,10)">
+ <path d="M 8 1 L 15 14 L 1 14 Z" fill="#f0ab00" stroke="#c58c00"
stroke-width="1" stroke-linejoin="round"/>
+ <text x="8" y="12.5" font-size="9" font-weight="700" fill="#151515"
text-anchor="middle">!</text>
+ <text x="22" y="13" font-size="12" fill="#6a6e73">1 warning</text>
+ </g>
+
+ <!-- node 1: from file -->
+ <g filter="url(#shadow)">
+ <rect x="80" y="60" width="240" height="60" rx="8" fill="#ffffff"
stroke="#c7c7c7"/>
+ </g>
+ <circle cx="112" cy="90" r="18" fill="#0066cc"/>
+ <path d="M 105 80 L 115 80 L 120 85 L 120 100 L 105 100 Z M 115 80 L 115 85
L 120 85" fill="none" stroke="#ffffff" stroke-width="1.6"
stroke-linejoin="round"/>
+ <text x="142" y="86" font-size="14" font-weight="600"
fill="#151515">from</text>
+ <text x="142" y="105" font-size="12" fill="#6a6e73">file:inbox</text>
+
+ <!-- edge 1 -->
+ <line x1="200" y1="120" x2="200" y2="168" stroke="#8a8d90"
stroke-width="1.5" marker-end="url(#arrow)"/>
+ <rect x="212" y="133" width="118" height="22" rx="11" fill="#e7f1fa"
stroke="#73bcf7"/>
+ <text x="271" y="148" font-size="12" fill="#004080"
text-anchor="middle">file (GenericFile)</text>
+
+ <!-- node 2: unmarshal json -->
+ <g filter="url(#shadow)">
+ <rect x="80" y="170" width="240" height="60" rx="8" fill="#ffffff"
stroke="#c7c7c7"/>
+ </g>
+ <circle cx="112" cy="200" r="18" fill="#5e40be"/>
+ <text x="112" y="205.5" font-size="14" font-weight="700" fill="#ffffff"
text-anchor="middle">{ }</text>
+ <text x="142" y="196" font-size="14" font-weight="600"
fill="#151515">unmarshal</text>
+ <text x="142" y="215" font-size="12" fill="#6a6e73">json</text>
+
+ <!-- edge 2 -->
+ <line x1="200" y1="230" x2="200" y2="278" stroke="#8a8d90"
stroke-width="1.5" marker-end="url(#arrow)"/>
+ <rect x="212" y="243" width="104" height="22" rx="11" fill="#f3faf2"
stroke="#7cc674"/>
+ <text x="264" y="258" font-size="12" fill="#1e4f18" text-anchor="middle">Map
or List</text>
+
+ <!-- node 3: log -->
+ <g filter="url(#shadow)">
+ <rect x="80" y="280" width="240" height="60" rx="8" fill="#ffffff"
stroke="#c7c7c7"/>
+ </g>
+ <circle cx="112" cy="310" r="18" fill="#009596"/>
+ <path d="M 103 303 L 121 303 M 103 310 L 121 310 M 103 317 L 115 317"
stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
+ <text x="142" y="306" font-size="14" font-weight="600"
fill="#151515">log</text>
+ <text x="142" y="325" font-size="12" fill="#6a6e73">Order ${body[id]}
...</text>
+
+ <!-- edge 3: the body that does not fit -->
+ <line x1="200" y1="340" x2="200" y2="388" stroke="#c58c00" stroke-width="2"
stroke-dasharray="5 3" marker-end="url(#arrow-warn)"/>
+ <rect x="212" y="353" width="104" height="22" rx="11" fill="#fdf7e7"
stroke="#f0ab00"/>
+ <text x="264" y="368" font-size="12" fill="#795600" text-anchor="middle">Map
or List</text>
+
+ <!-- node 4: to file, with the warning -->
+ <g filter="url(#shadow)">
+ <rect x="80" y="390" width="240" height="60" rx="8" fill="#ffffff"
stroke="#f0ab00" stroke-width="2"/>
+ </g>
+ <circle cx="112" cy="420" r="18" fill="#0066cc"/>
+ <path d="M 103 420 L 120 420 M 114 414 L 120 420 L 114 426" fill="none"
stroke="#ffffff" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"/>
+ <text x="142" y="416" font-size="14" font-weight="600"
fill="#151515">to</text>
+ <text x="142" y="435" font-size="12" fill="#6a6e73">file:outbox</text>
+ <g transform="translate(298,376)">
+ <path d="M 14 1 L 27 24 L 1 24 Z" fill="#f0ab00" stroke="#c58c00"
stroke-width="1.2" stroke-linejoin="round"/>
+ <text x="14" y="21" font-size="15" font-weight="700" fill="#151515"
text-anchor="middle">!</text>
+ </g>
+
+ <!-- the callout -->
+ <path d="M 330 392 L 352 384 L 352 400 Z" fill="#fdf7e7" stroke="#f0ab00"/>
+ <g filter="url(#shadow)">
+ <rect x="351" y="300" width="390" height="176" rx="8" fill="#fdf7e7"
stroke="#f0ab00"/>
+ </g>
+ <rect x="352" y="385" width="3" height="14" fill="#fdf7e7"/>
+ <g transform="translate(368,314)">
+ <path d="M 9 1 L 17 15 L 1 15 Z" fill="#f0ab00" stroke="#c58c00"
stroke-width="1" stroke-linejoin="round"/>
+ <text x="9" y="13.5" font-size="10" font-weight="700" fill="#151515"
text-anchor="middle">!</text>
+ </g>
+ <text x="394" y="327" font-size="14" font-weight="600" fill="#151515">The
body here is parsed data</text>
+ <text x="368" y="352" font-size="12.5" fill="#151515">to: file needs the
body as text, bytes, a stream or a file,</text>
+ <text x="368" y="370" font-size="12.5" fill="#151515">but unmarshal: json
turned the text into a Map or a List,</text>
+ <text x="368" y="388" font-size="12.5" fill="#151515">which a file cannot be
written from.</text>
+ <rect x="368" y="404" width="200" height="28" rx="14" fill="#0066cc"/>
+ <text x="468" y="422.5" font-size="12.5" font-weight="600" fill="#ffffff"
text-anchor="middle">+ Add marshal: json before</text>
+ <text x="368" y="458" font-size="11.5" fill="#6a6e73">From the body types of
file, json and the log EIP in the catalog</text>
+
+ <!-- legend -->
+ <text x="80" y="500" font-size="12" fill="#6a6e73">The labels on the arrows
are the body each step leaves, before the route runs.</text>
+ <text x="80" y="518" font-size="12" fill="#6a6e73">An illustration of what a
visual route editor can show with the body types.</text>
+</svg>
diff --git
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-type-problem.png
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-type-problem.png
new file mode 100644
index 000000000000..747ff3423aaa
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-type-problem.png
differ
diff --git a/docs/user-manual/modules/ROOT/nav.adoc
b/docs/user-manual/modules/ROOT/nav.adoc
index 702ca4ebea49..8440caa8c701 100644
--- a/docs/user-manual/modules/ROOT/nav.adoc
+++ b/docs/user-manual/modules/ROOT/nav.adoc
@@ -113,6 +113,7 @@
** xref:jmx.adoc[JMX]
** xref:key-value-repository.adoc[KeyValueRepository]
** xref:lifecycle.adoc[Camel Lifecycle]
+** xref:body-types.adoc[Message Body Types]
** xref:message-size.adoc[Message Size]
** xref:oncompletion.adoc[OnCompletion]
** xref:pluggable-class-resolvers.adoc[Pluggable Class Resolvers]
diff --git a/docs/user-manual/modules/ROOT/pages/body-types.adoc
b/docs/user-manual/modules/ROOT/pages/body-types.adoc
new file mode 100644
index 000000000000..4e1b7ff4bf6b
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/body-types.adoc
@@ -0,0 +1,409 @@
+= Message Body Types
+
+*Available as of Camel 4.24*
+
+Every step of a route needs some kinds of message body and leaves one. The
`file` consumer gives the file it read,
+`unmarshal: json` turns text into a `Map` or a `List`, and the `file` producer
writes text, bytes, a stream or a file,
+but not a `Map`. When the body one step leaves is not a body the next step can
take, the route fails, but only when a
+message gets there.
+
+Camel describes these bodies in its catalog, as the _body types_ of each
component, data format, language and EIP.
+They say what each step needs and what it leaves, with its default options and
with the options that change it. Tools
+read them to tell before the route runs what the body is at each step, and
where it does not fit.
+
+The body types help you:
+
+* *Find a route that cannot work before it runs.* The YAML validator follows
the body through the steps of each route,
+ and reports a step that cannot take the body it gets, with a hint of what to
add.
+* *Know what a step gives without reading its code.* The documentation page of
each component, data format, language
+ and EIP has a _Message Body_ section, and the Camel CLI and its MCP server
show the same.
+* *Write routes with AI.* An AI assistant that reads the body types writes
steps that fit together, such as a
+ `marshal` before a step that sends text.
+* *See what the body was when the routes ran.* With the message history on,
Camel records the body each step has been
+ reached with. A step reached with bodies of more than one kind is a part of
the route that needs attention.
+
+== How it works, in a picture
+
+Each step has two body types: the bodies it needs, and the body it leaves. A
tool lines them up along a route, step by
+step, before the route runs:
+
+[source,text]
+----
+ from: file:inbox
+ │ file the file consumer gives the file it read
+ ▼
+ unmarshal: json needs text, bytes, a stream or a file ✓
+ │ Map or List the parsed JSON
+ ▼
+ log needs any body ✓
+ │ Map or List log leaves the body as it was
+ ▼
+ to: file:outbox needs text, bytes, a stream or a file ✗
+ ⚠ the body is a Map or a List: add marshal: json
before this step
+----
+
+The same body types tell what the body is where a route is not wrong: after
`marshal: json` the body is JSON text
+again, which the `file` producer writes.
+
+== Carriers: the kinds of body
+
+A body type names the _carriers_ of the body: the kinds of Java object it can
be in. They are coarse on purpose: a
+carrier that is right is better than a precise Java type that is wrong.
+
+[width="100%",cols="1,3,1",options="header"]
+|===
+| Carrier | The body is | Kind at runtime
+| `null` | No body. | `null`
+| `text` | A `String`. | `payload`
+| `bytes` | A `byte[]`. | `payload`
+| `stream` | An `InputStream`, a `Reader`, or the stream cache of Camel. |
`payload`
+| `file` | A file: the `GenericFile` of the file consumers, a `java.io.File`
or a `Path`. | `payload`
+| `value` | A single value: a number, a boolean or an enum. | `payload`
+| `map` | A `java.util.Map`, such as a JSON object parsed, or a row of a
query. | `data`
+| `pojo` | Another Java object, such as a DOM `Document` or a class a payload
is unmarshalled into. | `data`
+| `list` | A `List`, another `Collection` or an array, such as a JSON array
parsed, or the rows of a query. | `collection`
+| `iterator` | An `Iterator`, read once, element by element, with no size and
no index. | `collection`
+|===
+
+Three more words have a meaning of their own:
+
+* `unchanged` -- the body after the step is the body before it, such as after
a `log` or a `to: kafka`.
+* `any` -- the step takes any body, such as a `setHeader`. Only for the bodies
a step needs.
+* `unknown` -- the body depends on what the other system sends, or on a class
the user configures. Tools must not guess
+ it. Only for the body after a step.
+
+The _kinds_ are what Camel records at runtime (see
xref:components:eips:message-history.adoc[Message History]): a
+step reached with bodies of one kind, such as `String` and `byte[]`, is
dynamic but fine; a step reached with a payload
+for some messages and data for others needs attention.
+
+== Sides: what a step needs and what it leaves
+
+The body types of a model are a set of _sides_. A side that is not listed is
not known.
+
+[width="100%",cols="1,1,3",options="header"]
+|===
+| Model | Side | The body
+.4+| Component | `consumerOut` | The body of the message the consumer creates:
what a `from` gives the route.
+| `consumerReply` | The bodies a consumer that answers the caller can reply
with: what the route can end with.
+| `producerIn` | The bodies the producer can send or write: what a `to` needs.
+| `producerOut` | The body after the producer.
+.4+| Data format | `marshalIn` | The bodies the data format can marshal.
+| `marshalOut` | The body after `marshal`.
+| `unmarshalIn` | The bodies the data format can unmarshal.
+| `unmarshalOut` | The body after `unmarshal`.
+.2+| Language | `expressionIn` | The bodies the expressions of the language
can read.
+| `expressionOut` | The result of an expression.
+.3+| EIP | `eipIn` | The bodies the EIP needs, such as text for `tokenizer`.
+| `eipOut` | The body after the EIP.
+| `eipSteps` | The body the steps of the EIP start with, such as each part for
`split`.
+|===
+
+== Examples
+
+=== A step that cannot take the body
+
+This route reads JSON files, parses them, and writes them to another directory:
+
+// yaml-validator: skip
+[source,yaml]
+----
+- route:
+ from:
+ uri: file:inbox
+ steps:
+ - unmarshal:
+ json: {}
+ - to:
+ uri: file:outbox
+----
+
+The `file` consumer gives the file (`consumerOut` is `file`), `unmarshal:
json` leaves a `Map` or a `List`
+(`unmarshalOut`), and the `file` producer needs text, bytes, a stream or a
file (`producerIn`). So the validator
+reports the `to`:
+
+[source,text]
+----
+to: file needs the body as text, bytes, a stream or a file, but the body here
is parsed data (a Map or a List), not
+text - unmarshal: json turns the text into data: add marshal: json before this
step
+----
+
+The source editor of the Camel CLI checks a file when you save it, and does
not save a YAML file with a problem. The
+`to` line is marked, and the Error panel says what is wrong, with *Shift+F8*
to fix it with AI (the notes on the right
+were added for this page):
+
+image::jbang/camel-tui-source-body-type-problem.png[The orders route in the
source editor of the Camel CLI: the to line is marked, and the Error panel says
that the file producer needs text, bytes, a stream or a file, but the body is
parsed data after unmarshal json]
+
+With a `marshal` before it, the body is JSON text again, which the `file`
producer writes:
+
+[source,yaml]
+----
+- route:
+ from:
+ uri: file:inbox
+ steps:
+ - unmarshal:
+ json: {}
+ - marshal:
+ json: {}
+ - to:
+ uri: file:outbox
+----
+
+=== Options change the body
+
+The body after a step can depend on its options. The `xslt` producer leaves
the result as text, unless its `output`
+option says otherwise:
+
+[source,json]
+----
+"producerOut": {
+ "carriers": [ "text" ], "javaType": "java.lang.String", "dependsOn": [
"resultHandlerFactory" ],
+ "byOption": [
+ { "option": "output", "value": "bytes", "carriers": [ "bytes" ],
"javaType": "byte[]" },
+ { "option": "output", "value": "DOM", "carriers": [ "pojo" ], "javaType":
"org.w3c.dom.Document" },
+ { "option": "output", "value": "file", "carriers": [ "file" ], "javaType":
"java.io.File" } ],
+ "description": "The result of the transformation" }
+----
+
+With `output=bytes` the body is a `byte[]`. With a `resultHandlerFactory` the
factory decides the body, which the
+catalog cannot know, so it is not known.
+
+=== An option that names the class of the body
+
+Some options name the class the result is converted to: the `resultType` of a
language, the `unmarshalType` of a JSON
+data format, the `type` of `convertBodyTo`, or the `outputClass` of `sql` for
one row. When such an option is set, the
+body is an object of that class, or null:
+
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:order
+ steps:
+ - setBody:
+ simple:
+ expression: "${header.quantity}"
+ resultType: java.lang.Integer
+ - to:
+ uri: sql:select * from orders where id = :#id
+ parameters:
+ outputType: SelectOne
+ outputClass: com.acme.Order
+----
+
+`setBody` leaves the result of its expression, and with `resultType` that is
an `Integer`, a `value`. The `sql`
+producer leaves a `List` of rows by default, and with `outputType=SelectOne`
and `outputClass` one `com.acme.Order`,
+or null when there is no row.
+
+=== EIPs
+
+The body after an EIP is often the body of something else: `marshal` leaves
what its data format writes, `to` what
+the producer of its endpoint leaves, and `setBody` the result of its
expression. The catalog says so with `from`,
+instead of copying the body types of every data format, component and language:
+
+[source,json]
+----
+"eipOut": { "from": "marshalOut", "dependsOn": [ "variableReceive" ],
+ "description": "What the data format writes. With variableReceive the result
goes into the variable, and the body stays as it was" }
+----
+
+An EIP with steps of its own also says what they start with. The steps of a
`split` start with each part, and after
+it the body is as it was before the split:
+
+[source,json]
+----
+"eipOut": { "carriers": [ "unchanged" ], "dependsOn": [ "aggregationStrategy"
],
+ "description": "The body as it was before the split, unless an
aggregationStrategy is set" },
+"eipSteps": { "carriers": [ "unknown" ],
+ "description": "Each part of what the expression gives, such as an element
of a List or a line of text" }
+----
+
+== Tooling
+
+=== Documentation
+
+The page of each component, data format, language and EIP has a _Message Body_
section with a row per side, which
+is generated from the catalog.
+
+=== Camel CLI and its MCP server
+
+`camel catalog doc` prints the same table for a component, data format,
language or EIP, such as
+`camel catalog doc xslt` or `camel catalog doc eip:split`. The
`camel_catalog_doc` tool of the
+xref:camel-jbang-mcp.adoc[MCP server] gives each side as a line of text, so an
AI assistant reads them with the
+options:
+
+[source,text]
+----
+text (java.lang.String). The result of the transformation. With output=bytes:
bytes (byte[]); output=DOM: pojo
+(org.w3c.dom.Document); output=file: file (java.io.File). Not known when
resultHandlerFactory is set
+----
+
+=== YAML validator
+
+`camel validate source` follows the body through the steps of each route with
the body types, and reports a step
+that cannot take the body it gets (see
xref:camel-jbang-validation.adoc[Validation]). Where the catalog does not
+know the body, such as after an HTTP consumer, it says nothing, unless a
running integration has shown what the body
+is.
+
+=== Asking an AI assistant
+
+An AI assistant with the xref:camel-jbang-mcp.adoc[MCP server] of the Camel
CLI can answer "is something wrong with
+my route?". It validates the file with the `camel_validate_source` tool, which
follows the body with the body types:
+
+[source,text]
+----
+You: Is something wrong with my route in orders.camel.yaml? The files
never show up in outbox.
+
+Assistant: (calls camel_validate_source on orders.camel.yaml)
+ {"valid": false, "errors": ["Line 8: to: to: file needs the body
as text, bytes, a stream or a file,
+ but the body here is parsed data (a Map or a List), not text -
unmarshal: json turns the text into
+ data: add marshal: json before this step"]}
+
+ Yes. unmarshal: json turns each file into a Map (or a List for a
JSON array), and the file producer
+ cannot write a Map, so the exchange fails at the to step and
nothing reaches outbox. Add
+ marshal: json before the to to write the JSON again. If the route
only moves the files, remove the
+ unmarshal instead, as the file consumer gives the file as it is.
+----
+
+The assistant can also read the body types of a step with `camel_catalog_doc`,
such as what `unmarshal: json` leaves
+with `useList`, and it validates a route before it writes the file. In the
source editor of the Camel CLI, *Shift+F8*
+asks the AI of the Camel CLI to fix the problem of a line.
+
+=== In a visual route editor
+
+As the body types are in the catalog, a visual route editor can show them too:
the body on each arrow, and a warning
+on the step that cannot take it, with the step to add. An illustration of the
route above:
+
+image::body-types-route-diagram.svg[An illustration of a route diagram: from
file, unmarshal json, log and to file, with the body on each arrow, and a
yellow warning triangle on the to step, whose callout says that the body is
parsed data and offers to add marshal json before it]
+
+=== At runtime
+
+With the message history on, such as with `camel run --dev`, Camel records the
types and kinds of the bodies each
+step has been reached with:
+
+* xref:components:eips:message-history.adoc[Message History] explains the
kinds, the `BodyTypeRegistry`, the
+ `body-type` dev console and the JMX attributes.
+* `camel cmd route-structure` shows them per step (see
xref:camel-jbang-diagnostics.adoc[Diagnostics]), as do the
+ xref:camel-jbang-tui-diagram.adoc#_body_types[diagram] and the
+ xref:camel-jbang-tui-source-editor.adoc#_body_types[source editor] of the
Camel CLI.
+* The route coverage report of the xref:camel-report-maven-plugin.adoc[Camel
Report Maven Plugin] shows the body types
+ the tests reached each step with, and can fail the build on a step reached
with bodies of more than one kind.
+
+For example, the route coverage report of tests that sent two messages, where
only one was parsed before the `to`:
+
+[source,bash]
+----
+mvn camel-report:route-coverage -Dcamel.bodyTypes=true
+----
+
+[source,text]
+----
+ Line # Count Route Body types
+ ------ ----- ----- ----------
+ 3 2 from
+ 4 2 unmarshal String
+ 7 2 to LinkedHashMap | String (data and
payload)
+----
+
+The `to` has been reached with a `LinkedHashMap` (data) by one message and a
`String` (a payload) by the other, so it
+works for some messages only. The body types in the catalog tell this before
the route runs; the ones recorded at
+runtime show what the messages really brought.
+
+== Declaring body types
+
+A component declares its body types with the `@BodyTypes` annotation on its
endpoint class, a data format and a
+language on their class, and an EIP on its model class in `camel-core-model`.
That is also where the EIPs that other
+modules implement are declared, such as `circuitBreaker`. A subclass does not
have the body types of its superclass,
+as it may change the body:
+
+[source,java]
+----
+@BodyTypes(consumerOut = @BodyType(value = Carrier.FILE, description = "The
file as read ..."),
+ producerIn = @BodyType({ Carrier.TEXT, Carrier.BYTES,
Carrier.STREAM, Carrier.FILE }),
+ producerOut = @BodyType(Carrier.UNCHANGED))
+@UriEndpoint(firstVersion = "1.0.0", scheme = "file", ...)
+public class FileEndpoint extends GenericFileEndpoint<File> {
+----
+
+The values come from the code that sets the body. The build checks that:
+
+* each side belongs to the kind of model; a consumer-only component has no
producer sides;
+* the options a side names exist, and a value of an option is one the option
can have;
+* `unchanged` and `unknown` are only on the body after a step, and `any` only
on what a step needs;
+* `from` names a side the body can come from, and a side with `from` has no
carriers of its own;
+* a `javaTypeOption` is an option of the body after a step that the side does
not also depend on.
+
+The runtime body types the message history records are checked against them in
tests, such as for the EIPs of core.
+
+== Reference: the JSON of the body types
+
+This section is for the tools, and the AI assistants, that read the catalog.
+
+The JSON of a component, data format, language or EIP in the catalog has a
`bodyTypes` object next to its options, with
+an object per side. Each side has these fields; a field that does not apply is
left out:
+
+[width="100%",cols="1,1,4",options="header"]
+|===
+| Field | Type | Meaning
+| `carriers` | array of strings | The carriers of the body: one, or several
when the body can be any of them. Left out
+for a side with `from`.
+| `javaType` | string | The Java type of the body, when it is always of this
type.
+| `format` | string | The format of the payload, when it is always the same:
`json`, `xml`, `csv`, `yaml`, `text` or
+`binary`.
+| `dependsOn` | array of strings | The options the body depends on. The side
holds while they keep their default values.
+When the step sets one, only a `byOption` entry for that value says the body;
without one it is not known.
+| `byOption` | array of objects | The body for a value of an option: `option`,
`value`, `carriers`, and optionally
+`javaType` and `javaTypeOption`.
+| `javaTypeOption` | string | The option that names the class of the body,
such as `resultType`. When the step sets
+it, the body is an object of that class, or null.
+| `from` | string | For an EIP, the side of another model the body is:
`expressionIn`, `expressionOut` (the language
+of its expression), `marshalIn`, `marshalOut`, `unmarshalIn`, `unmarshalOut`
(its data format), `producerIn`,
+`producerOut` (its endpoint as a producer), `consumerOut` (its endpoint as a
consumer), or `steps` (what its own
+steps end with).
+| `description` | string | What to know about the body, in a sentence.
+|===
+
+=== Reading a side for a step
+
+To know the body a step leaves, or the bodies it needs, read its side with the
options the step sets, in this order:
+
+. *No side:* not known.
+. *A side with `from`:* when the step sets an option of `dependsOn`, not
known. Otherwise, read the side `from` names on
+the model the step uses (the language of its expression, its data format or
its endpoint), with the options of that
+model. `steps` is the body the steps of the EIP end with.
+. *An option the side names whose value is not known before the route runs* (a
`+{{placeholder}}+` or a `+${expression}+`),
+or an option the step does not set but that can be set elsewhere, such as on
the component: not known.
+. *An option of `dependsOn` the step sets:* the `byOption` entry for that
value says the body. When it has a
+`javaTypeOption` the step sets, the body is an object of that class. Without
such an entry: not known.
+. *The `javaTypeOption` of the side, when the step sets it:* an object of the
class it names, or null. It wins over the
+`byOption` entries of other options.
+. *The `byOption` entries for the values the step sets:* their carriers (or
the class of their `javaTypeOption`). When
+two of them say different bodies: not known.
+. *Otherwise:* the carriers of the side. `unknown` is not known, and
`unchanged` is the body before the step.
+
+The values of an option compare in any case, and in any style of an enum
constant, as Camel takes them:
+`convert-to-json`, `convertToJson` and `CONVERT_TO_JSON` are one value.
+
+The carrier of a class that a `javaTypeOption` names follows the class:
`String` is `text`, `byte[]` is `bytes`, an
+`InputStream` or a `Reader` is `stream`, a `File` or a `Path` is `file`, a
number, a boolean or a character is
+`value`, a `Map` is `map`, a `Collection` or an array is `list`, an `Iterator`
is `iterator`, `Object` is `unknown`, and
+any other class is `pojo`. The short names Camel takes, such as `String` and
`int`, count too.
+
+=== Java API
+
+The models of `camel-catalog` have the body types, and `BodyTypeResolver` of
`camel-tooling-model` reads a side for the
+options of a step, by the rule above. The YAML validator uses it:
+
+[source,java]
+----
+CamelCatalog catalog = new DefaultCamelCatalog();
+
+BodyTypeModel out =
catalog.componentModel("xslt").getBodyType(BodyTypeModel.PRODUCER_OUT);
+out.getCarriers(); // [text]
+out.getJavaType(); // java.lang.String
+
+BodyTypeResolver.Resolved bytes = BodyTypeResolver.resolve(out,
Map.of("output", "bytes"), Set.of());
+bytes.carriers(); // [bytes]
+
+BodyTypeModel.carrierOf("java.util.Map"); // map
+----
diff --git a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
index 2e7cc5513d6b..864526f0844e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
@@ -129,86 +129,20 @@ question people, IDEs and AI agents ask most when they
write the next step of a
file it read, the `http` producer leaves the response as a byte array or a
stream, and after `unmarshal: json` the body
is a `Map` or a `List`.
-A component, data format, language or EIP can declare its body types, and the
catalog then has a `bodyTypes` section
-with one entry per side:
-
-[width="100%",cols="25%,75%",options="header",]
-|===
-|Side |The body
-|`consumerOut` |The body of the message the consumer creates.
-|`consumerReply` |The bodies a consumer that answers the caller can reply
with: the body the route ends with.
-|`producerIn` |The bodies the producer can send or write.
-|`producerOut` |The body after the producer.
-|`marshalIn`, `marshalOut` |The bodies a data format can marshal, and the body
after marshal.
-|`unmarshalIn`, `unmarshalOut` |The bodies a data format can unmarshal, and
the body after unmarshal.
-|`expressionIn`, `expressionOut` |The bodies the expressions of a language can
read, and their result.
-|`eipIn` |The bodies an EIP needs, such as text for `tokenizer`.
-|`eipOut` |The body after an EIP.
-|`eipSteps` |The body the steps of an EIP start with, such as each part for
`split`.
-|===
-
-Each side has the _carriers_ of the body, the kinds of Java object it is in:
`null`, `text`, `bytes`, `stream`,
-`file`, `value`, `map`, `list`, `iterator` (read once, with no size and no
index) and `pojo`; `unchanged` for a step
-that leaves the body as it was, `any` for a step that takes any body, and
`unknown` for a body that depends on what
-the other system sends or on a class the user configures. A side can also have
the Java type when the body is always of that type, the format of the payload
(json,
-xml, csv, yaml, text or binary), the options the body depends on, the body for
other values of an option, and a
-description:
+A component, data format, language or EIP that declares its body types has a
`bodyTypes` section in the catalog, with
+an entry per side: what a consumer gives, what a producer needs and leaves,
what a data format marshals and unmarshals
+to, what an expression reads and gives, and what an EIP needs, leaves and
starts its own steps with:
[source,json]
----
"bodyTypes": {
- "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "pojo" ],
"format": "xml", "dependsOn": [ "source" ],
- "description": "The XML to transform: text, bytes, a stream, a file, a DOM
or another javax.xml.transform.Source. ..." },
- "producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String",
"dependsOn": [ "resultHandlerFactory" ],
- "byOption": [ { "option": "output", "value": "bytes", "carriers": [
"bytes" ], "javaType": "byte[]" }, ... ],
- "description": "The result of the transformation" }
+ "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ],
+ "description": "What converts to an InputStream: text, bytes, a stream or
a file. A Map or a List fails: marshal it first" },
+ "producerOut": { "carriers": [ "unchanged" ] }
}
----
-An EIP whose body is the body of another model says which side of it with
`from`, instead of carriers of its own:
-`marshal` leaves what its data format leaves (`marshalOut`), `to` what the
producer of its endpoint leaves
-(`producerOut`), `pollEnrich` what the consumer of its endpoint receives
(`consumerOut`), `setBody` the result of its
-expression (`expressionOut`), and a `filter` what its own steps end with
(`steps`):
-
-[source,json]
-----
-"bodyTypes": {
- "eipOut": { "from": "steps", "description": "What its steps end with; the
body as it was when the predicate does not match" },
- "eipSteps": { "carriers": [ "unchanged" ] }
-}
-----
-
-An option that names the class of the body is the `javaTypeOption` of a side,
such as the `resultType` of a language,
-whose result is converted to that class, the `unmarshalType` of a JSON data
format, and the `type` of `convertBodyTo`.
-When the option is set, the body is an object of the class it names, or null
when it does not convert. That holds while
-the options the side depends on keep their defaults (an option that makes the
body a collection of that class, such
-as `useList` of `jackson`, is one of them), and it wins over the bodies
`byOption` gives for other options. The
-carrier follows the class: `String` is `text`, `byte[]` is `bytes`, an
`InputStream` or a `Reader` is a `stream`, a
-`File` or a `Path` is a `file`, a number, a boolean or a character is a
`value`, a `Map` is a `map`, a `Collection` or
-an array is a `list`, an `Iterator` is an `iterator`, `Object` is `unknown`,
and any other class is a `pojo`.
-`BodyTypeModel.carrierOf` gives it for a class name, also for the short names
Camel takes, such as `String` and `int`:
-
-[source,json]
-----
-"bodyTypes": {
- "expressionOut": { "carriers": [ "unknown" ], "javaTypeOption": "resultType",
- "description": "A text with functions gives text. A single function, such
as ${body} or ${header.foo}, gives its value as it is" }
-}
-----
-
-A body for an option value can have a `javaTypeOption` too, when the option
names the class of the body only for that
-value: the `outputClass` of `sql` is the class of the one row with
`outputType=SelectOne`, and of each row in the List
-otherwise:
-
-[source,json]
-----
-"producerOut": { "carriers": [ "list", "unchanged" ], "dependsOn": [
"outputHeader", "noop" ],
- "byOption": [ { "option": "outputType", "value": "SelectOne", "carriers": [
"map", "value", "null" ], "javaTypeOption": "outputClass" }, ... ] }
-----
-
-The sides say what the body is with the default options. A side that is not in
the catalog is not known, and tooling
-should not guess it. The documentation page of a component, data format,
language or EIP shows them in its _Message
-Body_ section, and `camel catalog doc` prints them. The Java API gives them on
the model:
+The Java API gives them on the model:
[source,java]
----
@@ -219,30 +153,8 @@ out.getCarriers(); // [text]
out.getJavaType(); // java.lang.String
----
-Component developers declare them with the `@BodyTypes` annotation on the
endpoint, data format or language class, and
-the EIPs on their model class in `camel-core-model`, also when the EIP is
implemented in another module, such as
-`circuitBreaker`. A subclass does not have the body types of its superclass,
as it may change the body:
-
-[source,java]
-----
-@BodyTypes(consumerOut = @BodyType(value = Carrier.FILE, description = "The
file as read ..."),
- producerIn = @BodyType({ Carrier.TEXT, Carrier.BYTES,
Carrier.STREAM, Carrier.FILE }),
- producerOut = @BodyType(Carrier.UNCHANGED))
-@UriEndpoint(firstVersion = "1.0.0", scheme = "file", ...)
-public class FileEndpoint extends GenericFileEndpoint<File> {
-----
-
-The build checks that each side belongs to the kind (a consumer-only component
has no producer sides), that the
-options a side names exist, that a value of an option is one the option can
have (such as `chat-completion` for the
-`operation` of openai), that `unchanged` is only on the body after a step and
`any` only on what a step needs,
-that `from` names a side the body can come from, and that a `javaTypeOption`
is an option of the body after a step
-that the side does not also depend on.
-The values are read from the code that sets the body, and checked against the
body types the message history records
-when the routes run.
-
-The YAML validator of the Camel CLI follows the body through the steps of a
route with the body types, and reports a
-step that cannot work on it, such as a Map that a `to: file` cannot write (see
-xref:camel-jbang-validation.adoc#_what_a_running_integration_saw[Camel CLI
validation]).
+See xref:body-types.adoc[Message Body Types] for what the body types are, the
tools that use them, how components
+declare them, and the JSON of each side with the rule to read it for the
options of a step.
=== Documentation in the Catalog
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 305f64e8ccd0..15dae476ac2a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -318,7 +318,7 @@ project `directory` as an argument, the runtime tools take
the integration `name
its group of `application.properties` keys, the key first. A component, data
format or language that
declares its body types has them in `bodyTypes`, a line per side: what a
`from:` gives the route
(`consumerOut`), what a `to:` can send (`producerIn`) and the body after it
(`producerOut`), and so on (see
- xref:camel-catalog.adoc#_body_types_in_the_catalog[Body types in the
Catalog]). An answer is cut at 18 000
+ xref:body-types.adoc[Message Body Types]). An answer is cut at 18 000
characters, with a marker that says what was left out.
`includeOptions` picks the options listed: `common` (the default) leaves out
the deprecated and advanced
ones and says how many with `omittedOptions`, `required` lists the required
ones, `all` everything, `false`
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
index 54c39089b805..709da62cf506 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
@@ -76,7 +76,7 @@ A part of a Java route the parser cannot work out (a value
computed at runtime)
The YAML validator follows the message body through the steps of each route,
and reports a step that cannot work on
it: a field read such as `+${body.qty}+` on text that was never unmarshalled,
or a Map that a `to: file` cannot write.
What a component or a data format does with the body comes from the
-xref:camel-catalog.adoc#_body_types_in_the_catalog[body types in the catalog]:
the file consumer gives the file it
+xref:body-types.adoc[body types in the catalog]: the file consumer gives the
file it
read, the `http` producer leaves the response as bytes or a stream, and
`unmarshal: json` makes a Map or a List. The
answer of `openai:chat-completion` is text, also when it is asked for JSON
with `jsonSchema`, so a field read on it
needs `unmarshal: json` first. Where they do not tell what the body is, the
validator says nothing: after a `platform-http` consumer the body
diff --git a/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc
b/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc
index aa9b755a18b8..6bee27e2e912 100644
--- a/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc
+++ b/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc
@@ -33,8 +33,8 @@ annotated with one or more `@UriParam` or `@UriParams`
annotations
|`@Metadata` | Used for special situations to provide additional information.
|`@BodyTypes` | The message body the endpoint works with: the body the
consumer creates, the bodies the
-producer can send, and the body after the producer. See
xref:camel-catalog.adoc#_body_types_in_the_catalog[Body types
-in the Catalog].
+producer can send, and the body after the producer. See
xref:body-types.adoc[Message
+Body Types].
|=======================================================================
For example see the `TimerEndpoint` from the `camel-timer` component and
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
index a2ff7f8b230c..6cd0a7753bf4 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
@@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -31,11 +30,10 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.tooling.model.BaseOptionModel;
import org.apache.camel.tooling.model.BodyTypeModel;
-import org.apache.camel.tooling.model.BodyTypeModel.OptionBodyTypeModel;
+import org.apache.camel.tooling.model.BodyTypeResolver;
import org.apache.camel.tooling.model.ComponentModel;
import org.apache.camel.tooling.model.DataFormatModel;
import org.apache.camel.tooling.model.EipModel;
-import org.apache.camel.util.StringHelper;
import static org.apache.camel.dsl.yaml.validator.RouteGraph.scheme;
@@ -234,85 +232,12 @@ final class CatalogBodyTypes {
}
/**
- * What a side says for the options a step sets: the body for an option
value it names, or its body when the options
- * it depends on keep their defaults; null when it is not known for the
step. An option the side depends on decides
- * first; then the option that names the class of the body (CAMEL-25928),
and then the bodies for the values of the
- * other options.
+ * What a side says for the options a step sets, by the rule the tools
share (BodyTypeResolver of the tooling
+ * model); null when it is not known for the step.
*/
private static Answer answer(BodyTypeModel side, Map<String, String> set,
Set<String> configurable) {
- if (side == null) {
- return null;
- }
- Set<String> options = new LinkedHashSet<>(side.getDependsOn());
- side.getByOption().forEach(o -> options.add(o.getOption()));
- if (side.getJavaTypeOption() != null) {
- options.add(side.getJavaTypeOption());
- }
-
side.getByOption().stream().map(OptionBodyTypeModel::getJavaTypeOption).filter(o
-> o != null)
- .forEach(options::add);
- for (String option : options) {
- String value = set.get(option);
- if (value == null ? configurable.contains(option) :
value.contains("{{") || value.contains("${")) {
- // the component may set it, or its value is only known when
the route runs
- return null;
- }
- }
- for (String option : side.getDependsOn()) {
- String value = set.get(option);
- if (value != null && side.getByOption().stream()
- .noneMatch(w -> w.getOption().equals(option) &&
isSameValue(value, w.getValue()))) {
- // the body depends on this option, and no option value of the
side says the body for its value: the
- // value another option names does not hold then, such as
output=bytes with a resultHandlerFactory
- return null;
- }
- }
- boolean dependsOnSet =
side.getDependsOn().stream().anyMatch(set::containsKey);
- if (!dependsOnSet && side.getJavaTypeOption() != null &&
set.containsKey(side.getJavaTypeOption())) {
- // the option names the class of the body, whatever the values of
the other options say
- return ofClass(set.get(side.getJavaTypeOption()));
- }
- Answer matched = null;
- for (OptionBodyTypeModel when : side.getByOption()) {
- String value = set.get(when.getOption());
- if (value != null && isSameValue(value, when.getValue())) {
- Answer answer = when.getJavaTypeOption() != null &&
set.containsKey(when.getJavaTypeOption())
- ? ofClass(set.get(when.getJavaTypeOption()))
- : new Answer(when.getCarriers(), when.getJavaType(),
side.getFormat());
- if (answer == null || matched != null &&
!matched.carriers().equals(answer.carriers())) {
- // two options the step sets say different bodies
- return null;
- }
- matched = answer;
- }
- }
- if (matched != null) {
- return matched;
- }
- if (side.isUnknown() ||
side.getDependsOn().stream().anyMatch(set::containsKey)) {
- return null;
- }
- return new Answer(side.getCarriers(), side.getJavaType(),
side.getFormat());
- }
-
- /**
- * The body of the class an option names, such as unmarshalType: an object
of it, or null when it does not convert.
- * Object says nothing.
- */
- private static Answer ofClass(String javaType) {
- String carrier = BodyTypeModel.carrierOf(javaType);
- if ("unknown".equals(carrier)) {
- return null;
- }
- return new Answer(List.of(carrier, "null"), javaType.trim(), null);
- }
-
- /**
- * Whether the value a step sets is the value of an option body: in any
case, and in any style of an enum constant,
- * as the catalog and the enum type converter take it, so
operation=convert-to-json is CONVERT_TO_JSON.
- */
- private static boolean isSameValue(String value, String other) {
- return value.equalsIgnoreCase(other)
- ||
StringHelper.asEnumConstantValue(value).equals(StringHelper.asEnumConstantValue(other));
+ BodyTypeResolver.Resolved resolved = BodyTypeResolver.resolve(side,
set, configurable);
+ return resolved != null ? new Answer(resolved.carriers(),
resolved.javaType(), resolved.format()) : null;
}
/** The options of a node, by name in camel case: allowNullBody as well
for allow-null-body. */
diff --git
a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/BodyTypeResolver.java
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/BodyTypeResolver.java
new file mode 100644
index 000000000000..838e12c93206
--- /dev/null
+++
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/BodyTypeResolver.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.tooling.model;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.camel.tooling.model.BodyTypeModel.OptionBodyTypeModel;
+
+/**
+ * What one side of the body types of the catalog says for the options a step
sets (CAMEL-24961, CAMEL-25928): the rule
+ * that the YAML validator and the other tools share, so they all read the
catalog the same way.
+ * <p/>
+ * The rule, in order:
+ * <ol>
+ * <li>An option the side names (in dependsOn, byOption or javaTypeOption)
whose value is only known when the route
+ * runs, a {@code {{placeholder}}} or a {@code ${expression}}, or that the
step does not set but can be set elsewhere,
+ * such as on the component: not known.</li>
+ * <li>An option of dependsOn the step sets: the body of byOption for that
value decides, and without one it is not
+ * known.</li>
+ * <li>Otherwise, the javaTypeOption the step sets: an object of the class it
names, or null.</li>
+ * <li>Otherwise, the bodies of byOption for the values the step sets; when
two say different bodies, not known.</li>
+ * <li>Otherwise, the carriers of the side; unknown is not known.</li>
+ * </ol>
+ * A body of byOption that has a javaTypeOption the step sets is an object of
that class, or null. The values of an
+ * option compare in any case, and in any style of an enum constant, as Camel
takes them: convert-to-json is
+ * CONVERT_TO_JSON. A side with from is the body of a side of another model,
which the caller resolves.
+ */
+public final class BodyTypeResolver {
+
+ private BodyTypeResolver() {
+ }
+
+ /**
+ * What a side says for a step: the carriers of the body, and its Java
type and format when it has them.
+ */
+ public record Resolved(List<String> carriers, String javaType, String
format) {
+
+ public Resolved {
+ // a copy, not the list of the model the caller could change
+ carriers = List.copyOf(carriers);
+ }
+ }
+
+ /**
+ * What a side says for the options a step sets.
+ *
+ * @param side the side, null when the catalog has none
+ * @param options the options the step sets, by name: the endpoint uri,
the parameters or the options of a data
+ * format
+ * @param hidden the options that can be set elsewhere when the step
does not set them, such as the options of the
+ * component
+ * @return the body, or null when it is not known for the step
+ */
+ public static Resolved resolve(BodyTypeModel side, Map<String, String>
options, Set<String> hidden) {
+ if (side == null || side.getFrom() != null) {
+ return null;
+ }
+ Set<String> names = new LinkedHashSet<>(side.getDependsOn());
+ side.getByOption().forEach(o -> names.add(o.getOption()));
+ if (side.getJavaTypeOption() != null) {
+ names.add(side.getJavaTypeOption());
+ }
+
side.getByOption().stream().map(OptionBodyTypeModel::getJavaTypeOption).filter(Objects::nonNull)
+ .forEach(names::add);
+ for (String name : names) {
+ String value = options.get(name);
+ if (value == null ? hidden.contains(name) : value.contains("{{")
|| value.contains("${")) {
+ // it may be set elsewhere, or its value is only known when
the route runs
+ return null;
+ }
+ }
+ boolean dependsOnSet = false;
+ for (String name : side.getDependsOn()) {
+ String value = options.get(name);
+ if (value != null) {
+ dependsOnSet = true;
+ if (side.getByOption().stream()
+ .noneMatch(w -> w.getOption().equals(name) &&
isSameValue(value, w.getValue()))) {
+ // the body depends on this option, and no option value of
the side says the body for its value
+ return null;
+ }
+ }
+ }
+ if (!dependsOnSet && side.getJavaTypeOption() != null &&
options.containsKey(side.getJavaTypeOption())) {
+ return ofClass(options.get(side.getJavaTypeOption()));
+ }
+ Resolved matched = null;
+ for (OptionBodyTypeModel when : side.getByOption()) {
+ String value = options.get(when.getOption());
+ if (value != null && isSameValue(value, when.getValue())) {
+ Resolved resolved = when.getJavaTypeOption() != null &&
options.containsKey(when.getJavaTypeOption())
+ ? ofClass(options.get(when.getJavaTypeOption()))
+ : new Resolved(when.getCarriers(), when.getJavaType(),
side.getFormat());
+ if (resolved == null || matched != null &&
!matched.carriers().equals(resolved.carriers())) {
+ // two options the step sets say different bodies
+ return null;
+ }
+ matched = resolved;
+ }
+ }
+ if (matched != null) {
+ return matched;
+ }
+ if (dependsOnSet || side.isUnknown()) {
+ return null;
+ }
+ return new Resolved(side.getCarriers(), side.getJavaType(),
side.getFormat());
+ }
+
+ /**
+ * The body of the class an option names: an object of it, or null when it
does not convert. Object says nothing.
+ */
+ private static Resolved ofClass(String javaType) {
+ String carrier = BodyTypeModel.carrierOf(javaType);
+ if ("unknown".equals(carrier)) {
+ return null;
+ }
+ return new Resolved(List.of(carrier, "null"), javaType.trim(), null);
+ }
+
+ /**
+ * Whether a value of an option is the value a body is for: in any case,
and in any style of an enum constant, as
+ * the catalog and the enum type converter take it, so convert-to-json and
convertToJson are CONVERT_TO_JSON.
+ */
+ public static boolean isSameValue(String value, String other) {
+ return value.equalsIgnoreCase(other) ||
asEnumConstant(value).equals(asEnumConstant(other));
+ }
+
+ /** The value as an enum constant, as StringHelper.asEnumConstantValue in
camel-util gives it. */
+ private static String asEnumConstant(String value) {
+ return camelCaseToDash(value).replaceAll("-+", "-").replace('-',
'_').toUpperCase(Locale.ENGLISH);
+ }
+
+ /** As StringHelper.camelCaseToDash in camel-util, which this module does
not depend on. */
+ private static String camelCaseToDash(String text) {
+ StringBuilder answer = new StringBuilder(text.length() + 8);
+ char prev = 0;
+ char[] arr = text.toCharArray();
+ for (int i = 0; i < arr.length; i++) {
+ char ch = arr[i];
+ if (ch == '-' || ch == '_') {
+ answer.append('-');
+ } else if (Character.isUpperCase(ch) && prev != 0) {
+ char next = i < arr.length - 1 ? arr[i + 1] : 0;
+ if (!Character.isUpperCase(prev) || next != 0 &&
Character.isLowerCase(next)) {
+ if (prev != '-' && prev != '_') {
+ answer.append('-');
+ }
+ }
+ answer.append(Character.toLowerCase(ch));
+ } else {
+ answer.append(Character.toLowerCase(ch));
+ }
+ prev = ch;
+ }
+ return answer.toString();
+ }
+}
diff --git
a/tooling/camel-tooling-model/src/test/java/org/apache/camel/tooling/model/BodyTypeResolverTest.java
b/tooling/camel-tooling-model/src/test/java/org/apache/camel/tooling/model/BodyTypeResolverTest.java
new file mode 100644
index 000000000000..bc9f75acbea4
--- /dev/null
+++
b/tooling/camel-tooling-model/src/test/java/org/apache/camel/tooling/model/BodyTypeResolverTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.tooling.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.tooling.model.BodyTypeModel.OptionBodyTypeModel;
+import org.apache.camel.tooling.model.BodyTypeResolver.Resolved;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The rule the tools share to read a side of the body types for the options a
step sets (CAMEL-25928).
+ */
+class BodyTypeResolverTest {
+
+ @Test
+ void theCarriersWithTheDefaultOptions() {
+ BodyTypeModel side = side(List.of("map", "list"));
+ side.setFormat("json");
+ assertEquals(new Resolved(List.of("map", "list"), null, "json"),
resolve(side, Map.of()));
+ assertNull(resolve(side(List.of("unknown")), Map.of()));
+ assertNull(resolve(null, Map.of()));
+ }
+
+ @Test
+ void aSideFromAnotherModelIsResolvedByTheCaller() {
+ BodyTypeModel side = new BodyTypeModel();
+ side.setFrom(BodyTypeModel.MARSHAL_OUT);
+ assertNull(resolve(side, Map.of()));
+ }
+
+ @Test
+ void aValueKnownOnlyWhenTheRouteRunsIsNotKnown() {
+ BodyTypeModel side = jackson();
+ assertNull(resolve(side, Map.of("unmarshalType", "{{type}}")));
+ assertNull(resolve(side, Map.of("useList", "${header.list}")));
+ // an option the component can set, and the step does not
+ assertNull(BodyTypeResolver.resolve(side, Map.of(),
Set.of("useList")));
+ }
+
+ @Test
+ void anOptionTheBodyDependsOnDecidesFirst() {
+ BodyTypeModel side = jackson();
+ assertEquals(List.of("list"), resolve(side, Map.of("useList", "true",
"unmarshalType", "com.acme.Order")).carriers());
+ // no body for its value: not known, also with the class
+ assertNull(resolve(side, Map.of("collectionType", "java.util.Set",
"unmarshalType", "com.acme.Order")));
+ assertNull(resolve(side, Map.of("useList", "false")));
+ }
+
+ @Test
+ void theOptionThatNamesTheClassOfTheBody() {
+ BodyTypeModel side = jackson();
+ Resolved order = resolve(side, Map.of("unmarshalType",
"com.acme.Order"));
+ assertEquals(new Resolved(List.of("pojo", "null"), "com.acme.Order",
null), order);
+ assertEquals(List.of("text", "null"), resolve(side,
Map.of("unmarshalType", "String")).carriers());
+ // Object can be anything
+ assertNull(resolve(side, Map.of("unmarshalType", "java.lang.Object")));
+ }
+
+ @Test
+ void theOptionThatNamesTheClassWinsOverTheValuesOfOtherOptions() {
+ BodyTypeModel side = side(List.of("pojo"));
+ side.setJavaType("org.w3c.dom.NodeList");
+ side.setJavaTypeOption("resultType");
+ side.getByOption().add(when("resultQName", "STRING", List.of("text"),
null));
+ assertEquals(List.of("text"), resolve(side, Map.of("resultQName",
"string")).carriers());
+ assertEquals(List.of("value", "null"),
+ resolve(side, Map.of("resultQName", "STRING", "resultType",
"java.lang.Integer")).carriers());
+ }
+
+ @Test
+ void anOptionValueCanNameTheClassToo() {
+ BodyTypeModel side = side(List.of("list", "unchanged"));
+ side.getByOption().add(when("outputType", "SelectOne", List.of("map",
"value", "null"), "outputClass"));
+ side.getByOption().add(when("outputType", "StreamList",
List.of("iterator"), null));
+ assertEquals(List.of("map", "value", "null"), resolve(side,
Map.of("outputType", "SelectOne")).carriers());
+ assertEquals(new Resolved(List.of("pojo", "null"), "com.acme.Order",
null),
+ resolve(side, Map.of("outputType", "SelectOne", "outputClass",
"com.acme.Order")));
+ // Object can be anything, also for an option value
+ assertNull(resolve(side, Map.of("outputType", "SelectOne",
"outputClass", "java.lang.Object")));
+ // the class does not change the List of the default outputType
+ assertEquals(List.of("list", "unchanged"), resolve(side,
Map.of("outputClass", "com.acme.Order")).carriers());
+ assertEquals(List.of("iterator"), resolve(side, Map.of("outputType",
"stream-list")).carriers());
+ }
+
+ @Test
+ void theCarriersAreACopy() {
+ BodyTypeModel side = side(new ArrayList<>(List.of("text")));
+ Resolved resolved = resolve(side, Map.of());
+ side.getCarriers().add("bytes");
+ assertEquals(List.of("text"), resolved.carriers());
+ }
+
+ @Test
+ void twoOptionValuesThatSayDifferentBodiesAreNotKnown() {
+ BodyTypeModel side = side(List.of("text"));
+ side.getByOption().add(when("output", "bytes", List.of("bytes"),
null));
+ side.getByOption().add(when("binary", "true", List.of("stream"),
null));
+ assertNull(resolve(side, Map.of("output", "bytes", "binary", "true")));
+ assertEquals(List.of("bytes"), resolve(side, Map.of("output",
"BYTES")).carriers());
+ }
+
+ @Test
+ void theValuesOfAnEnumInAnyStyle() {
+ assertTrue(BodyTypeResolver.isSameValue("convert-to-json",
"CONVERT_TO_JSON"));
+ assertTrue(BodyTypeResolver.isSameValue("convertToJson",
"CONVERT_TO_JSON"));
+ assertTrue(BodyTypeResolver.isSameValue("selectOne", "SelectOne"));
+ assertFalse(BodyTypeResolver.isSameValue("SelectList", "SelectOne"));
+ }
+
+ private static Resolved resolve(BodyTypeModel side, Map<String, String>
options) {
+ return BodyTypeResolver.resolve(side, options, Set.of());
+ }
+
+ /** The body after unmarshal of jackson: a Map or a List, a List with
useList, or the class of unmarshalType. */
+ private static BodyTypeModel jackson() {
+ BodyTypeModel side = side(List.of("map", "list"));
+ side.setDependsOn(List.of("collectionType", "useList",
"allowUnmarshallType"));
+ side.setJavaTypeOption("unmarshalType");
+ side.getByOption().add(when("useList", "true", List.of("list"), null));
+ return side;
+ }
+
+ private static BodyTypeModel side(List<String> carriers) {
+ BodyTypeModel side = new BodyTypeModel();
+ side.setCarriers(carriers);
+ return side;
+ }
+
+ private static OptionBodyTypeModel when(String option, String value,
List<String> carriers, String javaTypeOption) {
+ OptionBodyTypeModel when = new OptionBodyTypeModel();
+ when.setOption(option);
+ when.setValue(value);
+ when.setCarriers(carriers);
+ when.setJavaTypeOption(javaTypeOption);
+ return when;
+ }
+}
diff --git
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
index 6369a6fa702f..06ab8560de80 100644
---
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
+++
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
@@ -21,7 +21,6 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
@@ -32,8 +31,8 @@ import org.apache.camel.spi.BodyTypes;
import org.apache.camel.tooling.model.BaseModel;
import org.apache.camel.tooling.model.BaseOptionModel;
import org.apache.camel.tooling.model.BodyTypeModel;
+import org.apache.camel.tooling.model.BodyTypeResolver;
import org.apache.camel.tooling.model.ComponentModel;
-import org.apache.camel.tooling.util.Strings;
/**
* Carries the {@link BodyTypes} of a component, data format, language or EIP
into its model, which the catalog JSON is
@@ -238,26 +237,13 @@ final class BodyTypesHelper {
List<String> values = option.getEnums() != null &&
!option.getEnums().isEmpty()
? option.getEnums()
: "boolean".equals(option.getType()) ? List.of("true",
"false") : List.of();
- if (!values.isEmpty() && values.stream().noneMatch(v -> isSameValue(v,
when.value()))) {
+ if (!values.isEmpty() && values.stream().noneMatch(v ->
BodyTypeResolver.isSameValue(v, when.value()))) {
throw new IllegalArgumentException(
owner.getName() + ": the @BodyTypes " + side + " has " +
when.option() + "=" + when.value()
+ ", which is not a value of
the option: " + String.join(", ", values));
}
}
- /**
- * Whether two values of an option are one value as Camel takes it: in any
case, and in any style of an enum
- * constant, so convert-to-json and convertToJson are CONVERT_TO_JSON, as
for the catalog and the enum type
- * converter (StringHelper.asEnumConstantValue in camel-util, which this
plugin does not depend on).
- */
- private static boolean isSameValue(String value, String other) {
- return value.equalsIgnoreCase(other) ||
asEnumConstant(value).equals(asEnumConstant(other));
- }
-
- private static String asEnumConstant(String value) {
- return Strings.camelCaseToDash(value).replaceAll("-+",
"-").replace('-', '_').toUpperCase(Locale.ENGLISH);
- }
-
/**
* A body has a carrier: an option value without one would say nothing.
UNCHANGED is for the body after a step, such
* as a sql query that leaves it unchanged unless it is a select. ANY is
for the bodies a step needs, and UNKNOWN