diff --git a/docs/modules/ROOT/attachments/examples.json b/docs/modules/ROOT/attachments/examples.json
index 8d688d02..99d3d7ff 100644
--- a/docs/modules/ROOT/attachments/examples.json
+++ b/docs/modules/ROOT/attachments/examples.json
@@ -114,6 +114,11 @@
"description": "Shows how to secure platform HTTP with Keycloak",
"link": "https://github.com/apache/camel-quarkus-examples/tree/main/platform-http-security-keycloak"
},
+ {
+ "title": "REST to SOAP with Keycloak and JMS",
+ "description": "Demonstrates REST-to-SOAP synchronous bridging with Keycloak security and asynchronous JMS topic events",
+ "link": "https://github.com/apache/camel-quarkus-examples/tree/main/rest-keycloak-soap-jms"
+ },
{
"title": "REST with Jackson",
"description": "Demonstrates how to create a REST service using the Camel REST DSL and Jackson.",
diff --git a/pom.xml b/pom.xml
index 01aa8cce..c9564387 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
platform-http-security-keycloak
quarkus-rest-json
rest-json
+ rest-keycloak-soap-jms
saga
spring-redis
timer-log
diff --git a/rest-keycloak-soap-jms/.gitignore b/rest-keycloak-soap-jms/.gitignore
new file mode 100644
index 00000000..099afde9
--- /dev/null
+++ b/rest-keycloak-soap-jms/.gitignore
@@ -0,0 +1,18 @@
+target/
+*.log
+.DS_Store
+.classpath
+.project
+.settings/
+.vscode/
+.idea/
+*.iml
+*.iws
+*.ipr
+.gradle/
+build/
+*.swp
+*.swo
+*~
+.env
+.env.local
\ No newline at end of file
diff --git a/rest-keycloak-soap-jms/README.adoc b/rest-keycloak-soap-jms/README.adoc
new file mode 100644
index 00000000..8a6c29b3
--- /dev/null
+++ b/rest-keycloak-soap-jms/README.adoc
@@ -0,0 +1,602 @@
+= REST to SOAP with Keycloak and JMS: A Camel Quarkus example
+:cq-example-description: An example that demonstrates REST-to-SOAP synchronous bridging with Keycloak security and asynchronous JMS topic events
+
+{cq-description}
+
+TIP: Check the https://camel.apache.org/camel-quarkus/latest/first-steps.html[Camel Quarkus User guide] for prerequisites
+and other general information.
+
+== Overview
+
+This example demonstrates how to bridge REST frontends with SOAP backends while securing both with Keycloak authentication.
+It also shows how to use AMQ Broker (Apache ActiveMQ Artemis) for asynchronous event processing without complicating the synchronous request-response flow.
+
+The example includes:
+
+* *REST-to-SOAP bridge*: REST endpoint calls SOAP service synchronously and returns response to client
+* *Async event processing*: Order events are published to JMS topic for multiple independent consumers:
+** Audit logging (compliance trail)
+** Email notifications (simulated)
+** Cache invalidation (consistency pattern)
+* *Unified security*: Both REST (Quarkus OIDC) and SOAP (Camel Keycloak Policy) secured with Keycloak
+* *Admin endpoint*: Demonstrates role-based access control with Keycloak
+
+=== Architecture
+
+[source]
+----
+REST Client
+ ↓ POST /api/orders/submit (with Bearer token)
+REST Endpoint (secured by Quarkus OIDC)
+ ├─→ wireTap (async, fire-and-forget)
+ │ ↓
+ │ JMS Topic: order-events
+ │ ├─→ Audit Logger (Consumer 1)
+ │ ├─→ Email Notification (Consumer 2)
+ │ └─→ Cache Invalidation (Consumer 3)
+ │
+ └─→ SOAP Service (sync, waits for response)
+ ↓
+ SOAP Response (secured by Keycloak Policy)
+ ↓
+ Returns to REST Client
+----
+
+*Key points*:
+- **Synchronous**: REST client gets immediate SOAP response
+- **Asynchronous**: JMS topic broadcasts order events to multiple consumers (audit, email, cache)
+- **Decoupled**: Adding new consumers doesn't change the main REST-to-SOAP flow
+- **Secured**: Both REST and SOAP endpoints require valid Keycloak tokens
+
+*Why this pattern?*
+- Main flow stays simple (direct REST-to-SOAP bridge)
+- Side effects (logging, notifications, cache) don't block response
+- JMS provides message persistence and durable subscriptions
+- Easy to add new event consumers without modifying existing code
+
+== Prerequisites
+
+The example application requires both a Keycloak instance and an AMQ Broker (Apache ActiveMQ Artemis) instance.
+
+You do not need to provide these instances yourself as long as you play with the example code in dev mode
+(a.k.a. `mvn quarkus:dev`) - read more https://quarkus.io/guides/getting-started#development-mode[here]
+or as long as you only run the supplied tests (`mvn test`).
+In those situations, Quarkus tooling automatically starts both Keycloak and Artemis containers for you via
+https://quarkus.io/guides/security-openid-connect-dev-services[Quarkus Dev Services] and
+https://docs.quarkiverse.io/quarkus-artemis/dev/index.html#_dev_services[Artemis Dev Services],
+and it also configures the application so that you do not need to touch anything in `application.properties`.
+
+[[users-configuration]]
+
+=== Users configuration
+
+In all scenarios which we will cover, we will need two users:
+- *customer* (with role `customer-role` and password `customer-pass`) - can submit orders
+- *admin* (with role `admin-role` and password `admin-pass`) - can submit orders and access admin endpoints
+
+WARNING: These hardcoded credentials are for demonstration purposes only and should never be used in production.
+
+=== Quarkus OIDC
+
+We use the approach described in https://quarkus.io/guides/security-openid-connect-client-reference[Quarkus Open ID Connect]
+to protect the application with Keycloak as our OIDC provider. This automatically secures our
+Camel Quarkus routes using the `keycloakPolicy` policy defined in the application.
+
+== Start in Development mode
+
+=== Run the app with Dev Services
+
+Run the application in development mode. Quarkus will automatically start Keycloak and AMQ Broker containers:
+
+[source,shell]
+----
+$ mvn clean compile quarkus:dev
+----
+
+The above command compiles the project, starts the application, and starts both Keycloak and AMQ Broker instances via Dev Services.
+The Quarkus tooling watches for changes in your workspace. Any modifications in your project will automatically take effect
+in the running application.
+
+TIP: Please refer to the Development mode section of
+https://camel.apache.org/camel-quarkus/latest/first-steps.html#_development_mode[Camel Quarkus User guide] for more details.
+
+Now you can move on to the <> section with the assumption that `KEYCLOAK_URL=http://localhost:8082` and `APP_URL=http://localhost:8080`.
+
+[[playground]]
+
+=== Playground
+
+The first thing to do is to obtain the Bearer token from the running Keycloak instance for each created user. Save those tokens for further authentication.
+
+For the `customer` user, extract value from response of key `access_token` and call it `CUSTOMER_TOKEN`:
+
+[source,shell]
+----
+$ curl -d "client_id=quarkus-client" -d "client_secret=secret" -d "username=customer" -d "password=customer-pass" -d "grant_type=password" $KEYCLOAK_URL/realms/quarkus/protocol/openid-connect/token
+----
+
+For the `admin` user, extract value from response of key `access_token` and call it `ADMIN_TOKEN`:
+
+[source,shell]
+----
+$ curl -d "client_id=quarkus-client" -d "client_secret=secret" -d "username=admin" -d "password=admin-pass" -d "grant_type=password" $KEYCLOAK_URL/realms/quarkus/protocol/openid-connect/token
+----
+
+Now we are ready to try the HTTP endpoints:
+
+==== Submit an order (authenticated endpoint)
+
+The `customer` user can submit orders (you should receive `200 OK` with the SOAP response showing updated stock):
+
+[source,shell]
+----
+$ curl -i -X POST -H "Authorization: Bearer $CUSTOMER_TOKEN" -H "Content-Type: application/json" \
+ -d '{"productId":"PRODUCT-001","quantity":5}' \
+ $APP_URL/api/orders/submit
+----
+
+Expected response:
+[source,json]
+----
+{"success":true,"message":"Stock updated successfully","newStock":95}
+----
+
+TIP: Check the application logs to see the asynchronous JMS consumers in action. You should see three types of log entries:
+- `AUDIT: Order event received` - Audit logging consumer
+- `EMAIL: Notification sent` - Email notification consumer (simulated)
+- `CACHE: Invalidated cache for key` - Cache invalidation consumer
+
+These consumers run independently and don't block the REST response.
+
+The `admin` user can also submit orders (same SOAP response format):
+
+[source,shell]
+----
+$ curl -i -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
+ -d '{"productId":"PRODUCT-002","quantity":10}' \
+ $APP_URL/api/orders/submit
+----
+
+Without authentication, the request should fail (you should receive `401 Unauthorized`):
+
+[source,shell]
+----
+$ curl -i -X POST -H "Content-Type: application/json" \
+ -d '{"productId":"PRODUCT-003","quantity":3}' \
+ $APP_URL/api/orders/submit
+----
+
+==== Access admin endpoint (authorized endpoint)
+
+The `customer` user cannot access admin endpoints (you should receive `403 Forbidden`):
+
+[source,shell]
+----
+$ curl -i -X GET -H "Authorization: Bearer $CUSTOMER_TOKEN" $APP_URL/api/admin/users
+----
+
+The `admin` user can access admin endpoints (you should receive `200 OK` with a list of Keycloak users):
+
+[source,shell]
+----
+$ curl -i -X GET -H "Authorization: Bearer $ADMIN_TOKEN" $APP_URL/api/admin/users
+----
+
+==== SOAP Inventory Service
+
+The SOAP endpoint is available at `/services/inventory` and is also secured with Keycloak authentication.
+You can test it using a SOAP client tool like SoapUI or curl with SOAP envelope:
+
+[source,shell]
+----
+$ curl -i -X POST -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: text/xml" \
+ -d '
+
+
+ PRODUCT-001
+ 5
+
+
+ ' \
+ $APP_URL/services/inventory
+----
+
+Expected response:
+[source,xml]
+----
+
+
+
+ true
+ Stock updated successfully
+ 95
+
+
+
+----
+
+[[external-instances-configuration]]
+
+== Prerequisites for externally running Keycloak and AMQ Broker instances
+
+For the next steps, we need to have externally running Keycloak and AMQ Broker instances.
+
+=== Run Keycloak
+
+You can start a Keycloak instance easily via Docker:
+
+[source,shell]
+----
+$ docker run --name keycloak_amq_demo -p 8082:8080 \
+ -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
+ quay.io/keycloak/keycloak:26.6.1 \
+ start-dev
+----
+
+=== Configure the Keycloak realm
+
+Then go to `http://localhost:8082/` click on `Administration Console` and login with `admin:admin`.
+
+Create a new realm named `quarkus` with the following configuration:
+
+*Create a client:*
+- Client ID: `quarkus-client`
+- Client authentication: On
+- Direct access grants: Enabled
+- Client secret: `secret`
+
+*Create two roles:*
+- `customer-role`
+- `admin-role`
+
+*Create two users:*
+- Username: `customer`, Password: `customer-pass`, Email: `customer@example.com`, Role: `customer-role`
+- Username: `admin`, Password: `admin-pass`, Email: `admin@example.com`, Role: `admin-role`
+
+IMPORTANT: For each user, you must set:
+- Email address (required - without it you'll get "Account is not fully set up" error)
+- Email verified: On
+- Password: Set as non-temporary
+- Assign the appropriate role
+
+TIP: Make sure to enable direct access grants for the client to support password grant type.
+
+=== Run AMQ Broker (Apache ActiveMQ Artemis)
+
+You can start an AMQ Broker instance via Docker:
+
+[source,shell]
+----
+$ docker run --name artemis_amq_demo -d -p 61616:61616 -p 8161:8161 \
+ -e AMQ_USER=admin -e AMQ_PASSWORD=admin \
+ quay.io/arkmq-org/activemq-artemis-broker:artemis.2.52.0
+----
+
+The broker will be available at `tcp://localhost:61616` for JMS connections and `http://localhost:8161` for the web console.
+
+== JVM mode
+
+[source,shell]
+----
+$ export QUARKUS_OIDC_AUTH_SERVER_URL=http://localhost:8082/realms/quarkus
+$ export QUARKUS_OIDC_CLIENT_ID=quarkus-client
+$ export QUARKUS_OIDC_CREDENTIALS_SECRET=secret
+$ export QUARKUS_ARTEMIS_URL=tcp://localhost:61616
+$ export QUARKUS_ARTEMIS_USERNAME=admin
+$ export QUARKUS_ARTEMIS_PASSWORD=admin
+$ mvn clean package -DskipTests
+$ java -jar target/quarkus-app/quarkus-run.jar
+----
+
+Now you can go to the <> section (with the assumption that `KEYCLOAK_URL=http://localhost:8082` and `APP_URL=http://localhost:8081`) and try it yourself.
+
+== Native mode
+
+IMPORTANT: Native mode requires having GraalVM and other tools installed. Please check the Prerequisites section
+of https://camel.apache.org/camel-quarkus/latest/first-steps.html#_prerequisites[Camel Quarkus User guide].
+
+To prepare a native executable using GraalVM, run the following command:
+
+[source,shell]
+----
+$ export QUARKUS_OIDC_AUTH_SERVER_URL=http://localhost:8082/realms/quarkus
+$ export QUARKUS_OIDC_CLIENT_ID=quarkus-client
+$ export QUARKUS_OIDC_CREDENTIALS_SECRET=secret
+$ export QUARKUS_ARTEMIS_URL=tcp://localhost:61616
+$ export QUARKUS_ARTEMIS_USERNAME=admin
+$ export QUARKUS_ARTEMIS_PASSWORD=admin
+$ mvn clean package -DskipTests -Dnative
+$ ./target/*-runner
+----
+
+Now you can go to the <> section (with the assumption that `KEYCLOAK_URL=http://localhost:8082` and `APP_URL=http://localhost:8081`) and try it yourself.
+
+== Deploying to Kubernetes
+
+You can build a container image for the application like this. Refer to the https://quarkus.io/guides/deploying-to-kubernetes[Quarkus Kubernetes guide] for options around customizing image names, registries etc.
+
+This example uses Jib to create the container image for Kubernetes deployment.
+
+=== Deploy Keycloak to Kubernetes
+
+For a simple Keycloak deployment on Kubernetes:
+
+[source,shell]
+----
+$ kubectl apply -f - <> to create the realm, client, roles, and users.
+
+IMPORTANT: When creating users via the Keycloak Admin Console or API, you MUST set the `firstName` and `lastName` fields. Without these fields, you will get an "Account is not fully set up" error when trying to authenticate.
+
+For local Docker Desktop Kubernetes testing, Keycloak will be available at `http://localhost:8080` (LoadBalancer service).
+For remote clusters, obtain the Keycloak URL from your ingress or service configuration.
+
+=== Deploy Camel Quarkus application to Kubernetes
+
+TIP: Because we use `quarkus.kubernetes.env.secrets=quarkus-keycloak` in `application.properties` all properties from the secret `quarkus-keycloak` will be presented as ENV variables to the pod.
+
+TIP: To trust self-signed certificates from the Kubernetes API server use `-Dquarkus.kubernetes-client.trust-certs=true` in the deploy command.
+
+**Create the Kubernetes secret:**
+
+[source,shell]
+----
+$ kubectl create secret generic quarkus-keycloak \
+ --from-literal=QUARKUS_OIDC_CREDENTIALS_SECRET=secret \
+ --from-literal=QUARKUS_ARTEMIS_URL=tcp://artemis-broker:61616 \
+ --from-literal=QUARKUS_ARTEMIS_USERNAME=admin \
+ --from-literal=QUARKUS_ARTEMIS_PASSWORD=admin
+----
+
+**Deploy the application:**
+
+For local Docker Desktop Kubernetes (without pushing to a registry):
+
+[source,shell]
+----
+$ mvn clean package -DskipTests -Pkubernetes \
+ -Dquarkus.container-image.build=true \
+ -Dquarkus.container-image.push=false \
+ -Dquarkus.kubernetes.image-pull-policy=Never \
+ -Dquarkus.kubernetes.namespace=default \
+ -Dquarkus.kubernetes.env.vars.QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/quarkus \
+ -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_SERVER_URL=http://keycloak:8080 \
+ -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_REALM=quarkus \
+ -Dquarkus.kubernetes.deploy=true
+----
+
+NOTE: The `-Pkubernetes` profile activates the Quarkus Kubernetes extension for deployment.
+
+For production clusters with a container registry:
+
+[source,shell]
+----
+$ mvn clean package -DskipTests -Pkubernetes \
+ -Dquarkus.container-image.registry= \
+ -Dquarkus.container-image.group= \
+ -Dquarkus.container-image.push=true \
+ -Dquarkus.kubernetes.namespace= \
+ -Dquarkus.kubernetes.env.vars.QUARKUS_OIDC_AUTH_SERVER_URL=http://keycloak:8080/realms/quarkus \
+ -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_SERVER_URL=http://keycloak:8080 \
+ -Dquarkus.kubernetes.env.vars.CAMEL_COMPONENT_KEYCLOAK_REALM=quarkus \
+ -Dquarkus.kubernetes.deploy=true
+----
+
+You can check the pod status:
+
+[source,shell]
+----
+$ kubectl get pods -l app.kubernetes.io/name=camel-quarkus-examples-rest-keycloak-soap-jms
+NAME READY STATUS RESTARTS AGE
+camel-quarkus-examples-rest-keycloak-soap-jms-xxx-xxx 1/1 Running 0 10m
+----
+
+=== Testing the Kubernetes Deployment
+
+IMPORTANT: Due to JWT token issuer validation, the access token must be obtained from within the Kubernetes cluster (using service names like `http://keycloak:8080`) rather than from `localhost`. Tokens obtained from `http://localhost:8080` will be rejected because the issuer URL doesn't match.
+
+**Test from inside the pod:**
+
+[source,shell]
+----
+$ kubectl exec deployment/camel-quarkus-examples-rest-keycloak-soap-jms -- sh -c '
+TOKEN=$(curl -s http://keycloak:8080/realms/quarkus/protocol/openid-connect/token \
+ -d "username=customer" \
+ -d "password=customer-pass" \
+ -d "grant_type=password" \
+ -d "client_id=quarkus-client" \
+ -d "client_secret=secret" | grep -o "\"access_token\":\"[^\"]*" | cut -d\" -f4)
+
+curl -s http://localhost:8081/api/orders/submit \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"productId\":\"PRODUCT-001\",\"quantity\":5}"
+'
+----
+
+Expected response:
+[source,json]
+----
+{"success":true,"message":"Stock updated successfully","newStock":95}
+----
+
+**Test admin endpoint with customer role (should get 403):**
+
+[source,shell]
+----
+$ kubectl exec deployment/camel-quarkus-examples-rest-keycloak-soap-jms -- sh -c '
+CUSTOMER_TOKEN=$(curl -s http://keycloak:8080/realms/quarkus/protocol/openid-connect/token \
+ -d "username=customer" \
+ -d "password=customer-pass" \
+ -d "grant_type=password" \
+ -d "client_id=quarkus-client" \
+ -d "client_secret=secret" | grep -o "\"access_token\":\"[^\"]*" | cut -d\" -f4)
+
+curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8081/api/admin/users \
+ -H "Authorization: Bearer $CUSTOMER_TOKEN"
+'
+----
+
+Expected: `HTTP 403`
+
+**Test admin endpoint with admin role (should get 200):**
+
+[source,shell]
+----
+$ kubectl exec deployment/camel-quarkus-examples-rest-keycloak-soap-jms -- sh -c '
+ADMIN_TOKEN=$(curl -s http://keycloak:8080/realms/quarkus/protocol/openid-connect/token \
+ -d "username=admin" \
+ -d "password=admin-pass" \
+ -d "grant_type=password" \
+ -d "client_id=quarkus-client" \
+ -d "client_secret=secret" | grep -o "\"access_token\":\"[^\"]*" | cut -d\" -f4)
+
+curl -s http://localhost:8081/api/admin/users \
+ -H "Authorization: Bearer $ADMIN_TOKEN"
+'
+----
+
+Expected response:
+[source,json]
+----
+[{"role":"customer-role","email":"customer@example.com","username":"customer"},{"role":"admin-role","email":"admin@example.com","username":"admin"}]
+----
+
+=== Clean up
+
+To clean up all Kubernetes resources:
+
+[source,shell]
+----
+# Delete the application
+$ kubectl delete all -l app.kubernetes.io/name=camel-quarkus-examples-rest-keycloak-soap-jms
+$ kubectl delete secret quarkus-keycloak
+$ kubectl delete ingress camel-quarkus-examples-rest-keycloak-soap-jms
+
+# Delete Keycloak
+$ kubectl delete deployment,service keycloak
+
+# Delete AMQ Broker
+$ kubectl delete deployment,service artemis-broker
+----
+
+== Feedback
+
+Please report bugs and propose improvements via https://github.com/apache/camel-quarkus/issues[GitHub issues of Camel Quarkus] project.
\ No newline at end of file
diff --git a/rest-keycloak-soap-jms/eclipse-formatter-config.xml b/rest-keycloak-soap-jms/eclipse-formatter-config.xml
new file mode 100644
index 00000000..2248b2b8
--- /dev/null
+++ b/rest-keycloak-soap-jms/eclipse-formatter-config.xml
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rest-keycloak-soap-jms/pom.xml b/rest-keycloak-soap-jms/pom.xml
new file mode 100644
index 00000000..d3f2e701
--- /dev/null
+++ b/rest-keycloak-soap-jms/pom.xml
@@ -0,0 +1,443 @@
+
+
+
+ 4.0.0
+
+ camel-quarkus-examples-rest-keycloak-soap-jms
+ org.apache.camel.quarkus.examples
+ 3.37.0-SNAPSHOT
+
+ Camel Quarkus :: Examples :: REST Keycloak SOAP JMS
+ Camel Quarkus Example :: REST to SOAP bridge with Keycloak security and JMS async events
+
+
+ 3.36.1
+ 3.37.0-SNAPSHOT
+
+ io.quarkus
+ quarkus-bom
+ org.apache.camel.quarkus
+ camel-quarkus-bom
+
+ UTF-8
+ UTF-8
+ 17
+
+ 2.29.0
+ 1.13.0
+ 5.0.0
+ 3.15.0
+ 3.5.0
+ 3.3.1
+ 3.5.6
+
+ 3.14.3
+
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ ${quarkus.platform.artifact-id}
+ ${quarkus.platform.version}
+ pom
+ import
+
+
+ ${camel-quarkus.platform.group-id}
+ ${camel-quarkus.platform.artifact-id}
+ ${camel-quarkus.platform.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-direct
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-log
+
+
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-rest
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-jackson
+
+
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-jms
+
+
+ io.quarkiverse.artemis
+ quarkus-artemis-jms
+ ${quarkiverse-artemis.version}
+
+
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-support-jaxb
+
+
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-cxf-soap
+
+
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-bean
+
+
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-http
+
+
+
+
+ io.quarkus
+ quarkus-oidc
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-keycloak
+
+
+
+
+ io.quarkus
+ quarkus-junit
+ test
+
+
+ org.apache.camel.quarkus
+ camel-quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ io.quarkus
+ quarkus-test-keycloak-server
+ test
+
+
+
+
+
+
+
+
+ net.revelc.code.formatter
+ formatter-maven-plugin
+ ${formatter-maven-plugin.version}
+
+ ${maven.multiModuleProjectDirectory}/eclipse-formatter-config.xml
+ LF
+
+
+
+
+ net.revelc.code
+ impsort-maven-plugin
+ ${impsort-maven-plugin.version}
+
+ java.,javax.,org.w3c.,org.xml.,junit.
+ true
+ true
+ java.,javax.,org.w3c.,org.xml.,junit.
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+ true
+ true
+
+ -Xlint:unchecked
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+ false
+
+ org.jboss.logmanager.LogManager
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${maven-surefire-plugin.version}
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ ${maven-jar-plugin.version}
+
+
+
+ com.mycila
+ license-maven-plugin
+ ${license-maven-plugin.version}
+
+ true
+ ${maven.multiModuleProjectDirectory}/header.txt
+
+ **/*.adoc
+ **/*.txt
+ **/LICENSE.txt
+ **/LICENSE
+ **/NOTICE.txt
+ **/NOTICE
+ **/README
+ **/pom.xml.versionsBackup
+ **/quarkus.log*
+ **/target/generated-sources/**
+
+
+ SLASHSTAR_STYLE
+ CAMEL_PROPERTIES_STYLE
+ XML_STYLE
+
+
+ ${maven.multiModuleProjectDirectory}/license-properties-headerdefinition.xml
+
+
+
+
+
+
+ org.apache.cxf
+ cxf-codegen-plugin
+ 4.0.5
+
+
+
+
+
+
+ org.apache.cxf
+ cxf-codegen-plugin
+
+
+ generate-sources
+ generate-sources
+
+ wsdl2java
+
+
+
+
+ ${project.basedir}/src/main/resources/wsdl/InventoryService.wsdl
+
+ org.acme.inventory
+
+
+
+
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+
+
+ build
+
+ build
+
+
+
+
+
+
+ net.revelc.code.formatter
+ formatter-maven-plugin
+
+
+ format
+
+ format
+
+ process-sources
+
+
+
+
+
+ net.revelc.code
+ impsort-maven-plugin
+
+
+ sort-imports
+
+ sort
+
+ process-sources
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+
+
+ license-format
+
+ format
+
+ process-sources
+
+
+
+
+
+
+
+
+ native
+
+
+ native
+
+
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+ ${quarkus.native.enabled}
+
+
+
+
+
+
+
+
+
+ kubernetes
+
+
+ kubernetes
+
+
+
+
+ io.quarkus
+ quarkus-kubernetes
+
+
+ io.quarkus
+ quarkus-container-image-jib
+
+
+
+
+ openshift
+
+
+ openshift
+
+
+
+
+ io.quarkus
+ quarkus-openshift
+
+
+
+
+ skip-testcontainers-tests
+
+
+ skip-testcontainers-tests
+
+
+
+ true
+
+
+
+
+
diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/config/KeycloakConfig.java b/rest-keycloak-soap-jms/src/main/java/org/acme/config/KeycloakConfig.java
new file mode 100644
index 00000000..3a204abc
--- /dev/null
+++ b/rest-keycloak-soap-jms/src/main/java/org/acme/config/KeycloakConfig.java
@@ -0,0 +1,102 @@
+/*
+ * 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.acme.config;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Named;
+import org.acme.inventory.InventoryServicePortType;
+import org.apache.camel.component.cxf.jaxws.CxfEndpoint;
+import org.apache.camel.component.keycloak.security.KeycloakSecurityPolicy;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+@ApplicationScoped
+public class KeycloakConfig {
+
+ private static final String REALMS_PATH = "/realms/";
+
+ @ConfigProperty(name = "quarkus.oidc.auth-server-url")
+ private String authServerUrl;
+
+ @ConfigProperty(name = "quarkus.oidc.client-id")
+ private String clientId;
+
+ @ConfigProperty(name = "quarkus.oidc.credentials.secret", defaultValue = "")
+ private String clientSecret;
+
+ @ConfigProperty(name = "soap.inventory.client.address")
+ private String soapClientAddress;
+
+ @Produces
+ @Named("keycloakPolicy")
+ public KeycloakSecurityPolicy keycloakSecurityPolicy() {
+ // Validate auth-server-url is provided
+ if (authServerUrl == null || authServerUrl.trim().isEmpty()) {
+ throw new IllegalStateException(
+ "quarkus.oidc.auth-server-url is required but was not configured");
+ }
+
+ // Parse auth-server-url to extract serverUrl and realm
+ // Expected format: http://localhost:8082/realms/amq-demo
+ int realmsIndex = authServerUrl.indexOf(REALMS_PATH);
+ if (realmsIndex == -1) {
+ throw new IllegalArgumentException(
+ "Invalid auth-server-url format. Expected format: /realms/, " +
+ "but got: " + authServerUrl);
+ }
+
+ String serverUrl = authServerUrl.substring(0, realmsIndex);
+ String realm = authServerUrl.substring(realmsIndex + REALMS_PATH.length());
+
+ // Validate realm name is not empty
+ if (realm.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Realm name cannot be empty in auth-server-url: " + authServerUrl);
+ }
+
+ KeycloakSecurityPolicy policy = new KeycloakSecurityPolicy();
+ policy.setServerUrl(serverUrl);
+ policy.setRealm(realm);
+ policy.setClientId(clientId);
+ policy.setClientSecret(clientSecret);
+ return policy;
+ }
+
+ @Produces
+ @ApplicationScoped
+ @Named("inventoryService")
+ public CxfEndpoint inventoryService() {
+ // SOAP Server endpoint
+ CxfEndpoint inventoryEndpoint = new CxfEndpoint();
+ inventoryEndpoint.setWsdlURL("wsdl/InventoryService.wsdl");
+ inventoryEndpoint.setServiceClass(InventoryServicePortType.class);
+ inventoryEndpoint.setAddress("/inventory");
+ return inventoryEndpoint;
+ }
+
+ @Produces
+ @ApplicationScoped
+ @Named("inventoryServiceClient")
+ public CxfEndpoint inventoryServiceClient() {
+ // SOAP Client endpoint - calls the local SOAP service
+ CxfEndpoint clientEndpoint = new CxfEndpoint();
+ clientEndpoint.setWsdlURL("wsdl/InventoryService.wsdl");
+ clientEndpoint.setServiceClass(InventoryServicePortType.class);
+ clientEndpoint.setAddress(soapClientAddress);
+ return clientEndpoint;
+ }
+}
diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/model/Order.java b/rest-keycloak-soap-jms/src/main/java/org/acme/model/Order.java
new file mode 100644
index 00000000..68136325
--- /dev/null
+++ b/rest-keycloak-soap-jms/src/main/java/org/acme/model/Order.java
@@ -0,0 +1,49 @@
+/*
+ * 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.acme.model;
+
+import io.quarkus.runtime.annotations.RegisterForReflection;
+
+@RegisterForReflection
+public class Order {
+ private String productId;
+ private Integer quantity;
+
+ public Order() {
+ }
+
+ public Order(String productId, Integer quantity) {
+ this.productId = productId;
+ this.quantity = quantity;
+ }
+
+ public String getProductId() {
+ return productId;
+ }
+
+ public void setProductId(String productId) {
+ this.productId = productId;
+ }
+
+ public Integer getQuantity() {
+ return quantity;
+ }
+
+ public void setQuantity(Integer quantity) {
+ this.quantity = quantity;
+ }
+}
diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/routes/JmsOrderProcessorRoute.java b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/JmsOrderProcessorRoute.java
new file mode 100644
index 00000000..af9160fd
--- /dev/null
+++ b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/JmsOrderProcessorRoute.java
@@ -0,0 +1,64 @@
+/*
+ * 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.acme.routes;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import org.acme.model.Order;
+import org.apache.camel.builder.RouteBuilder;
+
+@ApplicationScoped
+public class JmsOrderProcessorRoute extends RouteBuilder {
+
+ @Override
+ public void configure() {
+ // JMS Consumer 1: Audit logging
+ // Uses durable subscription to survive application restarts
+ from("jms:topic:order-events?clientId=audit-consumer&durableSubscriptionName=audit")
+ .routeId("order-audit-logger")
+ .log("AUDIT: Order event received - ${body}")
+ .to("log:org.acme.audit?level=INFO&showHeaders=false");
+
+ // JMS Consumer 2: Email notification (simulated)
+ // Demonstrates how to add independent consumers without changing main flow
+ from("jms:topic:order-events?clientId=email-consumer&durableSubscriptionName=email")
+ .routeId("order-email-notification")
+ .unmarshal().json(Order.class)
+ .process(exchange -> {
+ Order order = exchange.getIn().getBody(Order.class);
+ String emailBody = String.format(
+ "Order Notification:\n- Product: %s\n- Quantity: %d\n- Status: Processing",
+ order.getProductId(), order.getQuantity());
+ exchange.getIn().setBody(emailBody);
+ })
+ .to("log:org.acme.notification.email?level=INFO")
+ .log("EMAIL: Notification sent for product ${header.productId}");
+
+ // JMS Consumer 3: Cache invalidation (simulated)
+ // Shows pattern for updating cache when inventory changes
+ from("jms:topic:order-events?clientId=cache-consumer&durableSubscriptionName=cache")
+ .routeId("order-cache-invalidation")
+ .unmarshal().json(Order.class)
+ .process(exchange -> {
+ Order order = exchange.getIn().getBody(Order.class);
+ // this product will be removed/invalidated from cache in real scenario (cache-aside)
+ String cacheKey = "inventory:" + order.getProductId();
+ exchange.getIn().setHeader("cacheKey", cacheKey);
+ })
+ .to("log:org.acme.cache?level=INFO")
+ .log("CACHE: Invalidated cache for key ${header.cacheKey}");
+ }
+}
diff --git a/rest-keycloak-soap-jms/src/main/java/org/acme/routes/KeycloakAdminRoute.java b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/KeycloakAdminRoute.java
new file mode 100644
index 00000000..4f7c950f
--- /dev/null
+++ b/rest-keycloak-soap-jms/src/main/java/org/acme/routes/KeycloakAdminRoute.java
@@ -0,0 +1,69 @@
+/*
+ * 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.acme.routes;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.builder.RouteBuilder;
+
+@ApplicationScoped
+public class KeycloakAdminRoute extends RouteBuilder {
+
+ @Override
+ public void configure() {
+ // Error handling for Keycloak operations
+ onException(Exception.class)
+ .handled(true)
+ .setHeader("Content-Type", constant("application/json"))
+ .setHeader("CamelHttpResponseCode", constant(500))
+ .setBody(constant("{\"error\": \"Internal server error\", \"message\": \"${exception.message}\"}"))
+ .log("Error in Keycloak admin route: ${exception.message}");
+
+ rest("/api/admin")
+ .get("/users")
+ .produces("application/json")
+ .to("direct:list-users");
+
+ from("direct:list-users")
+ .routeId("keycloak-admin-users")
+ .log("Admin endpoint accessed - returning user list")
+ .process(exchange -> {
+ // For this example, return a mock user list demonstrating the endpoint works
+ // In production, you would configure proper Keycloak Admin API access
+ List