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; } }
