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 515378b199b8 CAMEL-26246: camel-azure-cosmosdb - parse the JSON of an
item given as text, bytes, a stream or a file, and fail a List body of the item
operations (#28954)
515378b199b8 is described below
commit 515378b199b85551f302bc6674874854bf082a1e
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 2 23:22:08 2026 +0100
CAMEL-26246: camel-azure-cosmosdb - parse the JSON of an item given as
text, bytes, a stream or a file, and fail a List body of the item operations
(#28954)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/catalog/components/azure-cosmosdb.json | 4 +-
.../catalog/docs/azure-cosmosdb-component.adoc | 31 +--
.../component/azure/cosmosdb/azure-cosmosdb.json | 4 +-
.../src/main/docs/azure-cosmosdb-component.adoc | 31 +--
.../CosmosDbConfigurationOptionsProxy.java | 102 +++++++-
.../component/azure/cosmosdb/CosmosDbEndpoint.java | 31 ++-
.../cosmosdb/CosmosDbProducerItemBodyTest.java | 278 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 +
8 files changed, 446 insertions(+), 47 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-cosmosdb.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-cosmosdb.json
index a68f0fa00040..e36481d9dc15 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-cosmosdb.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-cosmosdb.json
@@ -58,8 +58,8 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "list" ], "javaType":
"java.util.List<java.util.Map<String, Object>>", "description": "The items of
one batch of the change feed: a List with a Map per item created or changed,
with the system fields of Cosmos DB such as _etag and _lsn" },
- "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "createItem", "carriers": [ "map", "pojo", "bytes" ] }, {
"option": "operation", "value": "upsertItem", "carriers": [ "map", "pojo",
"bytes" ] }, { "option": "operation", "value": "replaceItem", "carriers": [
"map", "pojo", "bytes" ] } ], "description": "The item for createItem,
upsertItem and replaceItem: a Map or a POJO, which Jackson serializes, or the
JSON of the item as bytes. A JSON String fa [...]
- "producerOut": { "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>",
"dependsOn": [ "contentResponseOnWriteEnabled" ], "byOption": [ { "option":
"operation", "value": "queryDatabases", "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>" }, {
"option": "operation", "value": "listContainers", "carriers": [ "list" ],
"javaType": "java.util.List<com.azure.cosmos.models.CosmosContainerPr [...]
+ "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "createItem", "carriers": [ "map", "pojo", "text",
"bytes", "stream", "file" ] }, { "option": "operation", "value": "upsertItem",
"carriers": [ "map", "pojo", "text", "bytes", "stream", "file" ] }, { "option":
"operation", "value": "replaceItem", "carriers": [ "map", "pojo", "text",
"bytes", "stream", "file" ] } ], "description": "The item for createItem,
upsertItem and replaceItem: a Map or a POJO [...]
+ "producerOut": { "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>",
"dependsOn": [ "contentResponseOnWriteEnabled" ], "byOption": [ { "option":
"operation", "value": "queryDatabases", "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>" }, {
"option": "operation", "value": "listContainers", "carriers": [ "list" ],
"javaType": "java.util.List<com.azure.cosmos.models.CosmosContainerPr [...]
},
"properties": {
"databaseName": { "index": 0, "kind": "path", "displayName": "Database
Name", "group": "common", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.azure.cosmosdb.CosmosDbConfiguration",
"configurationField": "configuration", "description": "The name of the Cosmos
database that component should connect to. In case you are producing data and
have c [...]
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-cosmosdb-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-cosmosdb-component.adoc
index 30cd7422817c..dbfffa5fbdee 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-cosmosdb-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/azure-cosmosdb-component.adoc
@@ -149,15 +149,28 @@ For these operations, `databaseName` and `containerName`
is *required* for all o
|Operation |Description
|`deleteContainer` | Delete a container from the specified Azure CosmosDB
database.
|`replaceContainerThroughput` | Replace the throughput for the specified
Azure CosmosDB container.
-|`createItem` | *`itemPartitionKey` is required* Creates an item in the
specified container, it accepts POJO or key value as `Map<String, ?>`; a JSON
String fails: unmarshal it first.
-|`upsertItem` | *`itemPartitionKey` is required* Creates an item in the
specified container if it doesn't exist otherwise overwrite it if it exists, it
accepts POJO or key value as `Map<String, ?>`; a JSON String fails: unmarshal
it first.
-|`replaceItem` | *`itemPartitionKey` and `itemId` are required* Overwrites an
item in the specified container, it accepts POJO or key value as `Map<String,
?>`; a JSON String fails: unmarshal it first.
+|`createItem` | *`itemPartitionKey` is required* Creates an item in the
specified container, it accepts POJO or key value as `Map<String, ?>`, or the
JSON of the item (see below).
+|`upsertItem` | *`itemPartitionKey` is required* Creates an item in the
specified container if it doesn't exist otherwise overwrite it if it exists, it
accepts POJO or key value as `Map<String, ?>`, or the JSON of the item (see
below).
+|`replaceItem` | *`itemPartitionKey` and `itemId` are required* Overwrites an
item in the specified container, it accepts POJO or key value as `Map<String,
?>`, or the JSON of the item (see below).
|`deleteItem` | *`itemPartitionKey` and `itemId` are required* Deletes an
item in the specified container.
|`readItem` | *`itemPartitionKey` and `itemId` are required* Gets an item in
the specified container as `Map<String,?>` set in the exchange body message.
|`readAllItems` | *`itemPartitionKey`* Gets a list of items in the specified
container per the `itemPartitionKey` as `List<Map<String,?>>` set in the
exchange body message.
|`queryItems` | *`query` is required* Execute an SQL query against the
container level in order, for example, return only matching items per the SQL
query. It will set `List<Map<String,>?>` in the exchange message body.
|===
+The message body of `createItem`, `upsertItem` and `replaceItem` is one item:
a POJO or a `Map<String, ?>`, which the
+Azure SDK serializes with Jackson, or the JSON of the item as a `String`, a
`byte[]`, a stream or a file (such as the
+body of the file consumer), which the component parses into a `Map` first.
After the operation, the message body is
+the item as written by Cosmos DB, with its system fields such as `_etag`: a
`Map` for a `Map` or JSON body, and the
+class of the POJO for a POJO. With `contentResponseOnWriteEnabled=false` there
is no message body.
+
+A `byte[]`, an `InputStream` or a file is read as bytes, in which Jackson
detects JSON in UTF-8, UTF-16 or UTF-32. For
+JSON in another charset, such as a file that the file consumer reads with
`charset=ISO-8859-1`, convert the body to a
+`String` first, for example with `convertBodyTo(String.class)`, which reads
the file with its charset.
+
+These operations write one item: a `List` body, or JSON that is not an object
(such as a JSON array), fails with an
+`IllegalArgumentException`. To write several items, send each item in its own
exchange, for example with the Split EIP.
+
Refer to the example section in this page to learn how to use these operations
into your camel application.
== Examples
@@ -558,7 +571,6 @@ from("direct:start")
.setHeader("CamelAzureCosmosDbContainerPartitionKeyPath",
constant("partition"))
.setHeader("CamelAzureCosmosDbItemPartitionKey", constant("test-1"))
.setBody(constant("{\"id\":\"test-id-1\",\"partition\":\"test-1\",\"field1\":\"awesome!\"}"))
- .unmarshal().json()
.to("azure-cosmosdb://?operation=createItem")
.to("mock:result");
----
@@ -584,9 +596,6 @@ XML::
<setBody>
<constant>{"id":"test-id-1","partition":"test-1","field1":"awesome!"}</constant>
</setBody>
- <unmarshal>
- <json/>
- </unmarshal>
<to uri="azure-cosmosdb://?operation=createItem"/>
<to uri="mock:result"/>
</route>
@@ -614,8 +623,6 @@ YAML::
constant: "test-1"
- setBody:
constant:
'{"id":"test-id-1","partition":"test-1","field1":"awesome!"}'
- - unmarshal:
- json: {}
- to:
uri: azure-cosmosdb://
parameters:
@@ -639,7 +646,6 @@ from("direct:start")
.setHeader("CamelAzureCosmosDbItemPartitionKey", constant("test-1"))
.setHeader("CamelAzureCosmosDbItemId", constant("test-id-1"))
.setBody(constant("{\"id\":\"test-id-1\",\"partition\":\"test-1\",\"field1\":\"awesome!\"}"))
- .unmarshal().json()
.to("azure-cosmosdb://?operation=replaceItem")
.to("mock:result");
----
@@ -665,9 +671,6 @@ XML::
<setBody>
<constant>{"id":"test-id-1","partition":"test-1","field1":"awesome!"}</constant>
</setBody>
- <unmarshal>
- <json/>
- </unmarshal>
<to uri="azure-cosmosdb://?operation=replaceItem"/>
<to uri="mock:result"/>
</route>
@@ -695,8 +698,6 @@ YAML::
constant: "test-id-1"
- setBody:
constant:
'{"id":"test-id-1","partition":"test-1","field1":"awesome!"}'
- - unmarshal:
- json: {}
- to:
uri: azure-cosmosdb://
parameters:
diff --git
a/components/camel-azure/camel-azure-cosmosdb/src/generated/resources/META-INF/org/apache/camel/component/azure/cosmosdb/azure-cosmosdb.json
b/components/camel-azure/camel-azure-cosmosdb/src/generated/resources/META-INF/org/apache/camel/component/azure/cosmosdb/azure-cosmosdb.json
index a68f0fa00040..e36481d9dc15 100644
---
a/components/camel-azure/camel-azure-cosmosdb/src/generated/resources/META-INF/org/apache/camel/component/azure/cosmosdb/azure-cosmosdb.json
+++
b/components/camel-azure/camel-azure-cosmosdb/src/generated/resources/META-INF/org/apache/camel/component/azure/cosmosdb/azure-cosmosdb.json
@@ -58,8 +58,8 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "list" ], "javaType":
"java.util.List<java.util.Map<String, Object>>", "description": "The items of
one batch of the change feed: a List with a Map per item created or changed,
with the system fields of Cosmos DB such as _etag and _lsn" },
- "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "createItem", "carriers": [ "map", "pojo", "bytes" ] }, {
"option": "operation", "value": "upsertItem", "carriers": [ "map", "pojo",
"bytes" ] }, { "option": "operation", "value": "replaceItem", "carriers": [
"map", "pojo", "bytes" ] } ], "description": "The item for createItem,
upsertItem and replaceItem: a Map or a POJO, which Jackson serializes, or the
JSON of the item as bytes. A JSON String fa [...]
- "producerOut": { "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>",
"dependsOn": [ "contentResponseOnWriteEnabled" ], "byOption": [ { "option":
"operation", "value": "queryDatabases", "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>" }, {
"option": "operation", "value": "listContainers", "carriers": [ "list" ],
"javaType": "java.util.List<com.azure.cosmos.models.CosmosContainerPr [...]
+ "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "createItem", "carriers": [ "map", "pojo", "text",
"bytes", "stream", "file" ] }, { "option": "operation", "value": "upsertItem",
"carriers": [ "map", "pojo", "text", "bytes", "stream", "file" ] }, { "option":
"operation", "value": "replaceItem", "carriers": [ "map", "pojo", "text",
"bytes", "stream", "file" ] } ], "description": "The item for createItem,
upsertItem and replaceItem: a Map or a POJO [...]
+ "producerOut": { "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>",
"dependsOn": [ "contentResponseOnWriteEnabled" ], "byOption": [ { "option":
"operation", "value": "queryDatabases", "carriers": [ "list" ], "javaType":
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>" }, {
"option": "operation", "value": "listContainers", "carriers": [ "list" ],
"javaType": "java.util.List<com.azure.cosmos.models.CosmosContainerPr [...]
},
"properties": {
"databaseName": { "index": 0, "kind": "path", "displayName": "Database
Name", "group": "common", "label": "", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.azure.cosmosdb.CosmosDbConfiguration",
"configurationField": "configuration", "description": "The name of the Cosmos
database that component should connect to. In case you are producing data and
have c [...]
diff --git
a/components/camel-azure/camel-azure-cosmosdb/src/main/docs/azure-cosmosdb-component.adoc
b/components/camel-azure/camel-azure-cosmosdb/src/main/docs/azure-cosmosdb-component.adoc
index 30cd7422817c..dbfffa5fbdee 100644
---
a/components/camel-azure/camel-azure-cosmosdb/src/main/docs/azure-cosmosdb-component.adoc
+++
b/components/camel-azure/camel-azure-cosmosdb/src/main/docs/azure-cosmosdb-component.adoc
@@ -149,15 +149,28 @@ For these operations, `databaseName` and `containerName`
is *required* for all o
|Operation |Description
|`deleteContainer` | Delete a container from the specified Azure CosmosDB
database.
|`replaceContainerThroughput` | Replace the throughput for the specified
Azure CosmosDB container.
-|`createItem` | *`itemPartitionKey` is required* Creates an item in the
specified container, it accepts POJO or key value as `Map<String, ?>`; a JSON
String fails: unmarshal it first.
-|`upsertItem` | *`itemPartitionKey` is required* Creates an item in the
specified container if it doesn't exist otherwise overwrite it if it exists, it
accepts POJO or key value as `Map<String, ?>`; a JSON String fails: unmarshal
it first.
-|`replaceItem` | *`itemPartitionKey` and `itemId` are required* Overwrites an
item in the specified container, it accepts POJO or key value as `Map<String,
?>`; a JSON String fails: unmarshal it first.
+|`createItem` | *`itemPartitionKey` is required* Creates an item in the
specified container, it accepts POJO or key value as `Map<String, ?>`, or the
JSON of the item (see below).
+|`upsertItem` | *`itemPartitionKey` is required* Creates an item in the
specified container if it doesn't exist otherwise overwrite it if it exists, it
accepts POJO or key value as `Map<String, ?>`, or the JSON of the item (see
below).
+|`replaceItem` | *`itemPartitionKey` and `itemId` are required* Overwrites an
item in the specified container, it accepts POJO or key value as `Map<String,
?>`, or the JSON of the item (see below).
|`deleteItem` | *`itemPartitionKey` and `itemId` are required* Deletes an
item in the specified container.
|`readItem` | *`itemPartitionKey` and `itemId` are required* Gets an item in
the specified container as `Map<String,?>` set in the exchange body message.
|`readAllItems` | *`itemPartitionKey`* Gets a list of items in the specified
container per the `itemPartitionKey` as `List<Map<String,?>>` set in the
exchange body message.
|`queryItems` | *`query` is required* Execute an SQL query against the
container level in order, for example, return only matching items per the SQL
query. It will set `List<Map<String,>?>` in the exchange message body.
|===
+The message body of `createItem`, `upsertItem` and `replaceItem` is one item:
a POJO or a `Map<String, ?>`, which the
+Azure SDK serializes with Jackson, or the JSON of the item as a `String`, a
`byte[]`, a stream or a file (such as the
+body of the file consumer), which the component parses into a `Map` first.
After the operation, the message body is
+the item as written by Cosmos DB, with its system fields such as `_etag`: a
`Map` for a `Map` or JSON body, and the
+class of the POJO for a POJO. With `contentResponseOnWriteEnabled=false` there
is no message body.
+
+A `byte[]`, an `InputStream` or a file is read as bytes, in which Jackson
detects JSON in UTF-8, UTF-16 or UTF-32. For
+JSON in another charset, such as a file that the file consumer reads with
`charset=ISO-8859-1`, convert the body to a
+`String` first, for example with `convertBodyTo(String.class)`, which reads
the file with its charset.
+
+These operations write one item: a `List` body, or JSON that is not an object
(such as a JSON array), fails with an
+`IllegalArgumentException`. To write several items, send each item in its own
exchange, for example with the Split EIP.
+
Refer to the example section in this page to learn how to use these operations
into your camel application.
== Examples
@@ -558,7 +571,6 @@ from("direct:start")
.setHeader("CamelAzureCosmosDbContainerPartitionKeyPath",
constant("partition"))
.setHeader("CamelAzureCosmosDbItemPartitionKey", constant("test-1"))
.setBody(constant("{\"id\":\"test-id-1\",\"partition\":\"test-1\",\"field1\":\"awesome!\"}"))
- .unmarshal().json()
.to("azure-cosmosdb://?operation=createItem")
.to("mock:result");
----
@@ -584,9 +596,6 @@ XML::
<setBody>
<constant>{"id":"test-id-1","partition":"test-1","field1":"awesome!"}</constant>
</setBody>
- <unmarshal>
- <json/>
- </unmarshal>
<to uri="azure-cosmosdb://?operation=createItem"/>
<to uri="mock:result"/>
</route>
@@ -614,8 +623,6 @@ YAML::
constant: "test-1"
- setBody:
constant:
'{"id":"test-id-1","partition":"test-1","field1":"awesome!"}'
- - unmarshal:
- json: {}
- to:
uri: azure-cosmosdb://
parameters:
@@ -639,7 +646,6 @@ from("direct:start")
.setHeader("CamelAzureCosmosDbItemPartitionKey", constant("test-1"))
.setHeader("CamelAzureCosmosDbItemId", constant("test-id-1"))
.setBody(constant("{\"id\":\"test-id-1\",\"partition\":\"test-1\",\"field1\":\"awesome!\"}"))
- .unmarshal().json()
.to("azure-cosmosdb://?operation=replaceItem")
.to("mock:result");
----
@@ -665,9 +671,6 @@ XML::
<setBody>
<constant>{"id":"test-id-1","partition":"test-1","field1":"awesome!"}</constant>
</setBody>
- <unmarshal>
- <json/>
- </unmarshal>
<to uri="azure-cosmosdb://?operation=replaceItem"/>
<to uri="mock:result"/>
</route>
@@ -695,8 +698,6 @@ YAML::
constant: "test-id-1"
- setBody:
constant:
'{"id":"test-id-1","partition":"test-1","field1":"awesome!"}'
- - unmarshal:
- json: {}
- to:
uri: azure-cosmosdb://
parameters:
diff --git
a/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbConfigurationOptionsProxy.java
b/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbConfigurationOptionsProxy.java
index 8fb6704b814f..d188d2309e63 100644
---
a/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbConfigurationOptionsProxy.java
+++
b/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbConfigurationOptionsProxy.java
@@ -16,12 +16,29 @@
*/
package org.apache.camel.component.azure.cosmosdb;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.function.Supplier;
import com.azure.cosmos.models.*;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
import org.apache.camel.Exchange;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
import org.apache.camel.util.ObjectHelper;
/**
@@ -30,6 +47,12 @@ import org.apache.camel.util.ObjectHelper;
*/
public class CosmosDbConfigurationOptionsProxy {
+ // parses a JSON body into the item to write; JSON with more than one
value (such as JSON lines) fails
+ private static final ObjectMapper JSON_MAPPER
+ =
JsonMapper.builder().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS).build();
+ private static final TypeReference<Map<String, Object>> ITEM_TYPE = new
TypeReference<>() {
+ };
+
private final CosmosDbConfiguration configuration;
public CosmosDbConfigurationOptionsProxy(final CosmosDbConfiguration
configuration) {
@@ -99,8 +122,85 @@ public class CosmosDbConfigurationOptionsProxy {
String.class));
}
+ /**
+ * The item that createItem, upsertItem and replaceItem write. A Map or a
POJO is returned as it is: the SDK
+ * serializes it with Jackson. JSON as text, bytes, a stream or a file is
parsed into a Map: the SDK would write a
+ * String (or a file, as its path) as a JSON string and cannot serialize a
stream. These operations write one item,
+ * so a List of items, or JSON that is not an object, fails.
+ */
public Object getItem(final Exchange exchange) {
- return exchange.getIn().getBody();
+ final Object body = exchange.getIn().getBody();
+ if (ObjectHelper.isEmpty(body)) {
+ // the operation fails as the item is empty
+ return body;
+ }
+ if (body instanceof Collection || (body.getClass().isArray() && !(body
instanceof byte[]))) {
+ throw new IllegalArgumentException(
+ "The body of the " + getOperation(exchange) + " operation
must be one item, but it is a "
+ + body.getClass().getTypeName()
+ + ": send each item in its own
exchange, for example with the Split EIP");
+ }
+ if (isJson(body)) {
+ return parseItem(exchange, body);
+ }
+ return body;
+ }
+
+ /**
+ * Whether the body is a payload, which holds the JSON of the item: text,
bytes, a stream or a file.
+ */
+ private static boolean isJson(final Object body) {
+ return body instanceof String || body instanceof byte[]
+ || body instanceof InputStream || body instanceof Reader ||
body instanceof StreamCache
+ || body instanceof File || body instanceof Path || body
instanceof WrappedFile;
+ }
+
+ private Map<String, Object> parseItem(final Exchange exchange, final
Object body) {
+ final JsonNode json;
+ try {
+ json = readJson(exchange, body);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException(
+ "The body of the " + getOperation(exchange)
+ + " operation is not valid
JSON, or holds more than one JSON value",
+ e);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ if (json == null || json.isMissingNode()) {
+ // the operation fails as the item is empty
+ return null;
+ }
+ if (json.isArray()) {
+ throw new IllegalArgumentException(
+ "The body of the " + getOperation(exchange) + " operation
must be one item, but it is a JSON array:"
+ + " send each item in its own
exchange, for example with the Split EIP");
+ }
+ if (!json.isObject()) {
+ throw new IllegalArgumentException(
+ "The body of the " + getOperation(exchange) + " operation
must be a JSON object, but it is a JSON "
+ +
json.getNodeType().name().toLowerCase(Locale.ROOT));
+ }
+ return JSON_MAPPER.convertValue(json, ITEM_TYPE);
+ }
+
+ private static JsonNode readJson(final Exchange exchange, final Object
body) throws IOException {
+ if (body instanceof String text) {
+ return JSON_MAPPER.readTree(text);
+ }
+ if (body instanceof byte[] bytes) {
+ return JSON_MAPPER.readTree(bytes);
+ }
+ if (body instanceof Reader reader) {
+ return JSON_MAPPER.readTree(reader);
+ }
+ // a stream or a file: its content, as bytes, from which Jackson
detects the encoding of the JSON
+ final byte[] content = exchange.getIn().getBody(byte[].class);
+ if (content == null) {
+ throw new IllegalArgumentException(
+ "Cannot read the body of type " +
body.getClass().getName() + " as the JSON of the item");
+ }
+ return JSON_MAPPER.readTree(content);
}
public List<Object> getItems(final Exchange exchange) {
diff --git
a/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbEndpoint.java
b/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbEndpoint.java
index 350c78bf6cd8..a68e6e9635d5 100644
---
a/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbEndpoint.java
+++
b/components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbEndpoint.java
@@ -47,16 +47,22 @@ import org.apache.camel.util.ObjectHelper;
producerIn = @BodyType(value = Carrier.ANY,
byOption = {
@BodyType.When(option = "operation",
value = "createItem",
- carriers = {
Carrier.MAP, Carrier.POJO, Carrier.BYTES }),
+ carriers = {
+ Carrier.MAP,
Carrier.POJO, Carrier.TEXT, Carrier.BYTES,
+
Carrier.STREAM, Carrier.FILE }),
@BodyType.When(option = "operation",
value = "upsertItem",
- carriers = {
Carrier.MAP, Carrier.POJO, Carrier.BYTES }),
+ carriers = {
+ Carrier.MAP,
Carrier.POJO, Carrier.TEXT, Carrier.BYTES,
+
Carrier.STREAM, Carrier.FILE }),
@BodyType.When(option = "operation",
value = "replaceItem",
- carriers = {
Carrier.MAP, Carrier.POJO, Carrier.BYTES }) },
+ carriers = {
+ Carrier.MAP,
Carrier.POJO, Carrier.TEXT, Carrier.BYTES,
+
Carrier.STREAM, Carrier.FILE }) },
description = "The item for createItem,
upsertItem and replaceItem: a Map or a POJO, which"
- + " Jackson serializes, or the
JSON of the item as bytes. A JSON String fails, as"
- + " it is sent as a JSON
string and not as an object: unmarshal it first. The other"
- + " operations do not use the
body: the item id, the partition key and the query"
- + " are options or headers"),
+ + " Jackson serializes, or the
JSON of the item as text, bytes, a stream or a"
+ + " file, which is parsed into
a Map. These operations write one item: a List,"
+ + " or JSON that is not an
object, fails. The other operations do not use the"
+ + " body: the item id, the
partition key and the query are options or headers"),
producerOut = @BodyType(value = Carrier.LIST,
javaType =
"java.util.List<com.azure.cosmos.models.CosmosDatabaseProperties>",
dependsOn = "contentResponseOnWriteEnabled",
@@ -83,11 +89,11 @@ import org.apache.camel.util.ObjectHelper;
@BodyType.When(option =
"operation", value = "replaceContainerThroughput",
carriers =
Carrier.UNCHANGED),
@BodyType.When(option =
"operation", value = "createItem",
- carriers = {
Carrier.MAP, Carrier.POJO, Carrier.BYTES }),
+ carriers = {
Carrier.MAP, Carrier.POJO }),
@BodyType.When(option =
"operation", value = "upsertItem",
- carriers = {
Carrier.MAP, Carrier.POJO, Carrier.BYTES }),
+ carriers = {
Carrier.MAP, Carrier.POJO }),
@BodyType.When(option =
"operation", value = "replaceItem",
- carriers = {
Carrier.MAP, Carrier.POJO, Carrier.BYTES }),
+ carriers = {
Carrier.MAP, Carrier.POJO }),
@BodyType.When(option =
"operation", value = "readItem", carriers = Carrier.MAP,
javaType =
"java.util.Map<String, Object>"),
@BodyType.When(option =
"operation", value = "readAllItems", carriers = Carrier.LIST,
@@ -97,8 +103,9 @@ import org.apache.camel.util.ObjectHelper;
carriers =
Carrier.NULL) },
description = "A List of the properties of
the SDK for listDatabases, the default, and the"
+ " list and query operations
of databases and containers; the item as written"
- + " for createItem,
upsertItem and replaceItem, of the type of the body sent, or"
- + " no body with
contentResponseOnWriteEnabled=false; the item as a Map for"
+ + " for createItem,
upsertItem and replaceItem, as a Map for a Map or JSON body"
+ + " and of the class of the
POJO for a POJO, or no body with"
+ + "
contentResponseOnWriteEnabled=false; the item as a Map for"
+ " readItem, and a List of
them for readAllItems and queryItems, whose elements"
+ " are values for a SELECT
VALUE query. The operations on databases, containers"
+ " and throughput leave the
body and set the response in headers"))
diff --git
a/components/camel-azure/camel-azure-cosmosdb/src/test/java/org/apache/camel/component/azure/cosmosdb/CosmosDbProducerItemBodyTest.java
b/components/camel-azure/camel-azure-cosmosdb/src/test/java/org/apache/camel/component/azure/cosmosdb/CosmosDbProducerItemBodyTest.java
new file mode 100644
index 000000000000..06c94c8f9cb9
--- /dev/null
+++
b/components/camel-azure/camel-azure-cosmosdb/src/test/java/org/apache/camel/component/azure/cosmosdb/CosmosDbProducerItemBodyTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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.apache.camel.component.azure.cosmosdb;
+
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.azure.cosmos.CosmosAsyncClient;
+import com.azure.cosmos.CosmosAsyncContainer;
+import com.azure.cosmos.CosmosAsyncDatabase;
+import com.azure.cosmos.CosmosItemSerializer;
+import com.azure.cosmos.models.CosmosItemResponse;
+import com.azure.cosmos.models.PartitionKey;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import reactor.core.publisher.Mono;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * The body of createItem, upsertItem and replaceItem is one item. The SDK
serializes a Map or a POJO with Jackson, but
+ * writes a String (or a file, as its path) as a JSON string and cannot
serialize a stream: the JSON of the item, as
+ * text, bytes, a stream or a file, is parsed into a Map first. A List of
items, or JSON that is not one object, fails
+ * before anything is written.
+ */
+class CosmosDbProducerItemBodyTest extends CamelTestSupport {
+
+ private static final String JSON =
"{\"id\":\"1\",\"pk\":\"a\",\"city\":\"Zürich\",\"price\":9.5,\"tags\":[\"x\",\"y\"]}";
+
+ @BindToRegistry("cosmosClient")
+ private final CosmosAsyncClient client = mock(CosmosAsyncClient.class);
+ private final CosmosAsyncDatabase database =
mock(CosmosAsyncDatabase.class);
+ private final CosmosAsyncContainer container =
mock(CosmosAsyncContainer.class);
+ // the items given to the SDK
+ private final List<Object> written = new CopyOnWriteArrayList<>();
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:createItem").to("azure-cosmosdb:myDb/items?operation=createItem&itemPartitionKey=a");
+
from("direct:upsertItem").to("azure-cosmosdb:myDb/items?operation=upsertItem&itemPartitionKey=a");
+ from("direct:replaceItem")
+
.to("azure-cosmosdb:myDb/items?operation=replaceItem&itemPartitionKey=a&itemId=1");
+ }
+ };
+ }
+
+ @BeforeEach
+ @SuppressWarnings("unchecked")
+ void mockContainer() {
+ CosmosItemResponse<Object> response = mock(CosmosItemResponse.class);
+ when(response.getStatusCode()).thenReturn(201);
+ when(response.getItem()).thenAnswer(invocation ->
written.get(written.size() - 1));
+
+ when(client.getDatabase("myDb")).thenReturn(database);
+ when(database.getContainer("items")).thenReturn(container);
+ when(container.createItem(any(), any(PartitionKey.class),
any())).thenAnswer(invocation -> {
+ written.add(invocation.getArgument(0));
+ return Mono.just(response);
+ });
+ when(container.upsertItem(any(), any(PartitionKey.class),
any())).thenAnswer(invocation -> {
+ written.add(invocation.getArgument(0));
+ return Mono.just(response);
+ });
+ when(container.replaceItem(any(), anyString(),
any(PartitionKey.class), any())).thenAnswer(invocation -> {
+ written.add(invocation.getArgument(0));
+ return Mono.just(response);
+ });
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "createItem", "upsertItem", "replaceItem" })
+ void jsonTextIsWrittenAsAnObject(String operation) {
+ Exchange exchange = template.send("direct:" + operation, e ->
e.getIn().setBody(JSON));
+
+ assertNull(exchange.getException());
+ Map<?, ?> item = assertInstanceOf(Map.class, writtenItem());
+ assertEquals("1", document(item).get("id"));
+ assertEquals("Zürich", item.get("city"));
+ assertEquals(9.5, item.get("price"));
+ assertEquals(List.of("x", "y"), item.get("tags"));
+ // the body is the item as written
+ assertSame(item, exchange.getMessage().getBody());
+ assertEquals(201,
exchange.getMessage().getHeader(CosmosDbConstants.STATUS_CODE));
+ }
+
+ @Test
+ void jsonBytesAreParsed() {
+ Exchange exchange = template.send("direct:createItem", e ->
e.getIn().setBody(JSON.getBytes(StandardCharsets.UTF_8)));
+
+ assertNull(exchange.getException());
+ Map<?, ?> item = assertInstanceOf(Map.class, writtenItem());
+ assertEquals("1", item.get("id"));
+ assertEquals("Zürich", item.get("city"));
+ }
+
+ @Test
+ void streamIsParsed() {
+ Exchange exchange = template.send("direct:createItem",
+ e -> e.getIn().setBody(new
ByteArrayInputStream(JSON.getBytes(StandardCharsets.UTF_8))));
+
+ assertNull(exchange.getException());
+ Map<?, ?> item = assertInstanceOf(Map.class, writtenItem());
+ assertEquals("1", document(item).get("id"));
+ assertEquals("Zürich", item.get("city"));
+ }
+
+ @Test
+ void readerIsParsed() {
+ Exchange exchange = template.send("direct:upsertItem", e ->
e.getIn().setBody(new StringReader(JSON)));
+
+ assertNull(exchange.getException());
+ Map<?, ?> item = assertInstanceOf(Map.class, writtenItem());
+ assertEquals("1", document(item).get("id"));
+ }
+
+ @Test
+ void fileAndPathAreParsed(@TempDir Path dir) throws Exception {
+ Path file = Files.writeString(dir.resolve("item.json"), JSON);
+
+ Exchange exchange = template.send("direct:createItem", e ->
e.getIn().setBody(file.toFile()));
+ assertNull(exchange.getException());
+ Map<?, ?> item = assertInstanceOf(Map.class, writtenItem());
+ assertEquals("1", document(item).get("id"));
+ assertEquals("Zürich", item.get("city"));
+
+ exchange = template.send("direct:replaceItem", e ->
e.getIn().setBody(file));
+ assertNull(exchange.getException());
+ item = assertInstanceOf(Map.class, writtenItem());
+ assertEquals("1", document(item).get("id"));
+ }
+
+ @Test
+ void fileOfTheFileConsumerIsParsed(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("item.json"), JSON);
+ MockEndpoint mock = getMockEndpoint("mock:written");
+ mock.expectedMessageCount(1);
+
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("file:" + dir.toAbsolutePath() +
"?noop=true&initialDelay=0&delay=10")
+
.to("azure-cosmosdb:myDb/items?operation=createItem&itemPartitionKey=a")
+ .to("mock:written");
+ }
+ });
+
+ mock.assertIsSatisfied();
+ Map<?, ?> item = assertInstanceOf(Map.class, writtenItem());
+ assertEquals("1", document(item).get("id"));
+ assertEquals("Zürich", item.get("city"));
+ }
+
+ @Test
+ void mapAndPojoAreWrittenAsTheyAre() {
+ Map<String, Object> map = Map.of("id", "1", "pk", "a");
+ template.sendBody("direct:createItem", map);
+ assertSame(map, writtenItem());
+
+ Item pojo = new Item("2", "a");
+ template.sendBody("direct:upsertItem", pojo);
+ assertSame(pojo, writtenItem());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "createItem", "upsertItem", "replaceItem" })
+ void listOfItemsFails(String operation) {
+ Exchange exchange = template.send("direct:" + operation,
+ e -> e.getIn().setBody(List.of(Map.of("id", "1", "pk", "a"),
Map.of("id", "2", "pk", "a"))));
+
+ IllegalArgumentException error =
assertInstanceOf(IllegalArgumentException.class, exchange.getException());
+ assertTrue(error.getMessage().contains(operation + " operation must be
one item"), error.getMessage());
+ assertTrue(written.isEmpty());
+ }
+
+ @Test
+ void jsonArrayFails() {
+ Exchange exchange = template.send("direct:createItem", e ->
e.getIn().setBody("[" + JSON + "," + JSON + "]"));
+
+ IllegalArgumentException error =
assertInstanceOf(IllegalArgumentException.class, exchange.getException());
+ assertTrue(error.getMessage().contains("must be one item, but it is a
JSON array"), error.getMessage());
+ assertTrue(written.isEmpty());
+ }
+
+ @Test
+ void jsonThatIsNotOneObjectFails() {
+ Exchange exchange = template.send("direct:createItem", e ->
e.getIn().setBody("\"some text\""));
+ IllegalArgumentException error =
assertInstanceOf(IllegalArgumentException.class, exchange.getException());
+ assertTrue(error.getMessage().contains("must be a JSON object, but it
is a JSON string"), error.getMessage());
+
+ // two items as JSON lines: only the first would be written
+ exchange = template.send("direct:createItem", e ->
e.getIn().setBody(JSON + "\n" + JSON));
+ error = assertInstanceOf(IllegalArgumentException.class,
exchange.getException());
+ assertTrue(error.getMessage().contains("more than one JSON value"),
error.getMessage());
+
+ assertTrue(written.isEmpty());
+ }
+
+ @Test
+ void contentThatIsNotJsonFails() {
+ Exchange exchange = template.send("direct:createItem", e ->
e.getIn().setBody("id=1"));
+ IllegalArgumentException error =
assertInstanceOf(IllegalArgumentException.class, exchange.getException());
+ assertTrue(error.getMessage().contains("createItem operation is not
valid JSON"), error.getMessage());
+
+ // 0xff is not UTF-8
+ byte[] bytes = { '{', '"', 'i', 'd', '"', ':', '"', (byte) 0xff, '"',
'}' };
+ exchange = template.send("direct:createItem", e ->
e.getIn().setBody(bytes));
+ error = assertInstanceOf(IllegalArgumentException.class,
exchange.getException());
+ assertTrue(error.getMessage().contains("createItem operation is not
valid JSON"), error.getMessage());
+
+ assertTrue(written.isEmpty());
+ }
+
+ @Test
+ void noBodyOrAnEmptyStreamFails() {
+ Exchange exchange = template.send("direct:createItem", e ->
e.getIn().setBody(null));
+ IllegalArgumentException error =
assertInstanceOf(IllegalArgumentException.class, exchange.getException());
+ assertEquals("item cannot be empty!", error.getMessage());
+
+ exchange = template.send("direct:createItem", e ->
e.getIn().setBody(new ByteArrayInputStream(new byte[0])));
+ error = assertInstanceOf(IllegalArgumentException.class,
exchange.getException());
+ assertEquals("item cannot be empty!", error.getMessage());
+
+ assertTrue(written.isEmpty());
+ }
+
+ private Object writtenItem() {
+ assertFalse(written.isEmpty(), "no item was written");
+ return written.get(written.size() - 1);
+ }
+
+ // the fields of the document the SDK writes for the item, as its default
serializer gives them
+ private static Map<String, Object> document(Object item) {
+ return CosmosItemSerializer.DEFAULT_SERIALIZER.serialize(item);
+ }
+
+ record Item(String id, String pk) {
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index f94f49502e3e..10f5a47c1b89 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -3635,6 +3635,18 @@ and from then on the other endpoints, and the
application, failed every request
Camel now closes only the clients that its endpoints create. Close a client
you set yourself when the application no
longer needs it (a client that is a Spring bean is closed by Spring).
+=== camel-azure-cosmosdb - the JSON of an item is parsed into a Map
+
+The `createItem`, `upsertItem` and `replaceItem` operations now parse a
message body that holds the JSON of the item
+(a `String`, a `byte[]`, a stream or a file) into a `Map` before they write
it. Before, a JSON `String` or a file was
+written as a JSON string and failed, and a stream could not be serialized. A
`byte[]` body was sent as it is, and the
+message body after the operation was the written item as a `byte[]`: it is now
a `Map`, as for a `Map` body. If the
+next steps need the JSON as bytes, marshal the body, for example with
`marshal().json()`.
+
+A `List` body, or JSON that is not one object (such as a JSON array), now
fails with an `IllegalArgumentException`
+before Cosmos DB is called; before, Cosmos DB rejected it with an error 400.
These operations write one item: send each
+item in its own exchange, for example with the Split EIP.
+
=== camel-telegram - a failed send fails the exchange once
When Telegram answers a message with an error status (for example `400` for an
unknown chat), or with a body that