This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new df4fa21ee94e CAMEL-26502: camel-bonita - startCase accepts a JSON text
body as the contract inputs (#28982)
df4fa21ee94e is described below
commit df4fa21ee94e595054e8b5d54bece8ec7fcd9fa2
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 21:56:25 2026 +0100
CAMEL-26502: camel-bonita - startCase accepts a JSON text body as the
contract inputs (#28982)
… 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;
}
}