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-jbang-examples.git
The following commit(s) were added to refs/heads/main by this push:
new f4e4c20 CAMEL-24808: rung 5 of the ladder: connect without a service
(#82)
f4e4c20 is described below
commit f4e4c20916844626c82492eb0025253b6ef83fee
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 18:49:24 2026 +0200
CAMEL-24808: rung 5 of the ladder: connect without a service (#82)
Three examples under connect/: stock-api (the shop's stock REST service on
the CLI's HTTP server, answering one SKU as JSON from a file or 404),
http-client (every order line checked against that service over HTTP, served by
the same example, with the SKU kept in exchange properties so it does not
travel as a header) and file-processing (invoices copied into an inbox by a
courier route, validated, archived by month and moved to done, a bad one to
failed, a note left alone by the inclu [...]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
.github/workflows/build.yml | 6 +
.gitignore | 3 +
camel-jbang-example-catalog.json | 134 +++++++++++++++++++++
connect/file-processing/README.md | 77 ++++++++++++
connect/file-processing/application.properties | 16 +++
connect/file-processing/file-processing.camel.yaml | 56 +++++++++
connect/file-processing/metadata.json | 30 +++++
connect/file-processing/samples/invoice-2001.json | 1 +
connect/file-processing/samples/invoice-2002.json | 1 +
connect/file-processing/samples/invoice-2003.json | 1 +
connect/file-processing/samples/invoice-2004.json | 1 +
connect/file-processing/samples/note-2005.txt | 1 +
.../test/file-processing.citrus.it.yaml | 26 ++++
connect/http-client/README.md | 76 ++++++++++++
connect/http-client/application.properties | 18 +++
connect/http-client/http-client.camel.yaml | 117 ++++++++++++++++++
connect/http-client/metadata.json | 37 ++++++
connect/http-client/orders/order-1001.json | 2 +
connect/http-client/orders/order-1002.json | 2 +
connect/http-client/orders/order-1003.json | 2 +
connect/http-client/stock.json | 5 +
.../http-client/test/http-client.citrus.it.yaml | 21 ++++
connect/stock-api/README.md | 79 ++++++++++++
connect/stock-api/application.properties | 18 +++
connect/stock-api/metadata.json | 33 +++++
connect/stock-api/stock-api.camel.yaml | 73 +++++++++++
connect/stock-api/stock.json | 5 +
connect/stock-api/test/stock-api.citrus.it.yaml | 49 ++++++++
28 files changed, 890 insertions(+)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ed9f2f2..16c00ec 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -62,6 +62,12 @@ jobs:
path: fail-well/error-handling
- name: Circuit breaker
path: fail-well/circuit-breaker
+ - name: Stock API
+ path: connect/stock-api
+ - name: HTTP client
+ path: connect/http-client
+ - name: File processing
+ path: connect/file-processing
- name: OpenAPI server
path: rest/openapi-server
- name: OpenAPI client
diff --git a/.gitignore b/.gitignore
index 6533f23..aaa583b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@ camel-runner.jar
outbox/
# written by the error-handling example at runtime
parked/
+# written by the file-processing example at runtime
+inbox/
+archive/
diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json
index cf84bdc..2d873e1 100644
--- a/camel-jbang-example-catalog.json
+++ b/camel-jbang-example-catalog.json
@@ -341,6 +341,140 @@
"http-to-aws-sqs.camel.yaml"
]
},
+ {
+ "name": "connect/file-processing",
+ "title": "File processing",
+ "description": "A courier route copies five files into an inbox; the
invoices are checked, archived under a month directory and moved to done, the
invoice with a negative amount is rejected with a warning and moved to failed,
and the driver's note is left alone because only .json files are picked up.",
+ "level": "connect",
+ "teaches": {
+ "components": [
+ "file",
+ "log"
+ ],
+ "eips": [
+ "validate",
+ "onException",
+ "unmarshal",
+ "marshal"
+ ],
+ "languages": [
+ "simple"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "file",
+ "inbox",
+ "archive"
+ ],
+ "bundled": false,
+ "requiresDocker": false,
+ "hasCitrusTests": true,
+ "files": [
+ "README.md",
+ "application.properties",
+ "file-processing.camel.yaml",
+ "samples/invoice-2001.json",
+ "samples/invoice-2002.json",
+ "samples/invoice-2003.json",
+ "samples/invoice-2004.json",
+ "samples/note-2005.txt"
+ ]
+ },
+ {
+ "name": "connect/http-client",
+ "title": "HTTP client",
+ "description": "Every line of the three orders is checked against the
stock service over HTTP, served by the same example; the log shows each line as
ok, or back-order when the stock is short, with the stock level from the
response.",
+ "level": "connect",
+ "teaches": {
+ "components": [
+ "file",
+ "http",
+ "rest",
+ "platform-http",
+ "direct",
+ "log"
+ ],
+ "eips": [
+ "split",
+ "setProperty",
+ "toD",
+ "choice",
+ "unmarshal",
+ "marshal"
+ ],
+ "languages": [
+ "simple",
+ "jsonpath"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "http",
+ "rest",
+ "client"
+ ],
+ "bundled": false,
+ "requiresDocker": false,
+ "hasCitrusTests": true,
+ "files": [
+ "README.md",
+ "application.properties",
+ "http-client.camel.yaml",
+ "orders/order-1001.json",
+ "orders/order-1002.json",
+ "orders/order-1003.json",
+ "stock.json"
+ ]
+ },
+ {
+ "name": "connect/stock-api",
+ "title": "Stock API",
+ "description": "The shop's stock service on port 8080: GET /stock
returns the stock file, GET /stock/{sku} returns one SKU as JSON and a 404 with
an error message for an unknown SKU.",
+ "level": "connect",
+ "teaches": {
+ "components": [
+ "rest",
+ "platform-http",
+ "direct"
+ ],
+ "eips": [
+ "setBody",
+ "setHeader",
+ "choice",
+ "marshal"
+ ],
+ "languages": [
+ "constant",
+ "jsonpath",
+ "simple"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "rest",
+ "http",
+ "platform-http"
+ ],
+ "bundled": false,
+ "requiresDocker": false,
+ "hasCitrusTests": false,
+ "files": [
+ "README.md",
+ "application.properties",
+ "stock-api.camel.yaml",
+ "stock.json"
+ ]
+ },
{
"name": "database/sql",
"title": "SQL Database",
diff --git a/connect/file-processing/README.md
b/connect/file-processing/README.md
new file mode 100644
index 0000000..a2d8717
--- /dev/null
+++ b/connect/file-processing/README.md
@@ -0,0 +1,77 @@
+# File processing
+
+The courier's billing system drops invoices into an inbox directory. Each
invoice is checked, archived under a
+month directory and moved to `done`; a bad one goes to `failed`; a file that
is not an invoice is left alone.
+The first route plays the courier: it copies the five files in `samples/` into
`inbox/`.
+
+## What you will see
+
+```text
+INFO ... file-processing.camel.yaml:47 : Invoice INV-2001 for ORD-1001: 49.4
EUR, archived as 2026-09/invoice-2001.json
+INFO ... file-processing.camel.yaml:47 : Invoice INV-2002 for ORD-1002: 28.5
EUR, archived as 2026-09/invoice-2002.json
+WARN ... file-processing.camel.yaml:11 : Invoice invoice-2003.json rejected:
Validation failed for Predicate[bodyOgnl([amount]) > 0]. ...
+ERROR ... DefaultErrorHandler : Failed delivery for (MessageId: ...).
Exhausted after delivery attempt: 1 caught: ...PredicateValidationException: ...
+WARN ... GenericFileOnCompletion : Rollback file strategy:
...GenericFileRenameProcessStrategy
+INFO ... file-processing.camel.yaml:47 : Invoice INV-2004 for ORD-1003: 53.45
EUR, archived as 2026-09/invoice-2004.json
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+Afterwards the directories tell the story: `inbox/done/` holds the three good
invoices, `inbox/failed/` the
+rejected one, `archive/<year-month>/` copies of the good ones, and
`inbox/note-2005.txt` is still where it was.
+Delete `inbox/` and `archive/` to run it again.
+
+## How it works
+
+- The `courier` route is a `file` consumer on `samples/` with `noop: true`
writing to `file:inbox`, a copy.
+- The `invoices` route is the lesson: a `file` consumer on `inbox/` with
`include: .*\.json`, so the driver's
+ note is never picked up; `move: done` for files that went through,
`moveFailed: failed` for files whose
+ exchange failed. Both are directories relative to the inbox.
+- `validate` throws a `PredicateValidationException` when the amount is not
positive; the `onException` logs a
+ warning without a stack trace and lets the exception through, which is what
makes the consumer use `moveFailed`.
+ Had it set `handled: true`, the file would have been moved to `done` as if
it had succeeded.
+- The archive copy uses a `fileName` expression with `${date:now:yyyy-MM}` for
the directory and `${file:name}`
+ for the name; the file component creates directories as needed.
+
+## Build it step by step
+
+1. A `file` route on `inbox` that logs `${file:name}`; drop a file in and see
it logged, then note the
+ `.camel` directory the consumer moved it to by default.
+2. Add `move: done` and `include: .*\.json`, drop the note in and see it
ignored.
+3. Add the `courier` route so the samples arrive by themselves, and
`unmarshal` and log the invoice.
+4. Add `validate` and `moveFailed: failed`, then the `onException` to tidy the
log.
+
+## Try changing
+
+- Add `idempotent: true` with `idempotentKey: ${file:name}` and drop the same
invoice twice.
+- Replace `move: done` with `delete: true`.
+- Point `archive` at a `sftp:` endpoint; the `ftp` example on the next rung
shows how.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/)
YAML DSL,
+`test/file-processing.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/file-processing.citrus.it.yaml
+```
+
+The test starts the routes and verifies the archived and rejected lines.
+
+## 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/connect/file-processing/application.properties
b/connect/file-processing/application.properties
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/connect/file-processing/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/connect/file-processing/file-processing.camel.yaml
b/connect/file-processing/file-processing.camel.yaml
new file mode 100644
index 0000000..fd9446c
--- /dev/null
+++ b/connect/file-processing/file-processing.camel.yaml
@@ -0,0 +1,56 @@
+# The courier's billing system drops invoices into the inbox. Each invoice
+# is checked, archived by month and moved to done; a bad one goes to failed.
+# The courier route plays the courier: it copies the samples into the inbox.
+- onException:
+ exception:
+ - org.apache.camel.support.processor.PredicateValidationException
+ redeliveryPolicy:
+ logStackTrace: false
+ logExhaustedMessageHistory: false
+ steps:
+ - log:
+ loggingLevel: WARN
+ message: "Invoice ${file:name} rejected: ${exception.message}"
+
+- route:
+ id: courier
+ from:
+ uri: file
+ parameters:
+ directoryName: samples
+ noop: true
+ sortBy: "file:name"
+ steps:
+ - to:
+ uri: file
+ parameters:
+ directoryName: inbox
+
+- route:
+ id: invoices
+ from:
+ uri: file
+ parameters:
+ directoryName: inbox
+ include: ".*\\.json"
+ move: done
+ moveFailed: failed
+ sortBy: "file:name"
+ steps:
+ - unmarshal:
+ json:
+ library: Jackson
+ - validate:
+ expression:
+ simple:
+ expression: "${body[amount]} > 0"
+ - log:
+ message: "Invoice ${body[invoiceId]} for ${body[orderId]}:
${body[amount]} EUR, archived as ${date:now:yyyy-MM}/${file:name}"
+ - marshal:
+ json:
+ library: Jackson
+ - to:
+ uri: file
+ parameters:
+ directoryName: archive
+ fileName: "${date:now:yyyy-MM}/${file:name}"
diff --git a/connect/file-processing/metadata.json
b/connect/file-processing/metadata.json
new file mode 100644
index 0000000..577d534
--- /dev/null
+++ b/connect/file-processing/metadata.json
@@ -0,0 +1,30 @@
+{
+ "title": "File processing",
+ "description": "A courier route copies five files into an inbox; the
invoices are checked, archived under a month directory and moved to done, the
invoice with a negative amount is rejected with a warning and moved to failed,
and the driver's note is left alone because only .json files are picked up.",
+ "level": "connect",
+ "teaches": {
+ "components": [
+ "file",
+ "log"
+ ],
+ "eips": [
+ "validate",
+ "onException",
+ "unmarshal",
+ "marshal"
+ ],
+ "languages": [
+ "simple"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "file",
+ "inbox",
+ "archive"
+ ],
+ "bundled": false
+}
diff --git a/connect/file-processing/samples/invoice-2001.json
b/connect/file-processing/samples/invoice-2001.json
new file mode 100644
index 0000000..51d79d5
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2001.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2001", "orderId": "ORD-1001", "amount": 49.40, "currency":
"EUR"}
diff --git a/connect/file-processing/samples/invoice-2002.json
b/connect/file-processing/samples/invoice-2002.json
new file mode 100644
index 0000000..0945236
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2002.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2002", "orderId": "ORD-1002", "amount": 28.50, "currency":
"EUR"}
diff --git a/connect/file-processing/samples/invoice-2003.json
b/connect/file-processing/samples/invoice-2003.json
new file mode 100644
index 0000000..a084e66
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2003.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2003", "orderId": "ORD-1003", "amount": -5.00, "currency":
"EUR"}
diff --git a/connect/file-processing/samples/invoice-2004.json
b/connect/file-processing/samples/invoice-2004.json
new file mode 100644
index 0000000..761c248
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2004.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2004", "orderId": "ORD-1003", "amount": 53.45, "currency":
"EUR"}
diff --git a/connect/file-processing/samples/note-2005.txt
b/connect/file-processing/samples/note-2005.txt
new file mode 100644
index 0000000..e82a2d8
--- /dev/null
+++ b/connect/file-processing/samples/note-2005.txt
@@ -0,0 +1 @@
+Handwritten note from the driver: parcel for ORD-1002 left with the neighbour.
diff --git a/connect/file-processing/test/file-processing.citrus.it.yaml
b/connect/file-processing/test/file-processing.citrus.it.yaml
new file mode 100644
index 0000000..00f2285
--- /dev/null
+++ b/connect/file-processing/test/file-processing.citrus.it.yaml
@@ -0,0 +1,26 @@
+name: file-processing-test
+description: The file-processing example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "file-processing"
+ file: "../file-processing.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "file-processing"
+ logMessage: "Invoice INV-2001 for ORD-1001: 49.4 EUR"
+ - camel:
+ jbang:
+ verify:
+ integration: "file-processing"
+ logMessage: "Invoice invoice-2003.json rejected"
+ - camel:
+ jbang:
+ verify:
+ integration: "file-processing"
+ logMessage: "Invoice INV-2004 for ORD-1003: 53.45 EUR"
diff --git a/connect/http-client/README.md b/connect/http-client/README.md
new file mode 100644
index 0000000..c24e51d
--- /dev/null
+++ b/connect/http-client/README.md
@@ -0,0 +1,76 @@
+# HTTP client
+
+Before an order goes to picking, every line is checked against the stock
service over HTTP. The stock service
+runs inside this same example, so there is nothing else to start: the first
two routes are a trimmed copy of
+`stock-api`, the third is the client.
+
+## What you will see
+
+```text
+INFO ... http-client.camel.yaml:112 : ORD-1001: CAMEL-TSHIRT x 2, 120 in
stock, ok
+INFO ... http-client.camel.yaml:112 : ORD-1001: CAMEL-MUG x 1, 42 in stock, ok
+INFO ... http-client.camel.yaml:112 : ORD-1002: CAMEL-MUG x 3, 42 in stock, ok
+INFO ... http-client.camel.yaml:112 : ORD-1003: CAMEL-TSHIRT x 1, 120 in
stock, ok
+INFO ... http-client.camel.yaml:116 : ORD-1003: CAMEL-CAP x 1, only 0 in
stock, back-order
+INFO ... http-client.camel.yaml:112 : ORD-1003: CAMEL-MUG x 2, 42 in stock, ok
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+The three orders in `orders/` are read once each (`noop: true` leaves the
files in place), so the run can be
+repeated. The stock levels are in `stock.json`.
+
+## How it works
+
+- The client splits each order into lines, as `order-lines` does, and calls
`toD` with
+ `http://localhost:8080/stock/${exchangeProperty.sku}`: `toD` because the URI
is built per message.
+- The SKU, the quantity and the order id are kept in exchange properties, not
headers: headers travel on the
+ HTTP request, and a header named `sku` would collide with the `{sku}` path
parameter on the server side.
+ Properties stay in the route.
+- The body is set to null before the call: a GET has no body, and the order
line would otherwise be sent. The
+ response replaces the body, which is why the line was saved in properties
first.
+- `throwExceptionOnFailure=false` turns a 404 into a normal response with
`CamelHttpResponseCode` set, so the
+ `choice` can log it instead of the error handler.
+
+## Build it step by step
+
+1. Start from `stock-api` and add a `file` route on `orders` that logs each
order.
+2. Split the lines and call `http://localhost:8080/stock/CAMEL-MUG` with a
fixed SKU; log the response body.
+3. Build the URI from the line with `toD` and a property; notice the body is
gone after the call.
+4. Add `throwExceptionOnFailure=false` and the `choice` on the response code
and the quantity.
+
+## Try changing
+
+- Remove `throwExceptionOnFailure=false` and add a SKU that does not exist to
an order: the 404 becomes an
+ `HttpOperationFailedException` handled by the error handler.
+- Call a public API instead, `https://api.github.com/repos/apache/camel` for
example, and log `${body[stargazers_count]}`.
+- Set `camel.server.port` to another port and update the URI to match.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/)
YAML DSL,
+`test/http-client.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/http-client.citrus.it.yaml
+```
+
+The test starts the routes and verifies the ok and back-order lines.
+
+## 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/connect/http-client/application.properties
b/connect/http-client/application.properties
new file mode 100644
index 0000000..5d54c44
--- /dev/null
+++ b/connect/http-client/application.properties
@@ -0,0 +1,18 @@
+# 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
diff --git a/connect/http-client/http-client.camel.yaml
b/connect/http-client/http-client.camel.yaml
new file mode 100644
index 0000000..79f40db
--- /dev/null
+++ b/connect/http-client/http-client.camel.yaml
@@ -0,0 +1,117 @@
+# Before an order goes to picking, every line is checked against the stock
+# service over HTTP. The stock service runs in this same example, so there
+# is nothing else to start.
+- rest:
+ path: /stock
+ get:
+ - path: "/{sku}"
+ to:
+ uri: direct:one-sku
+
+- route:
+ id: stock-service
+ from:
+ uri: direct
+ parameters:
+ name: one-sku
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: resource:file:stock.json
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$[?(@.sku == '${header.sku}')]"
+ resultType: java.util.List
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${body.size()} == 0"
+ steps:
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "404"
+ - setBody:
+ expression:
+ simple:
+ expression: '{"error": "unknown sku ${header.sku}"}'
+ otherwise:
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "${body[0]}"
+ - marshal:
+ json:
+ library: Jackson
+ - setHeader:
+ name: Content-Type
+ expression:
+ constant:
+ expression: application/json
+
+- route:
+ id: check-stock
+ from:
+ uri: file
+ parameters:
+ directoryName: orders
+ noop: true
+ sortBy: "file:name"
+ steps:
+ - unmarshal:
+ json:
+ library: Jackson
+ # exchange properties stay in the route; headers would travel on the
HTTP request
+ - setProperty:
+ name: orderId
+ expression:
+ simple:
+ expression: "${body[orderId]}"
+ - split:
+ expression:
+ simple:
+ expression: "${body[lines]}"
+ steps:
+ - setProperty:
+ name: sku
+ expression:
+ simple:
+ expression: "${body[sku]}"
+ - setProperty:
+ name: needed
+ expression:
+ simple:
+ expression: "${body[qty]}"
+ # a GET has no body, and the response replaces whatever the body
was
+ - setBody:
+ expression:
+ simple:
+ expression: "${null}"
+ - toD:
+ uri:
"http://localhost:8080/stock/${exchangeProperty.sku}?throwExceptionOnFailure=false"
+ - unmarshal:
+ json:
+ library: Jackson
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${header.CamelHttpResponseCode} != 200"
+ steps:
+ - log:
+ message: "${exchangeProperty.orderId}:
${body[error]} (HTTP ${header.CamelHttpResponseCode})"
+ - expression:
+ simple:
+ expression: "${body[qty]} >=
${exchangeProperty.needed}"
+ steps:
+ - log:
+ message: "${exchangeProperty.orderId}:
${exchangeProperty.sku} x ${exchangeProperty.needed}, ${body[qty]} in stock, ok"
+ otherwise:
+ steps:
+ - log:
+ message: "${exchangeProperty.orderId}:
${exchangeProperty.sku} x ${exchangeProperty.needed}, only ${body[qty]} in
stock, back-order"
diff --git a/connect/http-client/metadata.json
b/connect/http-client/metadata.json
new file mode 100644
index 0000000..4b1d7be
--- /dev/null
+++ b/connect/http-client/metadata.json
@@ -0,0 +1,37 @@
+{
+ "title": "HTTP client",
+ "description": "Every line of the three orders is checked against the
stock service over HTTP, served by the same example; the log shows each line as
ok, or back-order when the stock is short, with the stock level from the
response.",
+ "level": "connect",
+ "teaches": {
+ "components": [
+ "file",
+ "http",
+ "rest",
+ "platform-http",
+ "direct",
+ "log"
+ ],
+ "eips": [
+ "split",
+ "setProperty",
+ "toD",
+ "choice",
+ "unmarshal",
+ "marshal"
+ ],
+ "languages": [
+ "simple",
+ "jsonpath"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "http",
+ "rest",
+ "client"
+ ],
+ "bundled": false
+}
diff --git a/connect/http-client/orders/order-1001.json
b/connect/http-client/orders/order-1001.json
new file mode 100644
index 0000000..3eb5f03
--- /dev/null
+++ b/connect/http-client/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/connect/http-client/orders/order-1002.json
b/connect/http-client/orders/order-1002.json
new file mode 100644
index 0000000..bb7aa0e
--- /dev/null
+++ b/connect/http-client/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/connect/http-client/orders/order-1003.json
b/connect/http-client/orders/order-1003.json
new file mode 100644
index 0000000..9edab76
--- /dev/null
+++ b/connect/http-client/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/connect/http-client/stock.json b/connect/http-client/stock.json
new file mode 100644
index 0000000..efab5de
--- /dev/null
+++ b/connect/http-client/stock.json
@@ -0,0 +1,5 @@
+[
+ {"sku": "CAMEL-TSHIRT", "qty": 120},
+ {"sku": "CAMEL-MUG", "qty": 42},
+ {"sku": "CAMEL-CAP", "qty": 0}
+]
diff --git a/connect/http-client/test/http-client.citrus.it.yaml
b/connect/http-client/test/http-client.citrus.it.yaml
new file mode 100644
index 0000000..1d40c02
--- /dev/null
+++ b/connect/http-client/test/http-client.citrus.it.yaml
@@ -0,0 +1,21 @@
+name: http-client-test
+description: The http-client example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "http-client"
+ file: "../http-client.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "http-client"
+ logMessage: "ORD-1001: CAMEL-TSHIRT x 2, 120 in stock, ok"
+ - camel:
+ jbang:
+ verify:
+ integration: "http-client"
+ logMessage: "ORD-1003: CAMEL-CAP x 1, only 0 in stock, back-order"
diff --git a/connect/stock-api/README.md b/connect/stock-api/README.md
new file mode 100644
index 0000000..eeb0e75
--- /dev/null
+++ b/connect/stock-api/README.md
@@ -0,0 +1,79 @@
+# Stock API
+
+The shop's stock service: what the warehouse has on the shelf, per SKU. A REST
API with two operations, served by
+the HTTP server built into the Camel CLI, answering from a JSON file.
+
+## What you will see
+
+```text
+$ curl localhost:8080/stock/CAMEL-MUG
+{"sku":"CAMEL-MUG","qty":42}
+
+$ curl -i localhost:8080/stock/CAMEL-SOCKS
+HTTP/1.1 404 Not Found
+{"error": "unknown sku CAMEL-SOCKS"}
+
+$ curl localhost:8080/stock
+[
+ {"sku": "CAMEL-TSHIRT", "qty": 120},
+ {"sku": "CAMEL-MUG", "qty": 42},
+ {"sku": "CAMEL-CAP", "qty": 0}
+]
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+Then, in another terminal, call it with `curl` as above. Stop it with `ctrl` +
`c`.
+
+## How it works
+
+- `rest` with `path: /stock` declares the API: a `get` with no path for the
list, a `get` with `/{sku}` for one
+ item. Each operation hands over to a `direct` route. The CLI starts the HTTP
server on port 8080 because the
+ file uses the REST DSL; `application.properties` sets the port.
+- The list route sets the body to `resource:file:stock.json`, the file next to
the route, and the content type.
+- The item route loads the same file and picks the item with a `jsonpath`
filter, `$[?(@.sku == '...')]`, in which
+ the `{sku}` path parameter arrives as the header `sku`. The result is a
list: empty means 404 with a small error
+ body, otherwise the first element is marshalled back to JSON.
+- `CamelHttpResponseCode` is the header that sets the status code; 200 is the
default.
+
+## Build it step by step
+
+1. A `rest` with one `get` that answers `resource:file:stock.json`; run it and
`curl localhost:8080/stock`.
+2. Add the `/{sku}` operation and log `${header.sku}` to see the path
parameter arrive.
+3. Filter the file with `jsonpath` and return the first element; `curl` a
known SKU.
+4. Add the `choice` for the empty result and the 404.
+
+## Try changing
+
+- Add `POST /stock/{sku}` that logs the body: `curl -X POST -d '{"qty": 5}'
localhost:8080/stock/CAMEL-CAP`.
+- Change the port in `application.properties` and see the CLI pick it up.
+- The `http-client` example calls this service from another route.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/)
YAML DSL,
+`test/stock-api.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/stock-api.citrus.it.yaml
+```
+
+The test starts the service and calls both operations over HTTP, checking the
JSON and the 404.
+
+## 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/connect/stock-api/application.properties
b/connect/stock-api/application.properties
new file mode 100644
index 0000000..5d54c44
--- /dev/null
+++ b/connect/stock-api/application.properties
@@ -0,0 +1,18 @@
+# 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
diff --git a/connect/stock-api/metadata.json b/connect/stock-api/metadata.json
new file mode 100644
index 0000000..1f9704a
--- /dev/null
+++ b/connect/stock-api/metadata.json
@@ -0,0 +1,33 @@
+{
+ "title": "Stock API",
+ "description": "The shop's stock service on port 8080: GET /stock returns
the stock file, GET /stock/{sku} returns one SKU as JSON and a 404 with an
error message for an unknown SKU.",
+ "level": "connect",
+ "teaches": {
+ "components": [
+ "rest",
+ "platform-http",
+ "direct"
+ ],
+ "eips": [
+ "setBody",
+ "setHeader",
+ "choice",
+ "marshal"
+ ],
+ "languages": [
+ "constant",
+ "jsonpath",
+ "simple"
+ ],
+ "dataformats": [
+ "json"
+ ]
+ },
+ "tags": [
+ "shop",
+ "rest",
+ "http",
+ "platform-http"
+ ],
+ "bundled": false
+}
diff --git a/connect/stock-api/stock-api.camel.yaml
b/connect/stock-api/stock-api.camel.yaml
new file mode 100644
index 0000000..b5464b8
--- /dev/null
+++ b/connect/stock-api/stock-api.camel.yaml
@@ -0,0 +1,73 @@
+# The shop's stock service: what the warehouse has on the shelf, per SKU.
+# GET /stock lists everything, GET /stock/{sku} answers for one SKU or 404.
+- rest:
+ path: /stock
+ get:
+ - to:
+ uri: direct:all-stock
+ - path: "/{sku}"
+ to:
+ uri: direct:one-sku
+
+- route:
+ id: all-stock
+ from:
+ uri: direct
+ parameters:
+ name: all-stock
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: resource:file:stock.json
+ - setHeader:
+ name: Content-Type
+ expression:
+ constant:
+ expression: application/json
+
+- route:
+ id: one-sku
+ from:
+ uri: direct
+ parameters:
+ name: one-sku
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: resource:file:stock.json
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$[?(@.sku == '${header.sku}')]"
+ resultType: java.util.List
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${body.size()} == 0"
+ steps:
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "404"
+ - setBody:
+ expression:
+ simple:
+ expression: '{"error": "unknown sku ${header.sku}"}'
+ otherwise:
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "${body[0]}"
+ - marshal:
+ json:
+ library: Jackson
+ - setHeader:
+ name: Content-Type
+ expression:
+ constant:
+ expression: application/json
diff --git a/connect/stock-api/stock.json b/connect/stock-api/stock.json
new file mode 100644
index 0000000..efab5de
--- /dev/null
+++ b/connect/stock-api/stock.json
@@ -0,0 +1,5 @@
+[
+ {"sku": "CAMEL-TSHIRT", "qty": 120},
+ {"sku": "CAMEL-MUG", "qty": 42},
+ {"sku": "CAMEL-CAP", "qty": 0}
+]
diff --git a/connect/stock-api/test/stock-api.citrus.it.yaml
b/connect/stock-api/test/stock-api.citrus.it.yaml
new file mode 100644
index 0000000..7d653d2
--- /dev/null
+++ b/connect/stock-api/test/stock-api.citrus.it.yaml
@@ -0,0 +1,49 @@
+name: stock-api-test
+description: The stock API answers one SKU as JSON and 404 for an unknown one
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "stock-api"
+ file: "../stock-api.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "stock.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "stock-api"
+ logMessage: "(stock-api) started"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/stock/CAMEL-MUG"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ contentType: "application/json"
+ validate:
+ - jsonPath:
+ - expression: "$.sku"
+ value: "CAMEL-MUG"
+ - expression: "$.qty"
+ value: "42"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/stock/CAMEL-SOCKS"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "404"
+ validate:
+ - jsonPath:
+ - expression: "$.error"
+ value: "unknown sku CAMEL-SOCKS"