gnodet-bot commented on code in PR #27792:
URL: https://github.com/apache/camel/pull/27792#discussion_r4279837152
##########
components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc:
##########
@@ -0,0 +1,409 @@
+= Citrus Component
+:doctitle: Citrus
+:shortname: citrus
+:artifactid: camel-test-citrus
+:description: Citrus message validation component for testing
+:since: 4.24
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The Citrus component integrates with the https://citrusframework.org/[Citrus
integration test framework]
+to perform message validation on exchanges arriving at a Camel endpoint. It
allows test authors to
+verify exchange body content, headers, and path expressions (JsonPath, XPath)
using Citrus validation
+modules.
+
+This component is intended for use in Camel route unit and integration tests.
+
+== Maven dependency
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-citrus</artifactId>
+ <scope>test</scope>
+</dependency>
+----
+
+== URI format
+
+----
+citrus:operation/name[?options]
+----
+
+Where `operation` is `validate` or `receive`, and `name` is an optional
identifier to distinguish
+multiple Citrus endpoints in the same route.
+
+* `validate` — validates exchanges without modifying them (pass-through
validation).
+* `receive` — validates exchanges and returns a simulated response
(request/response simulation).
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+== Usage
+
+The Citrus endpoint acts as a producer-only endpoint. Routes send exchanges
*to* `citrus:validate`,
+and the component validates each exchange using Citrus message validation.
+
+=== Validation modules
+
+The component does not bundle Citrus validation modules. You must add the
required validation
+module dependency for your message type to your test classpath:
+
+[cols="1,2", options="header"]
+|===
+| Dependency | Description
+
+| `citrus-validation-json`
+| JSON message validation with JsonPath expressions and JSON schema support
+
+| `citrus-validation-xml`
+| XML message validation with XPath expressions, XML schema (XSD/WSDL) and
XSLT support
+
+| `citrus-validation-text`
+| Plain text message validation including binary (stream and base64) and gzip
support
+
+| `citrus-validation-yaml`
+| YAML message validation
+
+| `citrus-validation-binary`
+| Binary message validation with stream comparison and base64 encoding
+
+| `citrus-validation-hamcrest`
+| Hamcrest matcher assertions for use with JsonPath and XPath expressions
+|===
+
+All modules use the `org.citrusframework` group ID. Refer to the
+https://citrusframework.org/citrus/reference/html/[Citrus reference
documentation] for detailed
+usage of each validation module.
+
+=== Citrus empowered message validation
+
+Citrus uses a powerful template-based approach to message validation: you
provide an expected message template and
+Citrus compares it against the received message using format-aware comparison.
This brings several
+capabilities beyond simple string equality.
+
+==== Ignore expressions
+
+Use the `@ignore@` placeholder anywhere in an expected message template to
skip validation of dynamic
+values such as timestamps, generated IDs, or correlation tokens. The element
must be present, but its
+value is not checked.
+
+[source,json]
+----
+{
+ "orderId": "@ignore@",
+ "timestamp": "@ignore@",
+ "status": "created"
+}
+----
+
+This works across all supported message types — in XML element and attribute
values, JSON property values,
+and YAML property values. The ignore placeholder can also replace an entire
sub-object or element
+including its children.
+
+For plain text messages, the `@ignore@` placeholder works on a positional
basis — it matches a word-length
+character sequence at the exact position in the text. Use `@ignore(n)@` to
ignore exactly _n_ characters
+when the value contains non-word characters:
+
+[source,plaintext]
+----
+Your current id is "@ignore@", it expires on 01/01/2030
+----
+
+This ignores the word following `id is "` based on the position of the ignore
expression in the template.
+Because no character count is specified, Citrus ignores the entire word
regardless of its length.
+The word boundary is determined by the first non-word character (whitespace,
punctuation, etc.).
+
+[source,plaintext]
+----
+Your current id is "@ignore(12)@", it expires on @ignore(10)@
+----
+
+This ignores the next 12 characters starting at the position of the ignore
expression in the template.
+This also includes whitespace and non-word characters (e.g. `Your current id
is "1234/ 34567 D", it expires on 01/01/2030`).
+
+==== Validation matchers
+
+Citrus validation matchers are inline assertions embedded directly in the
expected message template,
+surrounded by `@` markers. They work across all message formats (JSON, XML,
YAML, plain text) and
+provide richer assertions than exact equality:
+
+[source,json]
+----
+{
+ "name": "@equalsIgnoreCase('JOHN')@",
+ "age": "@greaterThan(17)@",
+ "email": "@matches('[a-z]+@[a-z]+\\.[a-z]+')@",
+ "created": "@matchesDatePattern('yyyy-MM-dd')@",
+ "id": "@isNumber()@",
+ "token": "@startsWith('tk-')@",
+ "status": "@contains('active')@"
+}
+----
+
+You can also use `@variable('name')@` to extract the actual value into a
Citrus test variable for
+later use, and `@assertThat(hamcrestMatcher)@` to use Hamcrest matchers inline
(requires
+`citrus-validation-hamcrest`):
+
+[source,json]
+----
+{
+ "count": "@assertThat(allOf(greaterThan(0), lessThan(100)))@",
+ "type": "@assertThat(anyOf(equalTo(A), equalTo(B)))@"
+}
+----
+
+==== JSON validation
+
+Citrus JSON validation compares JSON object structures regardless of property
order.
+It supports two modes configurable via `citrus.json.message.validation.strict`:
+
+* **Strict mode** (default): the received JSON must match the expected
structure exactly — no
+ additional properties or array items are allowed.
+* **Non-strict mode**: the expected JSON can be a subset of the received
message — additional
+ properties and array items in the received JSON are silently ignored. This
is useful when you
+ only want to verify specific fields.
+
+JSON validation also supports:
+
+* **JsonPath expressions** for targeted field validation without specifying
the full message body
+* **JSON schema validation** against JSON Schema V3/V4 definitions
+* **`@ignore@` and validation matchers** within JSON values
+
+==== XML validation
+
+Citrus XML validation performs a structural DOM tree comparison that correctly
handles:
+
+* **XML namespaces** with different prefixes — the comparison is
namespace-aware, so
+ `<ns1:element xmlns:ns1="...">` and `<ns2:element xmlns:ns2="...">` are
treated as equivalent
+ when they share the same namespace URI.
+* **XML schema validation** against XSD and WSDL definitions to verify
structural correctness.
+* **XPath expressions** for targeted element validation, with full namespace
support via
+ namespace context configuration. This includes automatic namespace prefix
resolution when the
+ prefixes differ between the control template and the received message.
+* **`@ignore@` and validation matchers** within XML element values and
attributes.
+
+==== Plain text validation
+
+Plain text validation performs exact string comparison with support for:
+
+* **Positional `@ignore@`** to skip dynamic word-length sequences at specific
positions.
+* **`@ignore(n)@`** to skip exactly _n_ characters (useful for values with
non-word characters).
+* **`@variable('name')@`** to extract values at specific positions into test
variables.
+* **Whitespace normalization** (configurable) to ignore line separator
differences (CR, LF, CRLF)
+ or all whitespace.
+* **Gzip** and **binary/base64** content comparison via the respective message
types.
+
+==== YAML validation
+
+YAML validation compares YAML documents structurally, similar to JSON
validation:
+
+* **Strict/non-strict mode** — configurable via
`citrus.yaml.message.validation.strict`.
+* **Path expressions** for ignoring specific elements in the YAML structure.
+* **`@ignore@` and validation matchers** within YAML values.
+
+=== Validation specification API
+
+The endpoint provides a fluent API for specifying validation expectations. You
can define a single
+validation that applies to all incoming exchanges, or specify individual
validations for each
+exchange in sequence.
+
+==== Single validation for all exchanges
+
+Use `validateAll()` to define one validation specification that is applied to
every exchange.
+The returned `MessageValidationSpec` provides a fluent API with `body()`,
`header()`, and
+`validate()` methods:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+citrus.validateAll()
+ .body("{\"status\": \"ok\"}")
+ .header("operation", "test");
+citrus.expectedMessageCount(3);
+
+// All three exchanges are validated against the same spec
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+
+citrus.assertIsSatisfied();
+----
+
+==== Per-exchange sequential validation
+
+Use `validate()` to specify different validation expectations for the 1st,
2nd, 3rd, etc.
+exchange. Each call to `validate()` auto-increments the index, so specs are
applied in order.
+Each incoming exchange triggers the next validation spec in sequence:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+
+citrus.validate().body("{\"event\": \"created\"}").header("action", "create");
+citrus.validate().body("{\"event\": \"updated\"}").header("action", "update");
+citrus.validate().body("{\"event\": \"deleted\"}").header("action", "delete");
+citrus.expectedMessageCount(3);
+
+template.sendBodyAndHeader("direct:start", "{\"event\": \"created\"}",
"action", "create");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"updated\"}",
"action", "update");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"deleted\"}",
"action", "delete");
+
+citrus.assertIsSatisfied();
+----
+
+You can also use `validate(index)` to target a specific exchange by its
0-based position.
+
+When both `validate(index)` and `validateAll()` are configured, the indexed
spec takes priority —
+it replaces `validateAll()` for that exchange entirely (the two are never
combined). The
+`validateAll()` spec acts as a fallback for exchanges that have no matching
indexed spec.
+
+If an exchange arrives at an index with no matching indexed spec and no
`validateAll()` fallback,
+the endpoint records a failure. For the `validate` operation, the failure is
collected and reported
+when `assertIsSatisfied()` is called (similar to MockEndpoint). For the
`receive` operation, the
+failure is thrown immediately as an exception.
+
+[source,java]
+----
+citrus.validate(0).body("{\"event\": \"start\"}");
Review Comment:
⚠️ **`validate(index)` docs say 0-based but `MockEndpoint` counter is
1-based**
The docs say "0-based position" and show `validate(0)` / `// Exchange 0 →
...`. But `MockEndpoint.performAssertions()` calls `counter.incrementAndGet()`
(starting at 0 → 1 on first exchange) and then looks up `processors.get(1)`.
`validate(0)` registers at index 0 which is **never looked up** — the first
exchange looks up index 1.
The auto-incrementing `validate()` call correctly starts at 1
(`nextSpecIndex.incrementAndGet()` with initial value 0 → returns 1 on first
call), and all tests use 1-based indices. The docs need to say "1-based" and
use `validate(1)`.
```suggestion
You can also use `validate(index)` to target a specific exchange by its
1-based position.
```
##########
components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc:
##########
@@ -0,0 +1,409 @@
+= Citrus Component
+:doctitle: Citrus
+:shortname: citrus
+:artifactid: camel-test-citrus
+:description: Citrus message validation component for testing
+:since: 4.24
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The Citrus component integrates with the https://citrusframework.org/[Citrus
integration test framework]
+to perform message validation on exchanges arriving at a Camel endpoint. It
allows test authors to
+verify exchange body content, headers, and path expressions (JsonPath, XPath)
using Citrus validation
+modules.
+
+This component is intended for use in Camel route unit and integration tests.
+
+== Maven dependency
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-citrus</artifactId>
+ <scope>test</scope>
+</dependency>
+----
+
+== URI format
+
+----
+citrus:operation/name[?options]
+----
+
+Where `operation` is `validate` or `receive`, and `name` is an optional
identifier to distinguish
+multiple Citrus endpoints in the same route.
+
+* `validate` — validates exchanges without modifying them (pass-through
validation).
+* `receive` — validates exchanges and returns a simulated response
(request/response simulation).
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+== Usage
+
+The Citrus endpoint acts as a producer-only endpoint. Routes send exchanges
*to* `citrus:validate`,
+and the component validates each exchange using Citrus message validation.
+
+=== Validation modules
+
+The component does not bundle Citrus validation modules. You must add the
required validation
+module dependency for your message type to your test classpath:
+
+[cols="1,2", options="header"]
+|===
+| Dependency | Description
+
+| `citrus-validation-json`
+| JSON message validation with JsonPath expressions and JSON schema support
+
+| `citrus-validation-xml`
+| XML message validation with XPath expressions, XML schema (XSD/WSDL) and
XSLT support
+
+| `citrus-validation-text`
+| Plain text message validation including binary (stream and base64) and gzip
support
+
+| `citrus-validation-yaml`
+| YAML message validation
+
+| `citrus-validation-binary`
+| Binary message validation with stream comparison and base64 encoding
+
+| `citrus-validation-hamcrest`
+| Hamcrest matcher assertions for use with JsonPath and XPath expressions
+|===
+
+All modules use the `org.citrusframework` group ID. Refer to the
+https://citrusframework.org/citrus/reference/html/[Citrus reference
documentation] for detailed
+usage of each validation module.
+
+=== Citrus empowered message validation
+
+Citrus uses a powerful template-based approach to message validation: you
provide an expected message template and
+Citrus compares it against the received message using format-aware comparison.
This brings several
+capabilities beyond simple string equality.
+
+==== Ignore expressions
+
+Use the `@ignore@` placeholder anywhere in an expected message template to
skip validation of dynamic
+values such as timestamps, generated IDs, or correlation tokens. The element
must be present, but its
+value is not checked.
+
+[source,json]
+----
+{
+ "orderId": "@ignore@",
+ "timestamp": "@ignore@",
+ "status": "created"
+}
+----
+
+This works across all supported message types — in XML element and attribute
values, JSON property values,
+and YAML property values. The ignore placeholder can also replace an entire
sub-object or element
+including its children.
+
+For plain text messages, the `@ignore@` placeholder works on a positional
basis — it matches a word-length
+character sequence at the exact position in the text. Use `@ignore(n)@` to
ignore exactly _n_ characters
+when the value contains non-word characters:
+
+[source,plaintext]
+----
+Your current id is "@ignore@", it expires on 01/01/2030
+----
+
+This ignores the word following `id is "` based on the position of the ignore
expression in the template.
+Because no character count is specified, Citrus ignores the entire word
regardless of its length.
+The word boundary is determined by the first non-word character (whitespace,
punctuation, etc.).
+
+[source,plaintext]
+----
+Your current id is "@ignore(12)@", it expires on @ignore(10)@
+----
+
+This ignores the next 12 characters starting at the position of the ignore
expression in the template.
+This also includes whitespace and non-word characters (e.g. `Your current id
is "1234/ 34567 D", it expires on 01/01/2030`).
+
+==== Validation matchers
+
+Citrus validation matchers are inline assertions embedded directly in the
expected message template,
+surrounded by `@` markers. They work across all message formats (JSON, XML,
YAML, plain text) and
+provide richer assertions than exact equality:
+
+[source,json]
+----
+{
+ "name": "@equalsIgnoreCase('JOHN')@",
+ "age": "@greaterThan(17)@",
+ "email": "@matches('[a-z]+@[a-z]+\\.[a-z]+')@",
+ "created": "@matchesDatePattern('yyyy-MM-dd')@",
+ "id": "@isNumber()@",
+ "token": "@startsWith('tk-')@",
+ "status": "@contains('active')@"
+}
+----
+
+You can also use `@variable('name')@` to extract the actual value into a
Citrus test variable for
+later use, and `@assertThat(hamcrestMatcher)@` to use Hamcrest matchers inline
(requires
+`citrus-validation-hamcrest`):
+
+[source,json]
+----
+{
+ "count": "@assertThat(allOf(greaterThan(0), lessThan(100)))@",
+ "type": "@assertThat(anyOf(equalTo(A), equalTo(B)))@"
+}
+----
+
+==== JSON validation
+
+Citrus JSON validation compares JSON object structures regardless of property
order.
+It supports two modes configurable via `citrus.json.message.validation.strict`:
+
+* **Strict mode** (default): the received JSON must match the expected
structure exactly — no
+ additional properties or array items are allowed.
+* **Non-strict mode**: the expected JSON can be a subset of the received
message — additional
+ properties and array items in the received JSON are silently ignored. This
is useful when you
+ only want to verify specific fields.
+
+JSON validation also supports:
+
+* **JsonPath expressions** for targeted field validation without specifying
the full message body
+* **JSON schema validation** against JSON Schema V3/V4 definitions
+* **`@ignore@` and validation matchers** within JSON values
+
+==== XML validation
+
+Citrus XML validation performs a structural DOM tree comparison that correctly
handles:
+
+* **XML namespaces** with different prefixes — the comparison is
namespace-aware, so
+ `<ns1:element xmlns:ns1="...">` and `<ns2:element xmlns:ns2="...">` are
treated as equivalent
+ when they share the same namespace URI.
+* **XML schema validation** against XSD and WSDL definitions to verify
structural correctness.
+* **XPath expressions** for targeted element validation, with full namespace
support via
+ namespace context configuration. This includes automatic namespace prefix
resolution when the
+ prefixes differ between the control template and the received message.
+* **`@ignore@` and validation matchers** within XML element values and
attributes.
+
+==== Plain text validation
+
+Plain text validation performs exact string comparison with support for:
+
+* **Positional `@ignore@`** to skip dynamic word-length sequences at specific
positions.
+* **`@ignore(n)@`** to skip exactly _n_ characters (useful for values with
non-word characters).
+* **`@variable('name')@`** to extract values at specific positions into test
variables.
+* **Whitespace normalization** (configurable) to ignore line separator
differences (CR, LF, CRLF)
+ or all whitespace.
+* **Gzip** and **binary/base64** content comparison via the respective message
types.
+
+==== YAML validation
+
+YAML validation compares YAML documents structurally, similar to JSON
validation:
+
+* **Strict/non-strict mode** — configurable via
`citrus.yaml.message.validation.strict`.
+* **Path expressions** for ignoring specific elements in the YAML structure.
+* **`@ignore@` and validation matchers** within YAML values.
+
+=== Validation specification API
+
+The endpoint provides a fluent API for specifying validation expectations. You
can define a single
+validation that applies to all incoming exchanges, or specify individual
validations for each
+exchange in sequence.
+
+==== Single validation for all exchanges
+
+Use `validateAll()` to define one validation specification that is applied to
every exchange.
+The returned `MessageValidationSpec` provides a fluent API with `body()`,
`header()`, and
+`validate()` methods:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+citrus.validateAll()
+ .body("{\"status\": \"ok\"}")
+ .header("operation", "test");
+citrus.expectedMessageCount(3);
+
+// All three exchanges are validated against the same spec
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+
+citrus.assertIsSatisfied();
+----
+
+==== Per-exchange sequential validation
+
+Use `validate()` to specify different validation expectations for the 1st,
2nd, 3rd, etc.
+exchange. Each call to `validate()` auto-increments the index, so specs are
applied in order.
+Each incoming exchange triggers the next validation spec in sequence:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+
+citrus.validate().body("{\"event\": \"created\"}").header("action", "create");
+citrus.validate().body("{\"event\": \"updated\"}").header("action", "update");
+citrus.validate().body("{\"event\": \"deleted\"}").header("action", "delete");
+citrus.expectedMessageCount(3);
+
+template.sendBodyAndHeader("direct:start", "{\"event\": \"created\"}",
"action", "create");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"updated\"}",
"action", "update");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"deleted\"}",
"action", "delete");
+
+citrus.assertIsSatisfied();
+----
+
+You can also use `validate(index)` to target a specific exchange by its
0-based position.
+
+When both `validate(index)` and `validateAll()` are configured, the indexed
spec takes priority —
+it replaces `validateAll()` for that exchange entirely (the two are never
combined). The
+`validateAll()` spec acts as a fallback for exchanges that have no matching
indexed spec.
+
+If an exchange arrives at an index with no matching indexed spec and no
`validateAll()` fallback,
+the endpoint records a failure. For the `validate` operation, the failure is
collected and reported
+when `assertIsSatisfied()` is called (similar to MockEndpoint). For the
`receive` operation, the
+failure is thrown immediately as an exception.
+
+[source,java]
+----
+citrus.validate(0).body("{\"event\": \"start\"}");
+citrus.validateAll().body("{\"event\": \"progress\"}").header("type",
"status");
+
+// Exchange 0 → validated against validate(0) only (validateAll() is NOT
applied)
+// Exchange 1, 2, ... → validated against validateAll() (body + header)
+----
Review Comment:
⚠️ **Comment is wrong — index is 1-based**
```suggestion
// Exchange 1 → validated against validate(1) only (validateAll() is NOT
applied)
```
##########
components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc:
##########
@@ -0,0 +1,409 @@
+= Citrus Component
+:doctitle: Citrus
+:shortname: citrus
+:artifactid: camel-test-citrus
+:description: Citrus message validation component for testing
+:since: 4.24
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The Citrus component integrates with the https://citrusframework.org/[Citrus
integration test framework]
+to perform message validation on exchanges arriving at a Camel endpoint. It
allows test authors to
+verify exchange body content, headers, and path expressions (JsonPath, XPath)
using Citrus validation
+modules.
+
+This component is intended for use in Camel route unit and integration tests.
+
+== Maven dependency
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-citrus</artifactId>
+ <scope>test</scope>
+</dependency>
+----
+
+== URI format
+
+----
+citrus:operation/name[?options]
+----
+
+Where `operation` is `validate` or `receive`, and `name` is an optional
identifier to distinguish
+multiple Citrus endpoints in the same route.
+
+* `validate` — validates exchanges without modifying them (pass-through
validation).
+* `receive` — validates exchanges and returns a simulated response
(request/response simulation).
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+== Usage
+
+The Citrus endpoint acts as a producer-only endpoint. Routes send exchanges
*to* `citrus:validate`,
+and the component validates each exchange using Citrus message validation.
+
+=== Validation modules
+
+The component does not bundle Citrus validation modules. You must add the
required validation
+module dependency for your message type to your test classpath:
+
+[cols="1,2", options="header"]
+|===
+| Dependency | Description
+
+| `citrus-validation-json`
+| JSON message validation with JsonPath expressions and JSON schema support
+
+| `citrus-validation-xml`
+| XML message validation with XPath expressions, XML schema (XSD/WSDL) and
XSLT support
+
+| `citrus-validation-text`
+| Plain text message validation including binary (stream and base64) and gzip
support
+
+| `citrus-validation-yaml`
+| YAML message validation
+
+| `citrus-validation-binary`
+| Binary message validation with stream comparison and base64 encoding
+
+| `citrus-validation-hamcrest`
+| Hamcrest matcher assertions for use with JsonPath and XPath expressions
+|===
+
+All modules use the `org.citrusframework` group ID. Refer to the
+https://citrusframework.org/citrus/reference/html/[Citrus reference
documentation] for detailed
+usage of each validation module.
+
+=== Citrus empowered message validation
+
+Citrus uses a powerful template-based approach to message validation: you
provide an expected message template and
+Citrus compares it against the received message using format-aware comparison.
This brings several
+capabilities beyond simple string equality.
+
+==== Ignore expressions
+
+Use the `@ignore@` placeholder anywhere in an expected message template to
skip validation of dynamic
+values such as timestamps, generated IDs, or correlation tokens. The element
must be present, but its
+value is not checked.
+
+[source,json]
+----
+{
+ "orderId": "@ignore@",
+ "timestamp": "@ignore@",
+ "status": "created"
+}
+----
+
+This works across all supported message types — in XML element and attribute
values, JSON property values,
+and YAML property values. The ignore placeholder can also replace an entire
sub-object or element
+including its children.
+
+For plain text messages, the `@ignore@` placeholder works on a positional
basis — it matches a word-length
+character sequence at the exact position in the text. Use `@ignore(n)@` to
ignore exactly _n_ characters
+when the value contains non-word characters:
+
+[source,plaintext]
+----
+Your current id is "@ignore@", it expires on 01/01/2030
+----
+
+This ignores the word following `id is "` based on the position of the ignore
expression in the template.
+Because no character count is specified, Citrus ignores the entire word
regardless of its length.
+The word boundary is determined by the first non-word character (whitespace,
punctuation, etc.).
+
+[source,plaintext]
+----
+Your current id is "@ignore(12)@", it expires on @ignore(10)@
+----
+
+This ignores the next 12 characters starting at the position of the ignore
expression in the template.
+This also includes whitespace and non-word characters (e.g. `Your current id
is "1234/ 34567 D", it expires on 01/01/2030`).
+
+==== Validation matchers
+
+Citrus validation matchers are inline assertions embedded directly in the
expected message template,
+surrounded by `@` markers. They work across all message formats (JSON, XML,
YAML, plain text) and
+provide richer assertions than exact equality:
+
+[source,json]
+----
+{
+ "name": "@equalsIgnoreCase('JOHN')@",
+ "age": "@greaterThan(17)@",
+ "email": "@matches('[a-z]+@[a-z]+\\.[a-z]+')@",
+ "created": "@matchesDatePattern('yyyy-MM-dd')@",
+ "id": "@isNumber()@",
+ "token": "@startsWith('tk-')@",
+ "status": "@contains('active')@"
+}
+----
+
+You can also use `@variable('name')@` to extract the actual value into a
Citrus test variable for
+later use, and `@assertThat(hamcrestMatcher)@` to use Hamcrest matchers inline
(requires
+`citrus-validation-hamcrest`):
+
+[source,json]
+----
+{
+ "count": "@assertThat(allOf(greaterThan(0), lessThan(100)))@",
+ "type": "@assertThat(anyOf(equalTo(A), equalTo(B)))@"
+}
+----
+
+==== JSON validation
+
+Citrus JSON validation compares JSON object structures regardless of property
order.
+It supports two modes configurable via `citrus.json.message.validation.strict`:
+
+* **Strict mode** (default): the received JSON must match the expected
structure exactly — no
+ additional properties or array items are allowed.
+* **Non-strict mode**: the expected JSON can be a subset of the received
message — additional
+ properties and array items in the received JSON are silently ignored. This
is useful when you
+ only want to verify specific fields.
+
+JSON validation also supports:
+
+* **JsonPath expressions** for targeted field validation without specifying
the full message body
+* **JSON schema validation** against JSON Schema V3/V4 definitions
+* **`@ignore@` and validation matchers** within JSON values
+
+==== XML validation
+
+Citrus XML validation performs a structural DOM tree comparison that correctly
handles:
+
+* **XML namespaces** with different prefixes — the comparison is
namespace-aware, so
+ `<ns1:element xmlns:ns1="...">` and `<ns2:element xmlns:ns2="...">` are
treated as equivalent
+ when they share the same namespace URI.
+* **XML schema validation** against XSD and WSDL definitions to verify
structural correctness.
+* **XPath expressions** for targeted element validation, with full namespace
support via
+ namespace context configuration. This includes automatic namespace prefix
resolution when the
+ prefixes differ between the control template and the received message.
+* **`@ignore@` and validation matchers** within XML element values and
attributes.
+
+==== Plain text validation
+
+Plain text validation performs exact string comparison with support for:
+
+* **Positional `@ignore@`** to skip dynamic word-length sequences at specific
positions.
+* **`@ignore(n)@`** to skip exactly _n_ characters (useful for values with
non-word characters).
+* **`@variable('name')@`** to extract values at specific positions into test
variables.
+* **Whitespace normalization** (configurable) to ignore line separator
differences (CR, LF, CRLF)
+ or all whitespace.
+* **Gzip** and **binary/base64** content comparison via the respective message
types.
+
+==== YAML validation
+
+YAML validation compares YAML documents structurally, similar to JSON
validation:
+
+* **Strict/non-strict mode** — configurable via
`citrus.yaml.message.validation.strict`.
+* **Path expressions** for ignoring specific elements in the YAML structure.
+* **`@ignore@` and validation matchers** within YAML values.
+
+=== Validation specification API
+
+The endpoint provides a fluent API for specifying validation expectations. You
can define a single
+validation that applies to all incoming exchanges, or specify individual
validations for each
+exchange in sequence.
+
+==== Single validation for all exchanges
+
+Use `validateAll()` to define one validation specification that is applied to
every exchange.
+The returned `MessageValidationSpec` provides a fluent API with `body()`,
`header()`, and
+`validate()` methods:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+citrus.validateAll()
+ .body("{\"status\": \"ok\"}")
+ .header("operation", "test");
+citrus.expectedMessageCount(3);
+
+// All three exchanges are validated against the same spec
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+
+citrus.assertIsSatisfied();
+----
+
+==== Per-exchange sequential validation
+
+Use `validate()` to specify different validation expectations for the 1st,
2nd, 3rd, etc.
+exchange. Each call to `validate()` auto-increments the index, so specs are
applied in order.
+Each incoming exchange triggers the next validation spec in sequence:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+
+citrus.validate().body("{\"event\": \"created\"}").header("action", "create");
+citrus.validate().body("{\"event\": \"updated\"}").header("action", "update");
+citrus.validate().body("{\"event\": \"deleted\"}").header("action", "delete");
+citrus.expectedMessageCount(3);
+
+template.sendBodyAndHeader("direct:start", "{\"event\": \"created\"}",
"action", "create");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"updated\"}",
"action", "update");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"deleted\"}",
"action", "delete");
+
+citrus.assertIsSatisfied();
+----
+
+You can also use `validate(index)` to target a specific exchange by its
0-based position.
+
+When both `validate(index)` and `validateAll()` are configured, the indexed
spec takes priority —
+it replaces `validateAll()` for that exchange entirely (the two are never
combined). The
+`validateAll()` spec acts as a fallback for exchanges that have no matching
indexed spec.
+
+If an exchange arrives at an index with no matching indexed spec and no
`validateAll()` fallback,
+the endpoint records a failure. For the `validate` operation, the failure is
collected and reported
+when `assertIsSatisfied()` is called (similar to MockEndpoint). For the
`receive` operation, the
+failure is thrown immediately as an exception.
+
+[source,java]
+----
+citrus.validate(0).body("{\"event\": \"start\"}");
+citrus.validateAll().body("{\"event\": \"progress\"}").header("type",
"status");
+
+// Exchange 0 → validated against validate(0) only (validateAll() is NOT
applied)
Review Comment:
⚠️ **`validate(0)` example is wrong — must be `validate(1)`**
As explained above, index 0 is never matched by `MockEndpoint`'s counter.
The first exchange maps to index 1.
```suggestion
citrus.validate(1).body("{"event": "start"}");
```
##########
components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc:
##########
@@ -0,0 +1,409 @@
+= Citrus Component
+:doctitle: Citrus
+:shortname: citrus
+:artifactid: camel-test-citrus
+:description: Citrus message validation component for testing
+:since: 4.24
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The Citrus component integrates with the https://citrusframework.org/[Citrus
integration test framework]
+to perform message validation on exchanges arriving at a Camel endpoint. It
allows test authors to
+verify exchange body content, headers, and path expressions (JsonPath, XPath)
using Citrus validation
+modules.
+
+This component is intended for use in Camel route unit and integration tests.
+
+== Maven dependency
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-citrus</artifactId>
+ <scope>test</scope>
+</dependency>
+----
+
+== URI format
+
+----
+citrus:operation/name[?options]
+----
+
+Where `operation` is `validate` or `receive`, and `name` is an optional
identifier to distinguish
+multiple Citrus endpoints in the same route.
+
+* `validate` — validates exchanges without modifying them (pass-through
validation).
+* `receive` — validates exchanges and returns a simulated response
(request/response simulation).
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+== Usage
+
+The Citrus endpoint acts as a producer-only endpoint. Routes send exchanges
*to* `citrus:validate`,
+and the component validates each exchange using Citrus message validation.
+
+=== Validation modules
+
+The component does not bundle Citrus validation modules. You must add the
required validation
+module dependency for your message type to your test classpath:
+
+[cols="1,2", options="header"]
+|===
+| Dependency | Description
+
+| `citrus-validation-json`
+| JSON message validation with JsonPath expressions and JSON schema support
+
+| `citrus-validation-xml`
+| XML message validation with XPath expressions, XML schema (XSD/WSDL) and
XSLT support
+
+| `citrus-validation-text`
+| Plain text message validation including binary (stream and base64) and gzip
support
+
+| `citrus-validation-yaml`
+| YAML message validation
+
+| `citrus-validation-binary`
+| Binary message validation with stream comparison and base64 encoding
+
+| `citrus-validation-hamcrest`
+| Hamcrest matcher assertions for use with JsonPath and XPath expressions
+|===
+
+All modules use the `org.citrusframework` group ID. Refer to the
+https://citrusframework.org/citrus/reference/html/[Citrus reference
documentation] for detailed
+usage of each validation module.
+
+=== Citrus empowered message validation
+
+Citrus uses a powerful template-based approach to message validation: you
provide an expected message template and
+Citrus compares it against the received message using format-aware comparison.
This brings several
+capabilities beyond simple string equality.
+
+==== Ignore expressions
+
+Use the `@ignore@` placeholder anywhere in an expected message template to
skip validation of dynamic
+values such as timestamps, generated IDs, or correlation tokens. The element
must be present, but its
+value is not checked.
+
+[source,json]
+----
+{
+ "orderId": "@ignore@",
+ "timestamp": "@ignore@",
+ "status": "created"
+}
+----
+
+This works across all supported message types — in XML element and attribute
values, JSON property values,
+and YAML property values. The ignore placeholder can also replace an entire
sub-object or element
+including its children.
+
+For plain text messages, the `@ignore@` placeholder works on a positional
basis — it matches a word-length
+character sequence at the exact position in the text. Use `@ignore(n)@` to
ignore exactly _n_ characters
+when the value contains non-word characters:
+
+[source,plaintext]
+----
+Your current id is "@ignore@", it expires on 01/01/2030
+----
+
+This ignores the word following `id is "` based on the position of the ignore
expression in the template.
+Because no character count is specified, Citrus ignores the entire word
regardless of its length.
+The word boundary is determined by the first non-word character (whitespace,
punctuation, etc.).
+
+[source,plaintext]
+----
+Your current id is "@ignore(12)@", it expires on @ignore(10)@
+----
+
+This ignores the next 12 characters starting at the position of the ignore
expression in the template.
+This also includes whitespace and non-word characters (e.g. `Your current id
is "1234/ 34567 D", it expires on 01/01/2030`).
+
+==== Validation matchers
+
+Citrus validation matchers are inline assertions embedded directly in the
expected message template,
+surrounded by `@` markers. They work across all message formats (JSON, XML,
YAML, plain text) and
+provide richer assertions than exact equality:
+
+[source,json]
+----
+{
+ "name": "@equalsIgnoreCase('JOHN')@",
+ "age": "@greaterThan(17)@",
+ "email": "@matches('[a-z]+@[a-z]+\\.[a-z]+')@",
+ "created": "@matchesDatePattern('yyyy-MM-dd')@",
+ "id": "@isNumber()@",
+ "token": "@startsWith('tk-')@",
+ "status": "@contains('active')@"
+}
+----
+
+You can also use `@variable('name')@` to extract the actual value into a
Citrus test variable for
+later use, and `@assertThat(hamcrestMatcher)@` to use Hamcrest matchers inline
(requires
+`citrus-validation-hamcrest`):
+
+[source,json]
+----
+{
+ "count": "@assertThat(allOf(greaterThan(0), lessThan(100)))@",
+ "type": "@assertThat(anyOf(equalTo(A), equalTo(B)))@"
+}
+----
+
+==== JSON validation
+
+Citrus JSON validation compares JSON object structures regardless of property
order.
+It supports two modes configurable via `citrus.json.message.validation.strict`:
+
+* **Strict mode** (default): the received JSON must match the expected
structure exactly — no
+ additional properties or array items are allowed.
+* **Non-strict mode**: the expected JSON can be a subset of the received
message — additional
+ properties and array items in the received JSON are silently ignored. This
is useful when you
+ only want to verify specific fields.
+
+JSON validation also supports:
+
+* **JsonPath expressions** for targeted field validation without specifying
the full message body
+* **JSON schema validation** against JSON Schema V3/V4 definitions
+* **`@ignore@` and validation matchers** within JSON values
+
+==== XML validation
+
+Citrus XML validation performs a structural DOM tree comparison that correctly
handles:
+
+* **XML namespaces** with different prefixes — the comparison is
namespace-aware, so
+ `<ns1:element xmlns:ns1="...">` and `<ns2:element xmlns:ns2="...">` are
treated as equivalent
+ when they share the same namespace URI.
+* **XML schema validation** against XSD and WSDL definitions to verify
structural correctness.
+* **XPath expressions** for targeted element validation, with full namespace
support via
+ namespace context configuration. This includes automatic namespace prefix
resolution when the
+ prefixes differ between the control template and the received message.
+* **`@ignore@` and validation matchers** within XML element values and
attributes.
+
+==== Plain text validation
+
+Plain text validation performs exact string comparison with support for:
+
+* **Positional `@ignore@`** to skip dynamic word-length sequences at specific
positions.
+* **`@ignore(n)@`** to skip exactly _n_ characters (useful for values with
non-word characters).
+* **`@variable('name')@`** to extract values at specific positions into test
variables.
+* **Whitespace normalization** (configurable) to ignore line separator
differences (CR, LF, CRLF)
+ or all whitespace.
+* **Gzip** and **binary/base64** content comparison via the respective message
types.
+
+==== YAML validation
+
+YAML validation compares YAML documents structurally, similar to JSON
validation:
+
+* **Strict/non-strict mode** — configurable via
`citrus.yaml.message.validation.strict`.
+* **Path expressions** for ignoring specific elements in the YAML structure.
+* **`@ignore@` and validation matchers** within YAML values.
+
+=== Validation specification API
+
+The endpoint provides a fluent API for specifying validation expectations. You
can define a single
+validation that applies to all incoming exchanges, or specify individual
validations for each
+exchange in sequence.
+
+==== Single validation for all exchanges
+
+Use `validateAll()` to define one validation specification that is applied to
every exchange.
+The returned `MessageValidationSpec` provides a fluent API with `body()`,
`header()`, and
+`validate()` methods:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+citrus.validateAll()
+ .body("{\"status\": \"ok\"}")
+ .header("operation", "test");
+citrus.expectedMessageCount(3);
+
+// All three exchanges are validated against the same spec
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}",
"operation", "test");
+
+citrus.assertIsSatisfied();
+----
+
+==== Per-exchange sequential validation
+
+Use `validate()` to specify different validation expectations for the 1st,
2nd, 3rd, etc.
+exchange. Each call to `validate()` auto-increments the index, so specs are
applied in order.
+Each incoming exchange triggers the next validation spec in sequence:
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+
+citrus.validate().body("{\"event\": \"created\"}").header("action", "create");
+citrus.validate().body("{\"event\": \"updated\"}").header("action", "update");
+citrus.validate().body("{\"event\": \"deleted\"}").header("action", "delete");
+citrus.expectedMessageCount(3);
+
+template.sendBodyAndHeader("direct:start", "{\"event\": \"created\"}",
"action", "create");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"updated\"}",
"action", "update");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"deleted\"}",
"action", "delete");
+
+citrus.assertIsSatisfied();
+----
+
+You can also use `validate(index)` to target a specific exchange by its
0-based position.
+
+When both `validate(index)` and `validateAll()` are configured, the indexed
spec takes priority —
+it replaces `validateAll()` for that exchange entirely (the two are never
combined). The
+`validateAll()` spec acts as a fallback for exchanges that have no matching
indexed spec.
+
+If an exchange arrives at an index with no matching indexed spec and no
`validateAll()` fallback,
+the endpoint records a failure. For the `validate` operation, the failure is
collected and reported
+when `assertIsSatisfied()` is called (similar to MockEndpoint). For the
`receive` operation, the
+failure is thrown immediately as an exception.
+
+[source,java]
+----
+citrus.validate(0).body("{\"event\": \"start\"}");
+citrus.validateAll().body("{\"event\": \"progress\"}").header("type",
"status");
+
+// Exchange 0 → validated against validate(0) only (validateAll() is NOT
applied)
+// Exchange 1, 2, ... → validated against validateAll() (body + header)
+----
+
+=== Receive operation (request/response simulation)
+
+The `receive` operation extends the validation endpoint with response
simulation. It uses a
+Gherkin-style fluent API to define what the endpoint expects to receive
(`when`) and what it
+should respond with (`then`). This is useful for simulating backend services
in route tests.
+
+==== Basic usage
+
+[source,java]
+----
+CitrusEndpoint citrus =
context.getEndpoint("citrus:receive/service?messageType=json",
CitrusEndpoint.class);
+
+// Define expected request and simulated response
+citrus.when().allMessages().receive()
Review Comment:
⚠️ **Receive operation docs reference API methods that do not exist in this
commit**
The `WhenSpec` inner class has `receive()`, `receive(int index)`,
`receiveAll()`, and `then()` — but **no `allMessages()` method** and **no
`message(index)` method**. `CitrusEndpoint` has **no top-level `then()`
method** (only `when()`). The Gherkin-style API bulleted list and all code
examples in this section are wrong:
- `when().allMessages().receive()` — `WhenSpec` has no `allMessages()` →
does not compile
- `when().message(0).receive()` — `WhenSpec` has no `message()` → does not
compile
- `then().allMessages().respond()` — `CitrusEndpoint` has no `then()` → does
not compile
- `then().message(0).respond()` — same → does not compile
The correct API is the chained style used in `CitrusReceiveTest.java`:
```java
// All exchanges (same expectation)
citrus.when().receiveAll().body("{"action": "greet"}").then().respond()
.body("{"greeting": "Hello!"}");
// Per-exchange
citrus.when().receive(1).body("{"action": "create"}").then().respond()
.body("{"id": "1", "status": "created"}");
citrus.when().receive(2).body("{"action": "update"}").then().respond()
.body("{"id": "1", "status": "updated"}");
```
The entire "Receive operation" section needs to be rewritten to match the
actual `1188c529` API.
##########
components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusValidationProcessor.java:
##########
@@ -0,0 +1,215 @@
+/*
+ * 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.component.citrus;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.citrusframework.CitrusContext;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.message.DefaultMessage;
+import org.citrusframework.message.MessagePayloadBuilder;
+import org.citrusframework.message.MessagePayloadUtils;
+import org.citrusframework.message.MessageType;
+import org.citrusframework.validation.MessageValidator;
+import org.citrusframework.validation.MessageValidatorRegistry;
+import org.citrusframework.validation.context.DefaultMessageValidationContext;
+import org.citrusframework.validation.context.HeaderValidationContext;
+import org.citrusframework.validation.context.ValidationContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CitrusValidationProcessor implements Processor {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(CitrusValidationProcessor.class);
+
+ private final CitrusContext citrusContext;
+ private final String messageType;
+ private final MessageValidationSpec spec;
+ private final MessageResponseSpec responseSpec;
+
+ CitrusValidationProcessor(CitrusContext citrusContext, String messageType,
+ MessageValidationSpec validationSpec) {
+ this(citrusContext, messageType, validationSpec, null);
+ }
+
+ CitrusValidationProcessor(CitrusContext citrusContext, String messageType,
+ MessageValidationSpec validationSpec,
+ MessageResponseSpec responseSpec) {
+ this.citrusContext = citrusContext;
+ this.messageType = messageType;
+ this.spec = validationSpec;
+ this.responseSpec = responseSpec;
+ }
+
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ TestContext testContext = citrusContext.createTestContext();
+ propagateExchangeVariables(exchange, testContext);
+
+ try {
+ if (spec != null) {
+ DefaultMessage receivedMessage =
buildReceivedMessage(exchange);
+ DefaultMessage controlMessage = buildControlMessage(spec,
testContext);
+
+ String resolvedType = resolveMessageType(spec);
+ controlMessage.setType(resolvedType);
+
+ List<ValidationContext> validationContexts =
buildValidationContexts(spec);
+ performCitrusValidation(receivedMessage, controlMessage,
testContext, validationContexts, resolvedType);
+ }
+
+ if (responseSpec != null) {
+ applyResponse(exchange, responseSpec, testContext);
+ }
+ } finally {
+ writeBackExtractedVariables(exchange, testContext);
+ }
+ }
+
+ // --- Citrus validation helpers ---
+
+ private DefaultMessage buildReceivedMessage(Exchange exchange) {
+ Object body = exchange.getIn().getBody();
+ Map<String, Object> headers = new
LinkedHashMap<>(exchange.getIn().getHeaders());
+ return new DefaultMessage(body, headers);
+ }
+
+ private DefaultMessage buildControlMessage(MessageValidationSpec spec,
TestContext testContext) {
+ Object resolvedBody = resolveBody(spec.getBody(),
spec.getBodyBuilder(), testContext);
+ DefaultMessage controlMessage = resolvedBody != null
+ ? new DefaultMessage(resolvedBody)
+ : new DefaultMessage();
+
+ for (Map.Entry<String, Object> entry : spec.getHeaders().entrySet()) {
+ Object value = entry.getValue();
+ if (value instanceof String s) {
+ value = testContext.replaceDynamicContentInString(s);
+ }
+ controlMessage.setHeader(entry.getKey(), value);
+ }
+ return controlMessage;
+ }
+
+ private String resolveMessageType(MessageValidationSpec spec) {
+ if (messageType != null) {
+ if (MessageType.knows(messageType)) {
+ return
MessageType.valueOf(messageType.toUpperCase(Locale.ROOT)).name();
+ } else {
+ return messageType.toUpperCase(Locale.ROOT);
+ }
+ }
+
+ String expectedBody = spec.getBody();
+ if (expectedBody != null) {
+ if (MessagePayloadUtils.isJson(expectedBody)) {
+ return MessageType.JSON.name();
+ } else if (MessagePayloadUtils.isXml(expectedBody)) {
+ return MessageType.XML.name();
+ } else if (MessagePayloadUtils.isYaml(expectedBody)) {
+ return MessageType.YAML.name();
+ }
+ }
+ return MessageType.PLAINTEXT.name();
+ }
+
+ private List<ValidationContext>
buildValidationContexts(MessageValidationSpec spec) {
+ List<ValidationContext> contexts = new ArrayList<>();
+ contexts.add(new DefaultMessageValidationContext());
+ if (!spec.getHeaders().isEmpty()) {
+ contexts.add(new HeaderValidationContext());
+ }
+ for (ValidationContext.Builder<?, ?> builder :
spec.getValidationContextBuilders()) {
+ contexts.add(builder.build());
+ }
+ return contexts;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private void performCitrusValidation(
+ DefaultMessage receivedMessage, DefaultMessage controlMessage,
+ TestContext testContext, List<ValidationContext>
validationContexts,
+ String resolvedType) {
+
+ MessageValidatorRegistry registry =
testContext.getMessageValidatorRegistry();
+ List<MessageValidator<? extends ValidationContext>> validators
+ = registry.findMessageValidators(resolvedType,
receivedMessage);
+
+ if (validators.isEmpty()) {
+ throw new IllegalStateException(
+ "No Citrus message validator found for message type '" +
resolvedType
+ + "'. Make sure to add the
required Citrus validation module "
+ + "to the test classpath (e.g.
citrus-validation-json, "
+ + "citrus-validation-xml,
citrus-validation-text).");
+ }
+
+ LOG.debug("Validating exchange with {} Citrus validator(s) for message
type '{}'",
+ validators.size(), resolvedType);
+
+ for (MessageValidator validator : validators) {
+ validator.validateMessage(receivedMessage, controlMessage,
testContext, validationContexts);
+ }
+ }
+
+ private void applyResponse(Exchange exchange, MessageResponseSpec
responseSpec, TestContext testContext) {
+ Object resolvedBody = resolveBody(responseSpec.getBody(),
responseSpec.getBodyBuilder(), testContext);
+ if (resolvedBody != null) {
+ exchange.getIn().setBody(resolvedBody);
+ }
+ Map<String, Object> resolvedHeaders =
testContext.resolveDynamicValuesInMap(responseSpec.getHeaders());
+ resolvedHeaders.forEach((key, value) ->
exchange.getIn().setHeader(key, value));
+ }
+
+ private Object resolveBody(String body, MessagePayloadBuilder bodyBuilder,
TestContext testContext) {
+ if (bodyBuilder != null) {
+ return bodyBuilder.buildPayload(testContext);
+ }
+ if (body != null) {
+ return testContext.replaceDynamicContentInString(body);
+ }
+ return null;
+ }
+
+ // --- Variable propagation ---
+
+ private void propagateExchangeVariables(Exchange exchange, TestContext
testContext) {
+ for (Map.Entry<String, Object> entry :
exchange.getProperties().entrySet()) {
+ if (entry.getValue() != null) {
+ testContext.setVariable(entry.getKey(), entry.getValue());
+ }
+ }
+ for (Map.Entry<String, Object> entry :
exchange.getVariables().entrySet()) {
+ if (entry.getValue() != null) {
+ testContext.setVariable(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ private void writeBackExtractedVariables(Exchange exchange, TestContext
testContext) {
+ for (Map.Entry<String, Object> entry :
testContext.getVariables().entrySet()) {
+ String key = entry.getKey();
+ if (!exchange.getProperties().containsKey(key) &&
!exchange.getVariables().containsKey(key)) {
+ exchange.setVariable(key, entry.getValue());
Review Comment:
💡 **`writeBackExtractedVariables` leaks Citrus-internal variables to the
exchange**
This iterates ALL `testContext.getVariables()`, which includes
Citrus-internal entries like `citrus.test.name`, `citrus.test.package`,
`CITRUS_MESSAGES`, etc. The filter (`!containsKey(key)`) only skips keys
already on the exchange — freshly created Citrus-internal keys all leak into
exchange variables, polluting the exchange for downstream processors.
```suggestion
private void writeBackExtractedVariables(Exchange exchange, TestContext
testContext) {
for (Map.Entry<String, Object> entry :
testContext.getVariables().entrySet()) {
String key = entry.getKey();
if (key.startsWith("citrus.") || key.startsWith("CITRUS_")) {
continue;
}
if (!exchange.getProperties().containsKey(key) &&
!exchange.getVariables().containsKey(key)) {
exchange.setVariable(key, entry.getValue());
}
}
}
```
##########
components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusValidationProcessor.java:
##########
@@ -0,0 +1,215 @@
+/*
+ * 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.component.citrus;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.citrusframework.CitrusContext;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.message.DefaultMessage;
+import org.citrusframework.message.MessagePayloadBuilder;
+import org.citrusframework.message.MessagePayloadUtils;
+import org.citrusframework.message.MessageType;
+import org.citrusframework.validation.MessageValidator;
+import org.citrusframework.validation.MessageValidatorRegistry;
+import org.citrusframework.validation.context.DefaultMessageValidationContext;
+import org.citrusframework.validation.context.HeaderValidationContext;
+import org.citrusframework.validation.context.ValidationContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CitrusValidationProcessor implements Processor {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(CitrusValidationProcessor.class);
+
+ private final CitrusContext citrusContext;
+ private final String messageType;
+ private final MessageValidationSpec spec;
+ private final MessageResponseSpec responseSpec;
+
+ CitrusValidationProcessor(CitrusContext citrusContext, String messageType,
+ MessageValidationSpec validationSpec) {
+ this(citrusContext, messageType, validationSpec, null);
+ }
+
+ CitrusValidationProcessor(CitrusContext citrusContext, String messageType,
+ MessageValidationSpec validationSpec,
+ MessageResponseSpec responseSpec) {
+ this.citrusContext = citrusContext;
+ this.messageType = messageType;
+ this.spec = validationSpec;
+ this.responseSpec = responseSpec;
+ }
+
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ TestContext testContext = citrusContext.createTestContext();
+ propagateExchangeVariables(exchange, testContext);
+
+ try {
+ if (spec != null) {
+ DefaultMessage receivedMessage =
buildReceivedMessage(exchange);
+ DefaultMessage controlMessage = buildControlMessage(spec,
testContext);
+
+ String resolvedType = resolveMessageType(spec);
+ controlMessage.setType(resolvedType);
+
+ List<ValidationContext> validationContexts =
buildValidationContexts(spec);
+ performCitrusValidation(receivedMessage, controlMessage,
testContext, validationContexts, resolvedType);
+ }
+
+ if (responseSpec != null) {
+ applyResponse(exchange, responseSpec, testContext);
+ }
+ } finally {
+ writeBackExtractedVariables(exchange, testContext);
+ }
+ }
+
+ // --- Citrus validation helpers ---
+
+ private DefaultMessage buildReceivedMessage(Exchange exchange) {
Review Comment:
⚠️ **`StreamCache` body drain — `validate` operation is no longer
pass-through for stream bodies**
`exchange.getIn().getBody()` without type conversion drains a `StreamCache`
body (common with HTTP/file routes). Since `validate` is a pass-through
operation that should not modify the exchange, any downstream processor will
see an empty body after this call.
Fix: convert to `String` (or check for `StreamCache` and reset after
reading):
```suggestion
private DefaultMessage buildReceivedMessage(Exchange exchange) {
Object body = exchange.getIn().getBody();
if (body instanceof org.apache.camel.StreamCache sc) {
body = exchange.getIn().getBody(String.class);
sc.reset();
}
Map<String, Object> headers = new
LinkedHashMap<>(exchange.getIn().getHeaders());
return new DefaultMessage(body, headers);
}
```
##########
components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusValidationProcessor.java:
##########
@@ -0,0 +1,215 @@
+/*
+ * 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.component.citrus;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.citrusframework.CitrusContext;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.message.DefaultMessage;
+import org.citrusframework.message.MessagePayloadBuilder;
+import org.citrusframework.message.MessagePayloadUtils;
+import org.citrusframework.message.MessageType;
+import org.citrusframework.validation.MessageValidator;
+import org.citrusframework.validation.MessageValidatorRegistry;
+import org.citrusframework.validation.context.DefaultMessageValidationContext;
+import org.citrusframework.validation.context.HeaderValidationContext;
+import org.citrusframework.validation.context.ValidationContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CitrusValidationProcessor implements Processor {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(CitrusValidationProcessor.class);
+
+ private final CitrusContext citrusContext;
+ private final String messageType;
+ private final MessageValidationSpec spec;
+ private final MessageResponseSpec responseSpec;
+
+ CitrusValidationProcessor(CitrusContext citrusContext, String messageType,
+ MessageValidationSpec validationSpec) {
+ this(citrusContext, messageType, validationSpec, null);
+ }
+
+ CitrusValidationProcessor(CitrusContext citrusContext, String messageType,
+ MessageValidationSpec validationSpec,
+ MessageResponseSpec responseSpec) {
+ this.citrusContext = citrusContext;
+ this.messageType = messageType;
+ this.spec = validationSpec;
+ this.responseSpec = responseSpec;
+ }
+
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ TestContext testContext = citrusContext.createTestContext();
+ propagateExchangeVariables(exchange, testContext);
+
+ try {
+ if (spec != null) {
+ DefaultMessage receivedMessage =
buildReceivedMessage(exchange);
+ DefaultMessage controlMessage = buildControlMessage(spec,
testContext);
+
+ String resolvedType = resolveMessageType(spec);
+ controlMessage.setType(resolvedType);
+
+ List<ValidationContext> validationContexts =
buildValidationContexts(spec);
+ performCitrusValidation(receivedMessage, controlMessage,
testContext, validationContexts, resolvedType);
+ }
+
+ if (responseSpec != null) {
+ applyResponse(exchange, responseSpec, testContext);
+ }
+ } finally {
+ writeBackExtractedVariables(exchange, testContext);
+ }
+ }
+
+ // --- Citrus validation helpers ---
+
+ private DefaultMessage buildReceivedMessage(Exchange exchange) {
+ Object body = exchange.getIn().getBody();
Review Comment:
💡 **Case-insensitive Camel headers become case-sensitive in Citrus**
`new LinkedHashMap<>(exchange.getIn().getHeaders())` copies Camel's
`CaseInsensitiveMap` into a case-sensitive `LinkedHashMap`. Citrus header
validation then fails if the validation spec uses a different case than the
actual header key in the received message.
Consider adding `HeaderValidationContext.Builder#ignoreCase(true)` in
`buildValidationContexts()`, or at minimum document this limitation.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]