This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26502-bonita-json-text-body
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c803235c21e25efeca8c2b31d146b2a585222a9c
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 21:25:22 2026 +0100

    CAMEL-26502: camel-bonita - startCase accepts a JSON text body as the 
contract inputs
    
    Since CAMEL-26120 a JSON text body failed with an InvalidPayloadException 
and
    the docs said to unmarshal it to a Map first. startCase now parses a JSON
    object given as String, byte[] or InputStream into the Map of contract 
inputs
    with Jackson, as camel-wordpress does since CAMEL-26163. JSON text that is 
not
    an object (array, scalar, null, empty) and text that is not JSON still fail
    with an InvalidPayloadException, now with a cause that says why. Body types,
    component doc and catalog updated.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/components/bonita.json    |  4 +-
 .../camel/catalog/docs/bonita-component.adoc       |  8 +-
 components/camel-bonita/pom.xml                    |  4 +
 .../org/apache/camel/component/bonita/bonita.json  |  4 +-
 .../src/main/docs/bonita-component.adoc            |  8 +-
 .../camel/component/bonita/BonitaEndpoint.java     | 14 +--
 .../bonita/producer/BonitaStartProducer.java       | 40 ++++++++-
 .../component/bonita/BonitaStartProducerTest.java  | 99 ++++++++++++++++++++--
 8 files changed, 151 insertions(+), 30 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/bonita.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/bonita.json
index 0d94c987e055..b9053878602f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/bonita.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/bonita.json
@@ -31,8 +31,8 @@
     "CamelBonitaCaseId": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The id of the case that the startCase 
operation started.", "constantName": 
"org.apache.camel.component.bonita.BonitaConstants#CASE_ID" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "map" ], "description": "The contract inputs 
of the process: a Map of the input names and their Serializable values, a 
FileInput for a file to upload. Any other body, also JSON text, fails with an 
InvalidPayloadException: unmarshal it to a Map first" },
-    "producerOut": { "carriers": [ "unchanged" ], "description": "The Map of 
the inputs, in which each FileInput is replaced by a Map of its filename and 
the tempPath of the upload. The id of the case started is in the 
CamelBonitaCaseId header" }
+    "producerIn": { "carriers": [ "map", "text", "bytes", "stream" ], 
"description": "The contract inputs of the process: a Map of the input names 
and their Serializable values, a FileInput for a file to upload, or a JSON 
object as text that is parsed into that Map with Jackson. Any other body, also 
JSON text that is not an object, fails with an InvalidPayloadException" },
+    "producerOut": { "carriers": [ "unchanged" ], "description": "The body as 
sent: a Map of the inputs has each FileInput replaced by a Map of its filename 
and the tempPath of the upload. The id of the case started is in the 
CamelBonitaCaseId header" }
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "", "required": true, "type": "enum", "javaType": 
"org.apache.camel.component.bonita.util.BonitaOperation", "enum": [ "startCase" 
], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "configurationClass": 
"org.apache.camel.component.bonita.BonitaConfiguration", "configurationField": 
"configuration", "description": "Operation to use" },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/bonita-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/bonita-component.adoc
index 2ebe66b71ae8..f99e83c5ebd6 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/bonita-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/bonita-component.adoc
@@ -33,11 +33,11 @@ include::partial$component-endpoint-headers.adoc[]
 === Body content
 
 For the startCase operation, the input variables are retrieved from the body 
message.
-This one has to contain a `Map<String,Serializable>`.
-Any other body, also JSON text, fails with an `InvalidPayloadException`: 
unmarshal JSON text to a `Map` first
-(for example with `unmarshal().json()`).
+This one has to contain a `Map<String,Serializable>`, or a JSON object as text 
(`String`, `byte[]` or `InputStream`)
+such as `{"amount": 1}`, which is parsed into that `Map`.
+Any other body, also JSON text that is not an object (such as a JSON array), 
fails with an `InvalidPayloadException`.
 
-After the operation, the body is still the `Map` of the inputs.
+After the operation, the body is still the `Map` of the inputs, or the JSON 
text that was sent.
 The id of the started case is set in the `CamelBonitaCaseId` header.
 
 
diff --git a/components/camel-bonita/pom.xml b/components/camel-bonita/pom.xml
index a9b7769eaa25..ecd08fc36814 100644
--- a/components/camel-bonita/pom.xml
+++ b/components/camel-bonita/pom.xml
@@ -52,6 +52,10 @@
             <artifactId>cxf-rt-frontend-jaxrs</artifactId>
             <version>${cxf-version}</version>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
             <artifactId>jackson-jakarta-rs-json-provider</artifactId>
diff --git 
a/components/camel-bonita/src/generated/resources/META-INF/org/apache/camel/component/bonita/bonita.json
 
b/components/camel-bonita/src/generated/resources/META-INF/org/apache/camel/component/bonita/bonita.json
index 0d94c987e055..b9053878602f 100644
--- 
a/components/camel-bonita/src/generated/resources/META-INF/org/apache/camel/component/bonita/bonita.json
+++ 
b/components/camel-bonita/src/generated/resources/META-INF/org/apache/camel/component/bonita/bonita.json
@@ -31,8 +31,8 @@
     "CamelBonitaCaseId": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The id of the case that the startCase 
operation started.", "constantName": 
"org.apache.camel.component.bonita.BonitaConstants#CASE_ID" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "map" ], "description": "The contract inputs 
of the process: a Map of the input names and their Serializable values, a 
FileInput for a file to upload. Any other body, also JSON text, fails with an 
InvalidPayloadException: unmarshal it to a Map first" },
-    "producerOut": { "carriers": [ "unchanged" ], "description": "The Map of 
the inputs, in which each FileInput is replaced by a Map of its filename and 
the tempPath of the upload. The id of the case started is in the 
CamelBonitaCaseId header" }
+    "producerIn": { "carriers": [ "map", "text", "bytes", "stream" ], 
"description": "The contract inputs of the process: a Map of the input names 
and their Serializable values, a FileInput for a file to upload, or a JSON 
object as text that is parsed into that Map with Jackson. Any other body, also 
JSON text that is not an object, fails with an InvalidPayloadException" },
+    "producerOut": { "carriers": [ "unchanged" ], "description": "The body as 
sent: a Map of the inputs has each FileInput replaced by a Map of its filename 
and the tempPath of the upload. The id of the case started is in the 
CamelBonitaCaseId header" }
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "", "required": true, "type": "enum", "javaType": 
"org.apache.camel.component.bonita.util.BonitaOperation", "enum": [ "startCase" 
], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "configurationClass": 
"org.apache.camel.component.bonita.BonitaConfiguration", "configurationField": 
"configuration", "description": "Operation to use" },
diff --git a/components/camel-bonita/src/main/docs/bonita-component.adoc 
b/components/camel-bonita/src/main/docs/bonita-component.adoc
index 2ebe66b71ae8..f99e83c5ebd6 100644
--- a/components/camel-bonita/src/main/docs/bonita-component.adoc
+++ b/components/camel-bonita/src/main/docs/bonita-component.adoc
@@ -33,11 +33,11 @@ include::partial$component-endpoint-headers.adoc[]
 === Body content
 
 For the startCase operation, the input variables are retrieved from the body 
message.
-This one has to contain a `Map<String,Serializable>`.
-Any other body, also JSON text, fails with an `InvalidPayloadException`: 
unmarshal JSON text to a `Map` first
-(for example with `unmarshal().json()`).
+This one has to contain a `Map<String,Serializable>`, or a JSON object as text 
(`String`, `byte[]` or `InputStream`)
+such as `{"amount": 1}`, which is parsed into that `Map`.
+Any other body, also JSON text that is not an object (such as a JSON array), 
fails with an `InvalidPayloadException`.
 
-After the operation, the body is still the `Map` of the inputs.
+After the operation, the body is still the `Map` of the inputs, or the JSON 
text that was sent.
 The id of the started case is set in the `CamelBonitaCaseId` header.
 
 
diff --git 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/BonitaEndpoint.java
 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/BonitaEndpoint.java
index 3242cdff7d32..c36202d462a9 100644
--- 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/BonitaEndpoint.java
+++ 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/BonitaEndpoint.java
@@ -38,15 +38,15 @@ import org.apache.camel.support.DefaultEndpoint;
  */
 @UriEndpoint(firstVersion = "2.19.0", scheme = "bonita", title = "Bonita", 
syntax = "bonita:operation", producerOnly = true,
              category = { Category.WORKFLOW }, headersClass = 
BonitaConstants.class)
-@BodyTypes(producerIn = @BodyType(value = Carrier.MAP,
+@BodyTypes(producerIn = @BodyType(value = { Carrier.MAP, Carrier.TEXT, 
Carrier.BYTES, Carrier.STREAM },
                                   description = "The contract inputs of the 
process: a Map of the input names and their"
-                                                + " Serializable values, a 
FileInput for a file to upload. Any other body,"
-                                                + " also JSON text, fails with 
an InvalidPayloadException: unmarshal it to a"
-                                                + " Map first"),
+                                                + " Serializable values, a 
FileInput for a file to upload, or a JSON object as"
+                                                + " text that is parsed into 
that Map with Jackson. Any other body, also JSON"
+                                                + " text that is not an 
object, fails with an InvalidPayloadException"),
            producerOut = @BodyType(value = Carrier.UNCHANGED,
-                                   description = "The Map of the inputs, in 
which each FileInput is replaced by a Map of its"
-                                                 + " filename and the tempPath 
of the upload. The id of the case started is"
-                                                 + " in the CamelBonitaCaseId 
header"))
+                                   description = "The body as sent: a Map of 
the inputs has each FileInput replaced by a Map"
+                                                 + " of its filename and the 
tempPath of the upload. The id of the case"
+                                                 + " started is in the 
CamelBonitaCaseId header"))
 public class BonitaEndpoint extends DefaultEndpoint implements 
EndpointServiceLocation {
 
     @UriParam
diff --git 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
index 4a0ef06c5f18..b2f752f0f032 100644
--- 
a/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
+++ 
b/components/camel-bonita/src/main/java/org/apache/camel/component/bonita/producer/BonitaStartProducer.java
@@ -16,12 +16,19 @@
  */
 package org.apache.camel.component.bonita.producer;
 
+import java.io.InputStream;
+import java.util.Locale;
 import java.util.Map;
 
 import jakarta.ws.rs.client.Client;
 import jakarta.ws.rs.client.ClientBuilder;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.Message;
 import org.apache.camel.component.bonita.BonitaConfiguration;
 import org.apache.camel.component.bonita.BonitaConstants;
 import org.apache.camel.component.bonita.BonitaEndpoint;
@@ -33,6 +40,8 @@ import 
org.apache.camel.component.bonita.api.util.BonitaAPIConfig;
 
 public class BonitaStartProducer extends BonitaProducer {
 
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
     // one JAX-RS client for all the exchanges of this producer, created on 
start and closed on stop
     private volatile Client client;
     private volatile BonitaAPI bonitaApi;
@@ -67,8 +76,7 @@ public class BonitaStartProducer extends BonitaProducer {
 
     @Override
     public void process(Exchange exchange) throws Exception {
-        // a body that is not a Map (such as JSON text) fails here with its 
type, not as a null contract input
-        Map inputs = exchange.getIn().getMandatoryBody(Map.class);
+        Map inputs = getInputs(exchange);
         String processName = this.configuration.getProcessName();
         BonitaAPI api = bonitaApi;
         ProcessDefinitionResponse processDefinition = 
api.getProcessDefinition(processName);
@@ -78,4 +86,32 @@ public class BonitaStartProducer extends BonitaProducer {
         }
     }
 
+    /**
+     * The contract inputs of the case: the Map body, or a JSON object given 
as text (String, byte[] or InputStream)
+     * parsed into a Map. Any other body, and JSON text that is not an object, 
fails with an
+     * {@link InvalidPayloadException}, not as a null contract input.
+     */
+    private static Map getInputs(Exchange exchange) throws 
InvalidPayloadException {
+        Message in = exchange.getIn();
+        Object body = in.getBody();
+        if (body instanceof String || body instanceof byte[] || body 
instanceof InputStream) {
+            String text = in.getMandatoryBody(String.class);
+            JsonNode json;
+            try {
+                json = MAPPER.readTree(text);
+            } catch (JsonProcessingException e) {
+                throw new InvalidPayloadException(exchange, Map.class, in, e);
+            }
+            if (json == null || !json.isObject()) {
+                String type
+                        = json == null || json.isMissingNode() ? "empty" : 
json.getNodeType().name().toLowerCase(Locale.ROOT);
+                throw new InvalidPayloadException(
+                        exchange, Map.class, in,
+                        new IllegalArgumentException("The JSON text is not an 
object of contract inputs but " + type));
+            }
+            return MAPPER.convertValue(json, Map.class);
+        }
+        return in.getMandatoryBody(Map.class);
+    }
+
 }
diff --git 
a/components/camel-bonita/src/test/java/org/apache/camel/component/bonita/BonitaStartProducerTest.java
 
b/components/camel-bonita/src/test/java/org/apache/camel/component/bonita/BonitaStartProducerTest.java
index 16254e135eb6..395dd69b6064 100644
--- 
a/components/camel-bonita/src/test/java/org/apache/camel/component/bonita/BonitaStartProducerTest.java
+++ 
b/components/camel-bonita/src/test/java/org/apache/camel/component/bonita/BonitaStartProducerTest.java
@@ -16,10 +16,13 @@
  */
 package org.apache.camel.component.bonita;
 
+import java.io.ByteArrayInputStream;
 import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.github.tomakehurst.wiremock.WireMockServer;
 import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
 import org.apache.camel.CamelContext;
@@ -35,8 +38,10 @@ import org.junit.jupiter.api.Test;
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
 import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
 import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
 import static com.github.tomakehurst.wiremock.client.WireMock.get;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -47,7 +52,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Starts cases on a Bonita server (WireMock) and checks what the exchange 
carries afterwards: the id of the started
- * case, or the failure of a body that is not a Map of contract inputs.
+ * case, the contract inputs sent from a Map or JSON text body, or the failure 
of a body that is neither.
  */
 class BonitaStartProducerTest {
 
@@ -111,21 +116,97 @@ class BonitaStartProducerTest {
     }
 
     @Test
-    void jsonTextBodyFailsWithItsType() {
-        Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody("{\"amount\":1}"));
+    void jsonTextBodyIsParsedIntoTheContractInputs() {
+        String json = 
"{\"amount\":1,\"customer\":{\"name\":\"Jane\"},\"items\":[\"a\",\"b\"]}";
 
-        InvalidPayloadException cause = 
assertInstanceOf(InvalidPayloadException.class, exchange.getException());
-        assertEquals(Map.class, cause.getType(), "required body type");
-        assertTrue(cause.getMessage().contains("java.lang.String"), "message 
names the body type: " + cause.getMessage());
-        assertNull(exchange.getMessage().getHeader(BonitaConstants.CASE_ID), 
"case id header");
-        assertEquals(0, 
server.findAll(anyRequestedFor(urlEqualTo(INSTANTIATION))).size(), "cases 
started");
+        Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody(json));
+
+        assertCaseStartedWith(exchange, json);
+        assertEquals(json, exchange.getMessage().getBody(), "body after the 
case started");
+    }
+
+    @Test
+    void jsonBytesBodyIsParsedIntoTheContractInputs() {
+        String json = "{\"amount\":2}";
+
+        Exchange exchange = template.send("direct:start",
+                e -> e.getIn().setBody(json.getBytes(StandardCharsets.UTF_8)));
+
+        assertCaseStartedWith(exchange, json);
+    }
+
+    @Test
+    void jsonStreamBodyIsParsedIntoTheContractInputs() {
+        String json = "{\"amount\":3}";
+
+        Exchange exchange = template.send("direct:start",
+                e -> e.getIn().setBody(new 
ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8))));
+
+        assertCaseStartedWith(exchange, json);
+    }
+
+    @Test
+    void jsonArrayTextFailsAsNotAnObject() {
+        Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody("[{\"amount\":1}]"));
+
+        InvalidPayloadException cause = assertNoCaseStarted(exchange);
+        assertTrue(cause.getCause().getMessage().contains("not an object of 
contract inputs but array"),
+                "cause names the JSON type: " + cause.getCause().getMessage());
+    }
+
+    @Test
+    void jsonNullTextFailsAsNotAnObject() {
+        Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody("null"));
+
+        InvalidPayloadException cause = assertNoCaseStarted(exchange);
+        assertTrue(cause.getCause().getMessage().contains("not an object of 
contract inputs but null"),
+                "cause names the JSON type: " + cause.getCause().getMessage());
+    }
+
+    @Test
+    void emptyTextFailsAsNotAnObject() {
+        Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody(""));
+
+        InvalidPayloadException cause = assertNoCaseStarted(exchange);
+        assertTrue(cause.getCause().getMessage().contains("not an object of 
contract inputs but empty"),
+                "cause names the JSON type: " + cause.getCause().getMessage());
+    }
+
+    @Test
+    void textThatIsNotJsonFailsWithTheParseError() {
+        Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody("amount=1"));
+
+        InvalidPayloadException cause = assertNoCaseStarted(exchange);
+        assertInstanceOf(JsonProcessingException.class, cause.getCause(), 
"cause of the failure");
+    }
+
+    @Test
+    void otherBodyFailsWithItsType() {
+        Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody(42));
+
+        InvalidPayloadException cause = assertNoCaseStarted(exchange);
+        assertTrue(cause.getMessage().contains("java.lang.Integer"), "message 
names the body type: " + cause.getMessage());
     }
 
     @Test
     void missingBodyFailsAsNoBody() {
         Exchange exchange = template.send("direct:start", e -> 
e.getIn().setBody(null));
 
-        assertInstanceOf(InvalidPayloadException.class, 
exchange.getException());
+        assertNoCaseStarted(exchange);
+    }
+
+    private void assertCaseStartedWith(Exchange exchange, String json) {
+        assertNull(exchange.getException(), "exception of the exchange");
+        assertEquals("4021", 
exchange.getMessage().getHeader(BonitaConstants.CASE_ID), "case id header");
+        assertEquals(1, 
server.findAll(postRequestedFor(urlEqualTo(INSTANTIATION)).withRequestBody(equalToJson(json)))
+                .size(), "cases started with the contract inputs " + json);
+    }
+
+    private InvalidPayloadException assertNoCaseStarted(Exchange exchange) {
+        InvalidPayloadException cause = 
assertInstanceOf(InvalidPayloadException.class, exchange.getException());
+        assertEquals(Map.class, cause.getType(), "required body type");
+        assertNull(exchange.getMessage().getHeader(BonitaConstants.CASE_ID), 
"case id header");
         assertEquals(0, 
server.findAll(anyRequestedFor(urlEqualTo(INSTANTIATION))).size(), "cases 
started");
+        return cause;
     }
 }

Reply via email to