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 852177c72cc3 chore: camel-jbang - sync the bundled examples:
rest-code-first, dynamic-routing, enrich-and-dedupe and the circuit-breaker
fallback (#28190)
852177c72cc3 is described below
commit 852177c72cc318ca1caa6476887f9d24b03d8f4e
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 18:21:55 2026 +0200
chore: camel-jbang - sync the bundled examples: rest-code-first,
dynamic-routing, enrich-and-dedupe and the circuit-breaker fallback (#28190)
Synced with mvn -Psync-example-catalog generate-resources from
camel-jbang-examples main (#115-#119, #125-#128):
- the catalog lists five new examples: contracts/rest-code-first,
route/dynamic-routing and route/enrich-and-dedupe,
which are bundled, and ai/ticket-classifier and
connect-service/jms-request-reply, which need a broker or a model;
- fail-well/circuit-breaker: the fallback tells a failed call from one the
open breaker did not make;
- ai/langchain4j-chat: the description says the chat model is configured by
provider;
- contracts/openapi-server: the README says what a servers entry without a
path does.
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../examples/camel-jbang-example-catalog.json | 282 ++++++++++++++++++++-
.../examples/contracts/openapi-server/README.md | 4 +-
.../examples/contracts/rest-code-first/Order.java | 30 +++
.../examples/contracts/rest-code-first/README.md | 91 +++++++
.../rest-code-first/application.properties | 20 ++
.../examples/contracts/rest-code-first/orders.json | 5 +
.../rest-code-first/rest-code-first.camel.yaml | 107 ++++++++
.../examples/fail-well/circuit-breaker/README.md | 37 ++-
.../circuit-breaker/circuit-breaker.camel.yaml | 21 +-
.../examples/route/dynamic-routing/README.md | 82 ++++++
.../route/dynamic-routing/application.properties | 16 ++
.../dynamic-routing/dynamic-routing.camel.yaml | 111 ++++++++
.../route/dynamic-routing/orders/order-1001.json | 2 +
.../route/dynamic-routing/orders/order-1002.json | 2 +
.../route/dynamic-routing/orders/order-1003.json | 2 +
.../examples/route/enrich-and-dedupe/README.md | 86 +++++++
.../route/enrich-and-dedupe/application.properties | 19 ++
.../route/enrich-and-dedupe/customers/C-134.json | 1 +
.../route/enrich-and-dedupe/customers/C-207.json | 1 +
.../route/enrich-and-dedupe/customers/C-482.json | 1 +
.../enrich-and-dedupe/enrich-and-dedupe.camel.yaml | 86 +++++++
.../route/enrich-and-dedupe/orders/order-1001.json | 2 +
.../route/enrich-and-dedupe/orders/order-1002.json | 2 +
.../route/enrich-and-dedupe/orders/order-1003.json | 2 +
.../orders/resent-order-1001.json | 2 +
25 files changed, 985 insertions(+), 29 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
index 0be9e36d4066..f4e34cb639de 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
@@ -44,12 +44,12 @@
],
"needs": "a local model",
"ciSkip": true,
- "order": 4
+ "order": 5
},
{
"name": "ai/langchain4j-chat",
"title": "LangChain4j chat",
- "description": "A local Ollama model started with camel infra writes
the shipping notification for each of the three orders; the chat model is a
bean built from properties, the prompt comes from the order, and the log shows
the reply with its token counts.",
+ "description": "A local Ollama model started with camel infra writes
the shipping notification for each of the three orders; the chat model is
configured by provider on the endpoint, the prompt comes from the order, and
the log shows the reply with its token counts.",
"level": "ai",
"teaches": {
"components": [
@@ -138,7 +138,7 @@
"orders/order-1003.json",
"stock.json"
],
- "order": 2
+ "order": 3
},
{
"name": "ai/openai-pii-redaction",
@@ -169,7 +169,60 @@
],
"needs": "an OpenAI-compatible API: set `OPENAI_API_KEY`",
"ciSkip": true,
- "order": 3
+ "order": 4
+ },
+ {
+ "name": "ai/ticket-classifier",
+ "title": "Ticket classifier",
+ "description": "A local model reads each of three support tickets and
names its category, shipping, billing or account; the route checks the answer
and files the ticket in tickets-out/<category>/, and an answer outside the
three, or no answer within the circuit breaker's timeout, files it under
unknown.",
+ "level": "ai",
+ "teaches": {
+ "components": [
+ "langchain4j-chat",
+ "file",
+ "log"
+ ],
+ "eips": [
+ "circuitBreaker",
+ "filter",
+ "setVariable",
+ "setHeader",
+ "unmarshal",
+ "marshal"
+ ],
+ "languages": [
+ "simple",
+ "constant"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "ai",
+ "llm",
+ "ollama",
+ "langchain4j",
+ "classification"
+ ],
+ "bundled": false,
+ "requiresDocker": true,
+ "hasCitrusTests": true,
+ "files": [
+ "README.md",
+ "application.properties",
+ "ticket-classifier.camel.yaml",
+ "tickets/T-1.json",
+ "tickets/T-2.json",
+ "tickets/T-3.json"
+ ],
+ "infraServices": [
+ "ollama"
+ ],
+ "needs": "a local model: `ollama pull granite4:3b`",
+ "ciSkip": true,
+ "order": 2
},
{
"name": "cloud/aws-sqs",
@@ -299,7 +352,7 @@
"infraServices": [
"artemis"
],
- "order": 6
+ "order": 7
},
{
"name": "connect-service/ftp",
@@ -343,7 +396,64 @@
"artemis",
"ftp"
],
- "order": 5
+ "order": 6
+ },
+ {
+ "name": "connect-service/jms-request-reply",
+ "title": "JMS request-reply",
+ "description": "The three orders go on a queue of an ActiveMQ Artemis
broker started with camel infra; a consumer asks a stock service over JMS
request-reply for each, publishes the reserved ones on a topic that shipping
and invoicing both subscribe to, and ORD-1003, which the stock service cannot
reserve, is redelivered three times and then moved to a dead letter queue.",
+ "level": "connect-service",
+ "teaches": {
+ "components": [
+ "jms",
+ "file",
+ "log"
+ ],
+ "eips": [
+ "errorHandler",
+ "deadLetterChannel",
+ "redeliveryPolicy",
+ "filter",
+ "throwException",
+ "setVariable",
+ "setHeader",
+ "unmarshal"
+ ],
+ "languages": [
+ "simple",
+ "jsonpath",
+ "groovy"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "jms",
+ "artemis",
+ "queue",
+ "topic",
+ "request-reply",
+ "dead-letter",
+ "messaging"
+ ],
+ "bundled": false,
+ "requiresDocker": true,
+ "hasCitrusTests": true,
+ "files": [
+ "README.md",
+ "application.properties",
+ "jms-request-reply.camel.yaml",
+ "orders/order-1001.json",
+ "orders/order-1002.json",
+ "orders/order-1003.json",
+ "stock.json"
+ ],
+ "infraServices": [
+ "artemis"
+ ],
+ "order": 3
},
{
"name": "connect-service/kafka-orders",
@@ -392,7 +502,7 @@
"infraServices": [
"kafka"
],
- "order": 4
+ "order": 5
},
{
"name": "connect-service/mqtt",
@@ -433,7 +543,7 @@
"infraServices": [
"mosquitto"
],
- "order": 3
+ "order": 4
},
{
"name": "connect-service/sql",
@@ -657,7 +767,7 @@
"infraServices": [
"keycloak"
],
- "order": 3
+ "order": 4
},
{
"name": "contracts/openapi-client",
@@ -705,7 +815,7 @@
"stock-api.json"
],
"needs": "`contracts/openapi-server` running first",
- "order": 2
+ "order": 3
},
{
"name": "contracts/openapi-server",
@@ -753,12 +863,60 @@
"stock-api.json",
"stock.json"
],
+ "order": 2
+ },
+ {
+ "name": "contracts/rest-code-first",
+ "title": "REST code first",
+ "description": "The shop's order API written with the REST DSL: GET
/orders and GET /orders/{id} answer the orders as JSON (404 for an unknown id),
POST /orders binds the JSON of a new order to a Java record and answers 201
(400 without lines), and the API description generated from the operations is
served at /openapi.",
+ "level": "contracts",
+ "teaches": {
+ "components": [
+ "rest",
+ "platform-http",
+ "direct",
+ "log"
+ ],
+ "eips": [
+ "restConfiguration",
+ "rest",
+ "choice",
+ "filter",
+ "setHeader",
+ "unmarshal"
+ ],
+ "languages": [
+ "simple",
+ "groovy"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "rest",
+ "code-first",
+ "binding",
+ "openapi",
+ "java-record"
+ ],
+ "bundled": true,
+ "requiresDocker": false,
+ "hasCitrusTests": true,
+ "files": [
+ "Order.java",
+ "README.md",
+ "application.properties",
+ "orders.json",
+ "rest-code-first.camel.yaml"
+ ],
"order": 1
},
{
"name": "fail-well/circuit-breaker",
"title": "Circuit breaker",
- "description": "A stock check calls the supplier every second; the
supplier goes down for nine calls, the breaker opens after two failures in its
window of four, answers from the fallback while open, tries the supplier again
after five seconds and closes once a call succeeds; each line logs the breaker
state.",
+ "description": "A stock check calls the supplier every second; the
supplier goes down for nine calls, the breaker opens after two failures in its
window of four, answers from the fallback without calling the supplier while
open, tries it again after five seconds and closes once a call succeeds; each
line logs the breaker state and whether the supplier failed or was not called.",
"level": "fail-well",
"teaches": {
"components": [
@@ -1047,6 +1205,108 @@
],
"order": 1
},
+ {
+ "name": "route/dynamic-routing",
+ "title": "Dynamic routing",
+ "description": "Each of the three orders goes to the warehouse of its
country and to the billing of its status through a recipient list computed from
the order; billing a paid order fails over from a primary invoice service that
is down to a backup one, the pending order gets no invoice, and the US
warehouse is throttled to one order per second.",
+ "level": "route",
+ "teaches": {
+ "components": [
+ "file",
+ "direct",
+ "log"
+ ],
+ "eips": [
+ "recipientList",
+ "loadBalance",
+ "failoverLoadBalancer",
+ "throttle",
+ "throwException",
+ "noErrorHandler",
+ "unmarshal"
+ ],
+ "languages": [
+ "simple",
+ "constant"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "eip",
+ "recipient-list",
+ "failover",
+ "load-balancer",
+ "throttle"
+ ],
+ "bundled": true,
+ "requiresDocker": false,
+ "hasCitrusTests": true,
+ "files": [
+ "README.md",
+ "application.properties",
+ "dynamic-routing.camel.yaml",
+ "orders/order-1001.json",
+ "orders/order-1002.json",
+ "orders/order-1003.json"
+ ],
+ "order": 6
+ },
+ {
+ "name": "route/enrich-and-dedupe",
+ "title": "Enrich and deduplicate",
+ "description": "The three orders and a resent copy of ORD-1001 are
read; an idempotent consumer accepts each order id once and logs the resent one
as a duplicate, each accepted order is enriched with its customer's name and
tier from customers/ with pollEnrich, and a wire tap writes a copy to audit/
without holding the order up.",
+ "level": "route",
+ "teaches": {
+ "components": [
+ "file",
+ "direct",
+ "log"
+ ],
+ "eips": [
+ "idempotentConsumer",
+ "pollEnrich",
+ "wireTap",
+ "choice",
+ "setHeader",
+ "unmarshal",
+ "marshal"
+ ],
+ "languages": [
+ "simple"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "eip",
+ "idempotent",
+ "duplicates",
+ "enrich",
+ "wire-tap",
+ "audit"
+ ],
+ "bundled": true,
+ "requiresDocker": false,
+ "hasCitrusTests": true,
+ "files": [
+ "README.md",
+ "application.properties",
+ "customers/C-134.json",
+ "customers/C-207.json",
+ "customers/C-482.json",
+ "enrich-and-dedupe.camel.yaml",
+ "orders/order-1001.json",
+ "orders/order-1002.json",
+ "orders/order-1003.json",
+ "orders/resent-order-1001.json"
+ ],
+ "order": 5
+ },
{
"name": "route/filter-and-multicast",
"title": "Filter and multicast",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/README.md
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/README.md
index 746aaa90b5fa..2fb9be631c60 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/README.md
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/README.md
@@ -42,8 +42,8 @@ Stop it with `ctrl` + `c`.
## How it works
- `rest` with `openApi: specification: stock-api.json` is the whole API
definition: paths, verbs and parameters
- come from the contract, not from the route file, and the base path `/api`
from its `servers` entry, which
- must have a path. `restConfiguration` switches on `clientRequestValidation`,
+ come from the contract, not from the route file, and the base path `/api`
from its `servers` entry (an entry
+ without a path serves the operations at the root). `restConfiguration`
switches on `clientRequestValidation`,
which rejects a request without the body or content type the contract
requires with 400, and
`apiContextPath: openapi` serves the contract.
- Each operation is a `direct` route named after its `operationId`:
`listStock`, `getStock`, `reserveStock`.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/Order.java
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/Order.java
new file mode 100644
index 000000000000..cc393590bd71
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/Order.java
@@ -0,0 +1,30 @@
+/*
+ * 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 camel.example;
+
+import java.util.List;
+
+/**
+ * An order of the shop, as the JSON of POST /orders binds to it: the REST DSL
turns the request body into this record
+ * (bindingMode json, type camel.example.Order), and the record back into JSON
for the answer.
+ */
+public record Order(String orderId, String customer, String country, String
status, List<Line> lines) {
+
+ /** One line of an order. */
+ public record Line(String sku, int qty, double price) {
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/README.md
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/README.md
new file mode 100644
index 000000000000..fe96d6caa15e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/README.md
@@ -0,0 +1,91 @@
+# REST code first
+
+The shop's order API written with the REST DSL. The routes declare each
operation, the JSON of a new order is bound
+to a Java record, and Camel builds the API description from the operations and
serves it at `/openapi`.
+
+## What you will see
+
+```text
+INFO ... rest-code-first.camel.yaml:101 : Order ORD-1004 created with 1 line(s)
+```
+
+and the answers of the API:
+
+```shell
+$ curl http://localhost:8080/orders/ORD-1002
+{"orderId":"ORD-1002","customer":"C-207","country":"DE","status":"paid","lines":[{"sku":"CAMEL-MUG","qty":3,"price":9.5}]}
+
+$ curl -i http://localhost:8080/orders/ORD-9999
+HTTP/1.1 404 Not Found
+{"error": "unknown order ORD-9999"}
+
+$ curl -i -H 'Content-Type: application/json' -d '{"orderId": "ORD-1004",
"customer": "C-482", "country": "DK", "status": "paid", "lines": [{"sku":
"CAMEL-MUG", "qty": 1, "price": 9.5}]}' http://localhost:8080/orders
+HTTP/1.1 201 Created
+{"orderId":"ORD-1004","customer":"C-482","country":"DK","status":"paid","lines":[{"sku":"CAMEL-MUG","qty":1,"price":9.5}]}
+```
+
+## Install Camel CLI
+
+Install [JBang](https://www.jbang.dev/download/) and the Camel CLI as
described in the
+[root README](../../README.md#install-the-camel-cli); `camel --version`
confirms the install.
+
+## Run it
+
+```shell
+camel run *
+```
+
+Then call the API as shown above. `curl http://localhost:8080/openapi` shows
the API description.
+Stop the example with `ctrl` + `c`.
+
+## How it works
+
+- `rest-code-first.camel.yaml` declares the API with the REST DSL: `GET
/orders`, `GET /orders/{id}` and
+ `POST /orders`, each with an id and a description, and each sent to a
`direct` route.
+- `restConfiguration` sets `bindingMode: json`. The answers are Java objects
(a List, a Map, a record), and the
+ binding writes them as JSON. For `POST /orders`, `type: camel.example.Order`
makes the binding read the request
+ JSON into the record.
+- `Order.java` is the record, with a nested record per line. Simple reads its
fields as `${body.orderId}`.
+- An error answer, 404 or 400, is not bound (`skipBindingOnErrorCode` is on by
default), so the route writes it as
+ JSON text.
+- `apiContextPath: openapi` serves the API description that Camel generates
from the operations.
+ `application.properties` adds `camel-openapi-java` for it, which `camel run`
does not add by itself for a
+ code-first API.
+- `orders.json` holds the three orders of the shop.
+
+## Build it step by step
+
+Ask your assistant, or type it yourself, one step at a time, and run after
each:
+
+1. A REST DSL `GET /orders` that answers the orders from `orders.json`.
+2. `GET /orders/{id}`: the order with that id, or 404 with `{"error": "unknown
order <id>"}`.
+3. `POST /orders` with JSON binding to a Java record `camel.example.Order`:
400 when the order has no lines,
+ otherwise log the order and answer 201.
+4. Serve the API description at `/openapi`.
+
+## Try changing
+
+- `bindingMode: off`: the answers are no longer JSON, and the routes have to
marshal them themselves.
+- Add `PUT /orders/{id}` that changes the status of an order.
+- Compare with the [openapi-server](../openapi-server/) example, which starts
from the contract instead.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/)
YAML DSL,
+`test/rest-code-first.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/rest-code-first.citrus.it.yaml
+```
+
+The test calls each operation and checks the answers, and the API description.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/application.properties
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/application.properties
new file mode 100644
index 000000000000..ee269d643547
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/application.properties
@@ -0,0 +1,20 @@
+# 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.
+
+camel.server.port=8080
+# the API description at /openapi needs camel-openapi-java, which camel run
does not add by itself for code-first REST
+camel.jbang.dependencies=camel:openapi-java
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/orders.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/orders.json
new file mode 100644
index 000000000000..ceea517ee3b5
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/orders.json
@@ -0,0 +1,5 @@
+[
+ {"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status":
"paid", "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku":
"CAMEL-MUG", "qty": 1, "price": 9.5}]},
+ {"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status":
"paid", "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.5}]},
+ {"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status":
"pending", "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku":
"CAMEL-CAP", "qty": 1, "price": 14.0}, {"sku": "CAMEL-MUG", "qty": 2, "price":
9.5}]}
+]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/rest-code-first.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/rest-code-first.camel.yaml
new file mode 100644
index 000000000000..b5f0daa22f4d
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/rest-code-first.camel.yaml
@@ -0,0 +1,107 @@
+# The shop's order API, code first: the REST DSL declares each operation,
binds the JSON of a new order to a Java
+# record, and builds the API description from the operations, served at
/openapi.
+- restConfiguration:
+ bindingMode: json
+ apiContextPath: openapi
+
+- rest:
+ path: /orders
+ get:
+ - id: listOrders
+ description: All orders
+ to:
+ uri: direct:list-orders
+ - id: getOrder
+ path: "/{id}"
+ description: One order, by its id
+ to:
+ uri: direct:get-order
+ post:
+ - id: createOrder
+ description: Creates an order
+ type: camel.example.Order
+ to:
+ uri: direct:create-order
+
+# The answers are Java objects (a List, a Map, a record): the json binding
writes them as JSON
+- route:
+ id: list-orders
+ from:
+ uri: direct
+ parameters:
+ name: list-orders
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: "resource:file:orders.json"
+ - unmarshal:
+ json:
+ library: Jackson
+
+- route:
+ id: get-order
+ from:
+ uri: direct
+ parameters:
+ name: get-order
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: "resource:file:orders.json"
+ - unmarshal:
+ json:
+ library: Jackson
+ - setBody:
+ expression:
+ groovy:
+ expression: "body.find { it.orderId == headers.id }"
+ - filter:
+ expression:
+ simple:
+ expression: "${body} == null"
+ steps:
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "404"
+ # an error answer is not bound to JSON (skipBindingOnErrorCode),
so it is written as JSON text
+ - setBody:
+ expression:
+ simple:
+ expression: '{"error": "unknown order ${header.id}"}'
+
+# The binding has turned the request into a camel.example.Order record
+- route:
+ id: create-order
+ from:
+ uri: direct
+ parameters:
+ name: create-order
+ steps:
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${body.lines} == null ||
${body.lines.isEmpty()} == true"
+ steps:
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "400"
+ - setBody:
+ expression:
+ constant:
+ expression: '{"error": "an order needs at least one
line"}'
+ otherwise:
+ steps:
+ - log:
+ message: "Order ${body.orderId} created with
${body.lines.size()} line(s)"
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "201"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/README.md
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/README.md
index c38d1596fe7e..a688f20dc21a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/README.md
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/README.md
@@ -7,14 +7,18 @@ fallback without calling the supplier, and tries again after
five seconds until
## What you will see
```text
-INFO ... circuit-breaker.camel.yaml:30 : Stock check 1 (breaker CLOSED):
CAMEL-MUG: 47 in stock at the supplier
-INFO ... circuit-breaker.camel.yaml:30 : Stock check 2 (breaker CLOSED):
CAMEL-MUG: 35 in stock at the supplier
-INFO ... circuit-breaker.camel.yaml:30 : Stock check 3 (breaker CLOSED):
CAMEL-MUG: 49 in stock at the supplier
-INFO ... circuit-breaker.camel.yaml:30 : Stock check 4 (breaker CLOSED): no
answer, using last known stock
-INFO ... circuit-breaker.camel.yaml:30 : Stock check 5 (breaker OPEN): no
answer, using last known stock
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 1 (breaker CLOSED):
CAMEL-MUG: 24 in stock at the supplier
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 2 (breaker CLOSED):
CAMEL-MUG: 44 in stock at the supplier
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 3 (breaker CLOSED):
CAMEL-MUG: 31 in stock at the supplier
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 4 (breaker CLOSED):
supplier failed, using last known stock
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 5 (breaker OPEN):
supplier failed, using last known stock
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 6 (breaker OPEN): breaker
open, supplier not called, using last known stock
...
-INFO ... circuit-breaker.camel.yaml:30 : Stock check 14 (breaker OPEN): no
answer, using last known stock
-INFO ... circuit-breaker.camel.yaml:30 : Stock check 15 (breaker CLOSED):
CAMEL-MUG: 58 in stock at the supplier
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 10 (breaker OPEN):
supplier failed, using last known stock
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 11 (breaker OPEN):
breaker open, supplier not called, using last known stock
+...
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 14 (breaker OPEN):
breaker open, supplier not called, using last known stock
+INFO ... circuit-breaker.camel.yaml:43 : Stock check 15 (breaker CLOSED):
CAMEL-MUG: 46 in stock at the supplier
```
## Install Camel CLI
@@ -35,13 +39,18 @@ Stop it with `ctrl` + `c`; the state logged on each line is
the breaker's state
- `circuitBreaker` wraps the call to `direct:supplier` with
[Resilience4j](https://resilience4j.readme.io/):
the breaker counts the last four calls (`slidingWindowSize`) and opens when
half of them failed
(`failureRateThreshold`), which happens on check 5.
-- While open, the steps are not run at all; `onFallback` sets the body
instead. After `waitDurationInOpenState`,
- five seconds, the breaker goes half open and lets one call through
(`permittedNumberOfCallsInHalfOpenState`):
- check 10 still fails, so it opens for another five seconds; check 15
succeeds and closes it.
+- `onFallback` runs both when a call fails and when the open breaker does not
make the call at all, and
+ `CamelCircuitBreakerResponseRejected` tells them apart: `false` on checks 4
and 5, where the supplier was
+ called and failed (the failure on check 5 opens the breaker, so the state
after it is `OPEN`), and `true` on
+ check 6 onwards, where the steps are not run at all. That difference is what
the breaker buys: no call to a
+ service that is known to be down.
+- After `waitDurationInOpenState`, five seconds, the breaker goes half open
and lets one call through
+ (`permittedNumberOfCallsInHalfOpenState`): check 10 is that call and still
fails, so it opens for another five
+ seconds; check 15 succeeds and closes it.
- The supplier is a second route with `noErrorHandler`, so its exception
reaches the breaker; it is down while
the timer counter is 4 to 12.
-- `CamelCircuitBreakerState` is one of the exchange properties the breaker
sets; `CamelCircuitBreakerResponseSuccessfulExecution`
- and `CamelCircuitBreakerResponseFromFallback` are the others.
+- `CamelCircuitBreakerState` and `CamelCircuitBreakerResponseRejected` are
exchange properties the breaker sets;
+ `CamelCircuitBreakerResponseSuccessfulExecution` and
`CamelCircuitBreakerResponseFromFallback` are others.
## Build it step by step
@@ -51,9 +60,13 @@ Stop it with `ctrl` + `c`; the state logged on each line is
the breaker's state
3. Wrap the call in `circuitBreaker` with `onFallback`: no more stack traces,
but the supplier is still called
nine times because the default window is 100 calls.
4. Add the `resilience4jConfiguration` shown here and log
`CamelCircuitBreakerState` to watch it open and close.
+5. Choose the fallback body on `CamelCircuitBreakerResponseRejected`, so the
log shows which checks the breaker
+ saved from calling the supplier.
## Try changing
+- Run `camel get circuit-breaker` in another terminal while it runs: the state
and the success, failure,
+ rejected, fallback and timeout counters of the breaker.
- Set `waitDurationInOpenState` to 2000 and see the breaker try the supplier
sooner.
- Add `timeoutEnabled: true` and `timeoutDuration: 500` and a `delay` of 1000
in the supplier: a slow supplier
counts as a failure too.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/circuit-breaker.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/circuit-breaker.camel.yaml
index 90d73bfaaf16..87f6ef96c301 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/circuit-breaker.camel.yaml
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/circuit-breaker.camel.yaml
@@ -23,10 +23,23 @@
uri: direct:supplier
onFallback:
steps:
- - setBody:
- expression:
- simple:
- expression: "no answer, using last known stock"
+ # rejected: the breaker is open, so the supplier was not
called at all
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression:
"${exchangeProperty.CamelCircuitBreakerResponseRejected} == true"
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "breaker open, supplier not
called, using last known stock"
+ otherwise:
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "supplier failed, using last known
stock"
- log:
message: "Stock check ${header.CamelTimerCounter} (breaker
${exchangeProperty.CamelCircuitBreakerState}): ${body}"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/README.md
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/README.md
new file mode 100644
index 000000000000..16e3c56bb48e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/README.md
@@ -0,0 +1,82 @@
+# Dynamic routing
+
+Where an order goes is computed from the order itself: a recipient list sends
it to the warehouse of its country
+and to the billing of its status. Billing fails over to a backup invoice
service when the primary one is down, and
+the US warehouse takes at most one order per second.
+
+## What you will see
+
+```text
+INFO ... dynamic-routing.camel.yaml:16 : Order ORD-1001 for DK, paid
+INFO ... dynamic-routing.camel.yaml:30 : Warehouse DK picks ORD-1001
+INFO ... dynamic-routing.camel.yaml:110 : Invoice for ORD-1001 via the backup
service
+...
+INFO ... dynamic-routing.camel.yaml:16 : Order ORD-1003 for US, pending
+INFO ... dynamic-routing.camel.yaml:56 : Warehouse US picks ORD-1003
+INFO ... dynamic-routing.camel.yaml:86 : No invoice for ORD-1003 until it is
paid
+```
+
+## Install Camel CLI
+
+Install [JBang](https://www.jbang.dev/download/) and the Camel CLI as
described in the
+[root README](../../README.md#install-the-camel-cli); `camel --version`
confirms the install.
+
+## Run it
+
+```shell
+camel run *
+```
+
+Stop the example with `ctrl` + `c`.
+
+## How it works
+
+- `recipientList` computes the endpoints for each order with Simple:
+ `direct:warehouse-${body[country]},direct:billing-${body[status]}`. ORD-1001
goes to `direct:warehouse-DK` and
+ `direct:billing-paid`, ORD-1003 to `direct:warehouse-US` and
`direct:billing-pending`. Each endpoint is a route.
+- `billing-paid` sends the order through a `failoverLoadBalancer`: the primary
invoice service first, and the backup
+ when the primary fails with an `IllegalStateException`.
`inheritErrorHandler: false` makes it fail over at once,
+ without the route's error handler logging the failure first.
+- `invoice-primary` is down: it throws the exception, and `noErrorHandler`
hands the failure straight back to the
+ load balancer.
+- `throttle` lets at most one order per second into the US warehouse
(`timePeriodMillis: 1000`); an order over the
+ limit waits.
+
+## Build it step by step
+
+Ask your assistant, or type it yourself, one step at a time, and run after
each:
+
+1. A file route on `orders` that unmarshals each order and logs its country
and status.
+2. A recipient list to `direct:warehouse-<country>` and
`direct:billing-<status>`, with a route for each.
+3. Bill a paid order through a failover load balancer: a primary invoice
service that fails, and a backup that
+ takes over.
+4. Throttle the US warehouse to one order per second.
+
+## Try changing
+
+- Copy `order-1003.json` to `order-1004.json` and `order-1005.json` with new
order ids: the US warehouse picks
+ them one second apart.
+- Remove the `throwException` from `invoice-primary`: every invoice goes
through the primary service.
+- Add an order for a country without a warehouse route: the recipient list
fails on the unknown endpoint, unless
+ you set `ignoreInvalidEndpoints: true`.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/)
YAML DSL,
+`test/dynamic-routing.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/dynamic-routing.citrus.it.yaml
+```
+
+The test runs the routes and verifies the warehouses, the failover and the
pending order.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/application.properties
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/application.properties
new file mode 100644
index 000000000000..13a83393a912
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/application.properties
@@ -0,0 +1,16 @@
+# 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.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/dynamic-routing.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/dynamic-routing.camel.yaml
new file mode 100644
index 000000000000..e9f5f7e5e8af
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/dynamic-routing.camel.yaml
@@ -0,0 +1,111 @@
+# Each order goes to the warehouse of its country and to the billing of its
status: a recipient list computes both
+# endpoints from the order. Billing a paid order fails over to a backup
invoice service when the primary one is down,
+# and the US warehouse takes at most one order per second.
+- route:
+ id: incoming-orders
+ from:
+ uri: file
+ parameters:
+ directoryName: orders
+ noop: true
+ sortBy: "file:name"
+ steps:
+ - unmarshal:
+ json:
+ library: Jackson
+ - log:
+ message: "Order ${body[orderId]} for ${body[country]},
${body[status]}"
+ - recipientList:
+ expression:
+ simple:
+ expression:
"direct:warehouse-${body[country]},direct:billing-${body[status]}"
+
+- route:
+ id: warehouse-DK
+ from:
+ uri: direct
+ parameters:
+ name: warehouse-DK
+ steps:
+ - log:
+ message: "Warehouse DK picks ${body[orderId]}"
+
+- route:
+ id: warehouse-DE
+ from:
+ uri: direct
+ parameters:
+ name: warehouse-DE
+ steps:
+ - log:
+ message: "Warehouse DE picks ${body[orderId]}"
+
+# At most one order per second: an order over the limit waits
+- route:
+ id: warehouse-US
+ from:
+ uri: direct
+ parameters:
+ name: warehouse-US
+ steps:
+ - throttle:
+ expression:
+ constant:
+ expression: "1"
+ timePeriodMillis: 1000
+ - log:
+ message: "Warehouse US picks ${body[orderId]}"
+
+# The failover load balancer tries the primary invoice service first, and the
backup when it fails
+- route:
+ id: billing-paid
+ from:
+ uri: direct
+ parameters:
+ name: billing-paid
+ steps:
+ - loadBalance:
+ failoverLoadBalancer:
+ # fail over at once, without the route's error handler logging
the failure first
+ inheritErrorHandler: false
+ exception:
+ - java.lang.IllegalStateException
+ steps:
+ - to:
+ uri: direct:invoice-primary
+ - to:
+ uri: direct:invoice-backup
+
+- route:
+ id: billing-pending
+ from:
+ uri: direct
+ parameters:
+ name: billing-pending
+ steps:
+ - log:
+ message: "No invoice for ${body[orderId]} until it is paid"
+
+# The primary invoice service is down; noErrorHandler hands the failure
straight back to the load balancer
+- route:
+ id: invoice-primary
+ errorHandler:
+ noErrorHandler: {}
+ from:
+ uri: direct
+ parameters:
+ name: invoice-primary
+ steps:
+ - throwException:
+ exceptionType: java.lang.IllegalStateException
+ message: "The invoice service is down"
+
+- route:
+ id: invoice-backup
+ from:
+ uri: direct
+ parameters:
+ name: invoice-backup
+ steps:
+ - log:
+ message: "Invoice for ${body[orderId]} via the backup service"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1001.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1001.json
new file mode 100644
index 000000000000..3eb5f038d636
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku":
"CAMEL-MUG", "qty": 1, "price": 9.50}]}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1002.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1002.json
new file mode 100644
index 000000000000..bb7aa0e9ff1c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1002.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid",
+ "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1003.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1003.json
new file mode 100644
index 000000000000..9edab768cbd7
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/orders/order-1003.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status":
"pending",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku":
"CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price":
9.50}]}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/README.md
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/README.md
new file mode 100644
index 000000000000..541594325463
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/README.md
@@ -0,0 +1,86 @@
+# Enrich and deduplicate
+
+The shop sometimes sends an order twice. An idempotent consumer accepts each
order id once, each accepted order is
+enriched with its customer, and a wire tap writes a copy to an audit trail
without holding the order up.
+
+## What you will see
+
+```text
+INFO ... enrich-and-dedupe.camel.yaml:19 : Order ORD-1001 received
+INFO ... enrich-and-dedupe.camel.yaml:39 : Order ORD-1001 accepted
+INFO ... enrich-and-dedupe.camel.yaml:60 : Order ORD-1001 for Anna Jensen
(gold)
+...
+INFO ... enrich-and-dedupe.camel.yaml:19 : Order ORD-1001 received
+INFO ... enrich-and-dedupe.camel.yaml:35 : Order ORD-1001 is a duplicate,
skipped
+INFO ... enrich-and-dedupe.camel.yaml:85 : Audited ORD-1001.json
+...
+```
+
+## Install Camel CLI
+
+Install [JBang](https://www.jbang.dev/download/) and the Camel CLI as
described in the
+[root README](../../README.md#install-the-camel-cli); `camel --version`
confirms the install.
+
+## Run it
+
+```shell
+camel run *
+```
+
+The copies of the accepted orders land in `audit/`. Stop the example with
`ctrl` + `c`.
+
+## How it works
+
+- `orders/` holds the three orders and `resent-order-1001.json`, the same
ORD-1001 sent a second time.
+ `customers/` holds a file per customer with the name and the tier.
+- The `beans` entry declares `orderIds`, an in-memory idempotent repository:
the set of order ids seen so far.
+- `idempotentConsumer` computes the order id with Simple and checks it against
the repository. With
+ `skipDuplicate: false` a duplicate is not dropped but marked with the
exchange property `CamelDuplicateMessage`,
+ so the `choice` can log it.
+- `pollEnrich` reads the customer's file. The file name is computed into a
header first: the file consumer that
+ `pollEnrich` uses evaluates `fileName` with the headers of the message, not
its body. `variableReceive: customer`
+ keeps the customer in a variable, so the order stays the body, and
`unmarshal` with `variableSend` and
+ `variableReceive` parses the variable in place.
+- `wireTap` sends a copy of the order to the `audit` route and goes on at
once: the audit never holds the order up.
+ The `audit` route marshals the copy to JSON and writes
`audit/<orderId>.json`.
+- `application.properties` declares `audit/` as data (`camel.jbang.dataDir`),
so `camel run --dev` does not reload
+ when the audit writes a file.
+
+## Build it step by step
+
+Ask your assistant, or type it yourself, one step at a time, and run after
each:
+
+1. A file route on `orders` that unmarshals each order and logs "Order ...
received" (four files).
+2. Accept each order id only once with an idempotent consumer and an in-memory
repository, and log the resent
+ order as a duplicate.
+3. Read the customer's file from `customers/` with `pollEnrich` into a
variable, and log the name and the tier.
+4. Copy each accepted order to `audit/<orderId>.json` with a wire tap.
+
+## Try changing
+
+- `skipDuplicate: true`, the default: the duplicate is dropped without a word.
+- Replace the in-memory repository with a file one,
+ `org.apache.camel.support.processor.idempotent.FileIdempotentRepository`
with a `fileStore` property: the ids
+ then survive a restart, and on the next run every order is a duplicate.
+- Add an order for a customer without a file: `pollEnrich` waits for its
`timeout` and the variable stays empty.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/)
YAML DSL,
+`test/enrich-and-dedupe.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/enrich-and-dedupe.citrus.it.yaml
+```
+
+The test runs the routes and verifies the enrichment, the duplicate and the
audit.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/application.properties
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/application.properties
new file mode 100644
index 000000000000..e38c10e165c3
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/application.properties
@@ -0,0 +1,19 @@
+# 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.
+
+# audit/ is written by the example: dev mode does not watch it
+camel.jbang.dataDir=audit
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-134.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-134.json
new file mode 100644
index 000000000000..b1594dcd7e32
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-134.json
@@ -0,0 +1 @@
+{"id": "C-134", "name": "Emily Carter", "tier": "bronze"}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-207.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-207.json
new file mode 100644
index 000000000000..db5d2590a9aa
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-207.json
@@ -0,0 +1 @@
+{"id": "C-207", "name": "Jonas Weber", "tier": "silver"}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-482.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-482.json
new file mode 100644
index 000000000000..0d6881cbe4d1
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/customers/C-482.json
@@ -0,0 +1 @@
+{"id": "C-482", "name": "Anna Jensen", "tier": "gold"}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/enrich-and-dedupe.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/enrich-and-dedupe.camel.yaml
new file mode 100644
index 000000000000..f81fb64b3309
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/enrich-and-dedupe.camel.yaml
@@ -0,0 +1,86 @@
+# Each order id is accepted once: the shop sent ORD-1001 twice
(resent-order-1001.json). An accepted order is
+# enriched with its customer, read from customers/, and a copy goes to an
audit trail without holding the order up.
+- beans:
+ - name: orderIds
+ type:
org.apache.camel.support.processor.idempotent.MemoryIdempotentRepository
+
+- route:
+ id: incoming-orders
+ from:
+ uri: file
+ parameters:
+ directoryName: orders
+ noop: true
+ sortBy: "file:name"
+ steps:
+ - unmarshal:
+ json:
+ library: Jackson
+ - log:
+ message: "Order ${body[orderId]} received"
+ # skipDuplicate false lets a duplicate through, marked with the
CamelDuplicateMessage property
+ - idempotentConsumer:
+ expression:
+ simple:
+ expression: "${body[orderId]}"
+ idempotentRepository: orderIds
+ skipDuplicate: false
+ steps:
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression:
"${exchangeProperty.CamelDuplicateMessage} == true"
+ steps:
+ - log:
+ message: "Order ${body[orderId]} is a duplicate,
skipped"
+ otherwise:
+ steps:
+ - log:
+ message: "Order ${body[orderId]} accepted"
+ # the file name goes in a header: the file consumer of
pollEnrich evaluates fileName with
+ # the headers of the message, not its body; the customer
goes to a variable, so the order
+ # stays the body
+ - setHeader:
+ name: customerFile
+ expression:
+ simple:
+ expression: "${body[customer]}.json"
+ - pollEnrich:
+ expression:
+ simple:
+ expression:
"file:customers?fileName=${header.customerFile}&noop=true&idempotent=false"
+ timeout: 2000
+ variableReceive: customer
+ - unmarshal:
+ variableSend: customer
+ variableReceive: customer
+ json:
+ library: Jackson
+ - log:
+ message: "Order ${body[orderId]} for
${variable.customer[name]} (${variable.customer[tier]})"
+ - wireTap:
+ uri: direct:audit
+
+# Writes a copy of each accepted order to audit/<orderId>.json
+- route:
+ id: audit
+ from:
+ uri: direct
+ parameters:
+ name: audit
+ steps:
+ - setHeader:
+ name: CamelFileName
+ expression:
+ simple:
+ expression: "${body[orderId]}.json"
+ - marshal:
+ json:
+ library: Jackson
+ - to:
+ uri: file
+ parameters:
+ directoryName: audit
+ - log:
+ message: "Audited ${header.CamelFileName}"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1001.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1001.json
new file mode 100644
index 000000000000..3eb5f038d636
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku":
"CAMEL-MUG", "qty": 1, "price": 9.50}]}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1002.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1002.json
new file mode 100644
index 000000000000..bb7aa0e9ff1c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1002.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid",
+ "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1003.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1003.json
new file mode 100644
index 000000000000..9edab768cbd7
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/order-1003.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status":
"pending",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku":
"CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price":
9.50}]}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/resent-order-1001.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/resent-order-1001.json
new file mode 100644
index 000000000000..3eb5f038d636
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/orders/resent-order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku":
"CAMEL-MUG", "qty": 1, "price": 9.50}]}