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 44791372edc9 CAMEL-26175: camel-odata - send JSON given as bytes, a
stream or a file as it is, fail an empty CREATE/UPDATE body, and read a
response that is not a JSON object (#29003)
44791372edc9 is described below
commit 44791372edc9dabefa71dd62f95715603a6630d6
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 07:38:40 2026 +0100
CAMEL-26175: camel-odata - send JSON given as bytes, a stream or a file as
it is, fail an empty CREATE/UPDATE body, and read a response that is not a JSON
object (#29003)
…le as it is, fail an empty CREATE/UPDATE body, and read a response that is
not a JSON object
CREATE and UPDATE passed every body that is not a String to
Jsoner.serialize, so a byte
array was sent as a JSON array of numbers and a stream or a file failed.
They are now read
as text and sent as they are. A null or empty body sent a POST or PATCH
with no content;
it now fails with a clear message. A JSON array or plain value response
failed with
"Expected an OData JSON object response"; it is now the parsed body. The
operation option
shows its READ_SET default in the catalog.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/components/odata.json | 6 +-
.../apache/camel/catalog/docs/odata-component.adoc | 15 ++-
.../org/apache/camel/component/odata/odata.json | 6 +-
.../camel-odata/src/main/docs/odata-component.adoc | 15 ++-
.../camel/component/odata/ODataConfiguration.java | 2 +-
.../camel/component/odata/ODataEndpoint.java | 25 +++--
.../apache/camel/component/odata/ODataHelper.java | 33 ++++--
.../camel/component/odata/ODataProducer.java | 54 ++++++---
.../camel/component/odata/ODataProducerTest.java | 125 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 11 ++
.../endpoint/dsl/ODataEndpointBuilderFactory.java | 2 +
11 files changed, 250 insertions(+), 44 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
index bad2888ec647..a1776b176139 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
@@ -42,15 +42,15 @@
"CamelODataSkip": { "index": 11, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The OData skip query option", "constantName":
"org.apache.camel.component.odata.ODataConstants#SKIP" }
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "CREATE", "carriers": [ "text", "map", "list" ] }, {
"option": "operation", "value": "UPDATE", "carriers": [ "text", "map", "list" ]
} ], "description": "The body is not read by READ_SET, READ_ENTRY and DELETE.
CREATE and UPDATE send a String body as the JSON payload as it is, and
serialize a Map or a List to JSON; a stream, a file or a POJO fails, and bytes
are sent as a JSON array of numbers: con [...]
- "producerOut": { "carriers": [ "map", "null" ], "description": "The JSON
object of the response parsed into a Map (a JsonObject); for READ_SET the
entities are in its value entry. No body for a 204 or an empty response, such
as of a DELETE. The response code, the ETag, the count and the next link are in
headers" }
+ "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "CREATE", "carriers": [ "text", "bytes", "stream",
"file", "map", "list" ] }, { "option": "operation", "value": "UPDATE",
"carriers": [ "text", "bytes", "stream", "file", "map", "list" ] } ],
"description": "The body is not read by READ_SET, READ_ENTRY and DELETE. CREATE
and UPDATE send JSON text as a String, bytes, a stream or a file as the JSON
payload as it is, and serialize a Map or a List to J [...]
+ "producerOut": { "carriers": [ "map", "list", "value", "text", "null" ],
"description": "The JSON object of the response parsed into a Map (a
JsonObject); for READ_SET the entities are in its value entry. A JSON array is
parsed into a List (a JsonArray), and a plain value into a number, a String or
a boolean. No body for a 204 or an empty response, such as of a DELETE. The
response code, the ETag, the count and the next link are in headers" }
},
"properties": {
"httpUri": { "index": 0, "kind": "path", "displayName": "Http Uri",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.net.URI", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The base OData service
URI" },
"count": { "index": 1, "kind": "parameter", "displayName": "Count",
"group": "producer", "label": "producer", "required": false, "type": "boolean",
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $count query parameter" },
"expand": { "index": 2, "kind": "parameter", "displayName": "Expand",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $expand query parameter" },
"filter": { "index": 3, "kind": "parameter", "displayName": "Filter",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $filter query parameter" },
- "operation": { "index": 4, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "producer", "required": false,
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation",
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ],
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData oper [...]
+ "operation": { "index": 4, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "producer", "required": false,
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation",
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ],
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
"READ_SET", "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "d [...]
"orderBy": { "index": 5, "kind": "parameter", "displayName": "Order By",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $orderby query parameter" },
"select": { "index": 6, "kind": "parameter", "displayName": "Select",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $select query parameter" },
"skip": { "index": 7, "kind": "parameter", "displayName": "Skip", "group":
"producer", "label": "producer", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $skip query parameter" },
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
index 3a11d856ebe7..657e5e417193 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
@@ -122,6 +122,18 @@ For example, a string key can be supplied as `'ABC-123'`,
while a composite key
The key value should be a valid and trusted OData key expression.
+=== Message Body
+
+`CREATE` and `UPDATE` send the JSON of the entity from the message body:
+
+* JSON text as a `String`, a byte array, a stream or a file is sent as it is.
+* A `Map` or a `List` is serialized to JSON.
+
+Other Java objects (POJOs) are not serialized: marshal them to JSON first, for
example with the JSON data format.
+An empty body fails, as these operations need an entity: set the body to `{}`
to send an empty JSON object.
+
+The other operations do not read the message body.
+
== Message Headers
The following message headers can be used to control OData processing
dynamically.
@@ -447,7 +459,8 @@ exchange.getMessage().getHeader("CamelODataCount");
exchange.getMessage().getHeader("CamelODataNextLink");
----
-The response body contains the parsed JSON response.
+The response body contains the parsed JSON response: a JSON object is a `Map`,
a JSON array a `List`,
+and a plain value a number, a `String` or a boolean. A `204` or an empty
response gives no body.
== SSL Configuration
diff --git
a/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
b/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
index bad2888ec647..a1776b176139 100644
---
a/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
+++
b/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
@@ -42,15 +42,15 @@
"CamelODataSkip": { "index": 11, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The OData skip query option", "constantName":
"org.apache.camel.component.odata.ODataConstants#SKIP" }
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "CREATE", "carriers": [ "text", "map", "list" ] }, {
"option": "operation", "value": "UPDATE", "carriers": [ "text", "map", "list" ]
} ], "description": "The body is not read by READ_SET, READ_ENTRY and DELETE.
CREATE and UPDATE send a String body as the JSON payload as it is, and
serialize a Map or a List to JSON; a stream, a file or a POJO fails, and bytes
are sent as a JSON array of numbers: con [...]
- "producerOut": { "carriers": [ "map", "null" ], "description": "The JSON
object of the response parsed into a Map (a JsonObject); for READ_SET the
entities are in its value entry. No body for a 204 or an empty response, such
as of a DELETE. The response code, the ETag, the count and the next link are in
headers" }
+ "producerIn": { "carriers": [ "any" ], "byOption": [ { "option":
"operation", "value": "CREATE", "carriers": [ "text", "bytes", "stream",
"file", "map", "list" ] }, { "option": "operation", "value": "UPDATE",
"carriers": [ "text", "bytes", "stream", "file", "map", "list" ] } ],
"description": "The body is not read by READ_SET, READ_ENTRY and DELETE. CREATE
and UPDATE send JSON text as a String, bytes, a stream or a file as the JSON
payload as it is, and serialize a Map or a List to J [...]
+ "producerOut": { "carriers": [ "map", "list", "value", "text", "null" ],
"description": "The JSON object of the response parsed into a Map (a
JsonObject); for READ_SET the entities are in its value entry. A JSON array is
parsed into a List (a JsonArray), and a plain value into a number, a String or
a boolean. No body for a 204 or an empty response, such as of a DELETE. The
response code, the ETag, the count and the next link are in headers" }
},
"properties": {
"httpUri": { "index": 0, "kind": "path", "displayName": "Http Uri",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.net.URI", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The base OData service
URI" },
"count": { "index": 1, "kind": "parameter", "displayName": "Count",
"group": "producer", "label": "producer", "required": false, "type": "boolean",
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false,
"secret": false, "defaultValue": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $count query parameter" },
"expand": { "index": 2, "kind": "parameter", "displayName": "Expand",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $expand query parameter" },
"filter": { "index": 3, "kind": "parameter", "displayName": "Filter",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $filter query parameter" },
- "operation": { "index": 4, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "producer", "required": false,
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation",
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ],
"deprecated": false, "autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData oper [...]
+ "operation": { "index": 4, "kind": "parameter", "displayName":
"Operation", "group": "producer", "label": "producer", "required": false,
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation",
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ],
"deprecated": false, "autowired": false, "secret": false, "defaultValue":
"READ_SET", "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "d [...]
"orderBy": { "index": 5, "kind": "parameter", "displayName": "Order By",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $orderby query parameter" },
"select": { "index": 6, "kind": "parameter", "displayName": "Select",
"group": "producer", "label": "producer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $select query parameter" },
"skip": { "index": 7, "kind": "parameter", "displayName": "Skip", "group":
"producer", "label": "producer", "required": false, "type": "integer",
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false,
"secret": false, "configurationClass":
"org.apache.camel.component.odata.ODataConfiguration", "configurationField":
"configuration", "description": "The OData $skip query parameter" },
diff --git a/components/camel-odata/src/main/docs/odata-component.adoc
b/components/camel-odata/src/main/docs/odata-component.adoc
index 3a11d856ebe7..657e5e417193 100644
--- a/components/camel-odata/src/main/docs/odata-component.adoc
+++ b/components/camel-odata/src/main/docs/odata-component.adoc
@@ -122,6 +122,18 @@ For example, a string key can be supplied as `'ABC-123'`,
while a composite key
The key value should be a valid and trusted OData key expression.
+=== Message Body
+
+`CREATE` and `UPDATE` send the JSON of the entity from the message body:
+
+* JSON text as a `String`, a byte array, a stream or a file is sent as it is.
+* A `Map` or a `List` is serialized to JSON.
+
+Other Java objects (POJOs) are not serialized: marshal them to JSON first, for
example with the JSON data format.
+An empty body fails, as these operations need an entity: set the body to `{}`
to send an empty JSON object.
+
+The other operations do not read the message body.
+
== Message Headers
The following message headers can be used to control OData processing
dynamically.
@@ -447,7 +459,8 @@ exchange.getMessage().getHeader("CamelODataCount");
exchange.getMessage().getHeader("CamelODataNextLink");
----
-The response body contains the parsed JSON response.
+The response body contains the parsed JSON response: a JSON object is a `Map`,
a JSON array a `List`,
+and a plain value a number, a `String` or a boolean. A `204` or an empty
response gives no body.
== SSL Configuration
diff --git
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
index 807e5357e39c..0a60e60ed34c 100644
---
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
+++
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
@@ -23,7 +23,7 @@ import org.apache.camel.support.jsse.SSLContextParameters;
@UriParams
public class ODataConfiguration {
- @UriParam(label = "producer", description = "The OData operation to
perform")
+ @UriParam(label = "producer", defaultValue = "READ_SET", description =
"The OData operation to perform")
private ODataOperation operation = ODataOperation.READ_SET;
@UriParam(label = "producer", description = "The OData $filter query
parameter")
diff --git
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
index 6259aae79fda..889319966003 100644
---
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
+++
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
@@ -50,19 +50,24 @@ import org.apache.camel.support.jsse.SSLContextParameters;
@BodyTypes(producerIn = @BodyType(value = Carrier.ANY,
byOption = {
@BodyType.When(option = "operation",
value = "CREATE",
- carriers = {
Carrier.TEXT, Carrier.MAP, Carrier.LIST }),
+ carriers = {
+ Carrier.TEXT,
Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
+ Carrier.MAP,
Carrier.LIST }),
@BodyType.When(option = "operation",
value = "UPDATE",
- carriers = {
Carrier.TEXT, Carrier.MAP, Carrier.LIST }) },
+ carriers = {
+ Carrier.TEXT,
Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
+ Carrier.MAP,
Carrier.LIST }) },
description = "The body is not read by
READ_SET, READ_ENTRY and DELETE. CREATE and UPDATE"
- + " send a String body as the
JSON payload as it is, and serialize a Map or a"
- + " List to JSON; a stream, a
file or a POJO fails, and bytes are sent as a"
- + " JSON array of numbers:
convert them to a String first. The operation can"
- + " also be set with the
CamelODataOperation header"),
- producerOut = @BodyType(value = { Carrier.MAP, Carrier.NULL },
+ + " send JSON text as a
String, bytes, a stream or a file as the JSON payload"
+ + " as it is, and serialize a
Map or a List to JSON; a POJO fails, and so does"
+ + " an empty body (use {} for
an empty JSON object). The operation can also be"
+ + " set with the
CamelODataOperation header"),
+ producerOut = @BodyType(value = { Carrier.MAP, Carrier.LIST,
Carrier.VALUE, Carrier.TEXT, Carrier.NULL },
description = "The JSON object of the
response parsed into a Map (a JsonObject); for"
- + " READ_SET the entities are
in its value entry. No body for a 204 or an"
- + " empty response, such as
of a DELETE. The response code, the ETag, the"
- + " count and the next link
are in headers"))
+ + " READ_SET the entities are
in its value entry. A JSON array is parsed"
+ + " into a List (a
JsonArray), and a plain value into a number, a String or a"
+ + " boolean. No body for a
204 or an empty response, such as of a DELETE."
+ + " The response code, the
ETag, the count and the next link are in headers"))
public class ODataEndpoint extends DefaultEndpoint implements
EndpointServiceLocation, SSLContextParametersAware {
@UriPath
diff --git
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
index 1376e7a66a22..9b5e4105fb7b 100644
---
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
+++
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
@@ -16,8 +16,13 @@
*/
package org.apache.camel.component.odata;
-import java.util.Map;
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.file.Path;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
import org.apache.camel.util.json.DeserializationException;
import org.apache.camel.util.json.Jsoner;
@@ -38,17 +43,25 @@ public final class ODataHelper {
return Jsoner.serialize(value);
}
- @SuppressWarnings("unchecked")
- public static Map<String, Object> parseJsonObject(String value) throws
DeserializationException {
- if (value == null || value.isBlank()) {
- return Map.of();
- }
+ /**
+ * Whether the body is a payload that holds the JSON text as it is: bytes,
a stream or a file. It is read as text
+ * and sent as it is, as a String body is, and not serialized to JSON.
+ */
+ public static boolean isPayload(Object value) {
+ return value instanceof byte[]
+ || value instanceof InputStream || value instanceof Reader ||
value instanceof StreamCache
+ || value instanceof File || value instanceof Path || value
instanceof WrappedFile;
+ }
- Object parsed = Jsoner.deserialize(value);
- if (!(parsed instanceof Map)) {
- throw new IllegalArgumentException("Expected an OData JSON object
response");
+ /**
+ * Parses the JSON of a response: a JSON object is a Map, a JSON array a
List, and a plain value a number, a String
+ * or a boolean.
+ */
+ public static Object parseJson(String value) throws
DeserializationException {
+ if (value == null || value.isBlank()) {
+ return null;
}
- return (Map<String, Object>) parsed;
+ return Jsoner.deserialize(value);
}
}
diff --git
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
index fb717a00242c..61294bc73b12 100644
---
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
+++
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
@@ -25,6 +25,7 @@ import java.util.Map;
import org.apache.camel.Component;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
import org.apache.camel.Producer;
import org.apache.camel.component.http.HttpConstants;
import org.apache.camel.support.DefaultProducer;
@@ -151,12 +152,12 @@ public class ODataProducer extends DefaultProducer {
case CREATE -> {
httpExchange.getMessage().setHeader(HttpConstants.HTTP_METHOD,
"POST");
httpExchange.getMessage().setHeader(Exchange.CONTENT_TYPE,
"application/json");
-
httpExchange.getMessage().setBody(ODataHelper.toJson(exchange.getMessage().getBody()));
+ httpExchange.getMessage().setBody(toJsonPayload(exchange,
operation));
}
case UPDATE -> {
httpExchange.getMessage().setHeader(HttpConstants.HTTP_METHOD,
"PATCH");
httpExchange.getMessage().setHeader(Exchange.CONTENT_TYPE,
"application/json");
-
httpExchange.getMessage().setBody(ODataHelper.toJson(exchange.getMessage().getBody()));
+ httpExchange.getMessage().setBody(toJsonPayload(exchange,
operation));
}
case DELETE -> {
httpExchange.getMessage().setHeader(HttpConstants.HTTP_METHOD,
"DELETE");
@@ -200,22 +201,25 @@ public class ODataProducer extends DefaultProducer {
return;
}
- Map<String, Object> response = ODataHelper.parseJsonObject(body);
+ Object response = ODataHelper.parseJson(body);
- Object count = response.get("@odata.count");
- if (count instanceof Number number) {
- exchange.getMessage().setHeader(ODataConstants.COUNT,
number.longValue());
- }
+ // a JSON array or a plain value is the body as it is: only a JSON
object has the OData annotations
+ if (response instanceof Map<?, ?> map) {
+ Object count = map.get("@odata.count");
+ if (count instanceof Number number) {
+ exchange.getMessage().setHeader(ODataConstants.COUNT,
number.longValue());
+ }
- Object nextLink = response.get("@odata.nextLink");
- if (nextLink != null) {
- exchange.getMessage().setHeader(ODataConstants.NEXT_LINK,
nextLink.toString());
- }
+ Object nextLink = map.get("@odata.nextLink");
+ if (nextLink != null) {
+ exchange.getMessage().setHeader(ODataConstants.NEXT_LINK,
nextLink.toString());
+ }
- if (responseEtag == null) {
- Object odataEtag = response.get("@odata.etag");
- if (odataEtag != null) {
- responseEtag = odataEtag.toString();
+ if (responseEtag == null) {
+ Object odataEtag = map.get("@odata.etag");
+ if (odataEtag != null) {
+ responseEtag = odataEtag.toString();
+ }
}
}
@@ -226,6 +230,26 @@ public class ODataProducer extends DefaultProducer {
exchange.getMessage().setBody(response);
}
+ /**
+ * The JSON of the entity that CREATE and UPDATE send: JSON text as bytes,
a stream or a file is sent as it is, as a
+ * String is, and a Map or a List is serialized to JSON. These operations
need an entity, so an empty body fails.
+ */
+ private static String toJsonPayload(Exchange exchange, ODataOperation
operation) throws InvalidPayloadException {
+ Object body = exchange.getMessage().getBody();
+ String json;
+ if (ODataHelper.isPayload(body)) {
+ json = exchange.getMessage().getMandatoryBody(String.class);
+ } else {
+ json = ODataHelper.toJson(body);
+ }
+ if (json == null || json.isBlank()) {
+ throw new IllegalArgumentException(
+ "The " + operation + " operation needs the JSON of the
entity as the body, but the body is empty:"
+ + " set the body to {} to send
an empty JSON object");
+ }
+ return json;
+ }
+
private String buildTargetUri(Exchange exchange, ODataOperation operation)
throws Exception {
String base = endpoint.getHttpUri().toString();
diff --git
a/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
b/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
index 855c05e1836a..94102786e804 100644
---
a/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
+++
b/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
@@ -16,12 +16,17 @@
*/
package org.apache.camel.component.odata;
+import java.io.ByteArrayInputStream;
+import java.io.File;
import java.math.BigDecimal;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -45,6 +50,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
@@ -57,9 +63,11 @@ import static
com.github.tomakehurst.wiremock.client.WireMock.matching;
import static com.github.tomakehurst.wiremock.client.WireMock.patch;
import static
com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
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.urlPathEqualTo;
import static
com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -1143,4 +1151,121 @@ public class ODataProducerTest extends CamelTestSupport
{
wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
.withHeader("Authorization", equalTo(expected2)));
}
+
+ @Test
+ void testProcessCreateEntityWithBytesBody() throws Exception {
+ wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+
.withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}"))
+ .willReturn(aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":103,\"Name\":\"Tablet\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION,
ODataOperation.CREATE);
+
exchange.getMessage().setBody("{\"Name\":\"Tablet\",\"Price\":299.99}".getBytes(StandardCharsets.UTF_8));
+
+ producer.process(exchange);
+
+ assertEquals(201,
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(postRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Content-Type", containing("application/json"))
+
.withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}")));
+ }
+
+ @Test
+ void testProcessCreateEntityWithStreamBody() throws Exception {
+ wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+ .withRequestBody(equalToJson("{\"Name\":\"Tablet\"}"))
+ .willReturn(aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":103,\"Name\":\"Tablet\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION,
ODataOperation.CREATE);
+ exchange.getMessage().setBody(
+ new
ByteArrayInputStream("{\"Name\":\"Tablet\"}".getBytes(StandardCharsets.UTF_8)));
+
+ producer.process(exchange);
+
+ assertEquals(201,
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertEquals("Tablet", body.get("Name"));
+ }
+
+ @Test
+ void testProcessUpdateEntityWithFileBody(@TempDir Path dir) throws
Exception {
+ File file = dir.resolve("product.json").toFile();
+ Files.writeString(file.toPath(), "{\"Name\":\"Updated Laptop\"}");
+
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION,
ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+ exchange.getMessage().setBody(file);
+
+ producer.process(exchange);
+
+ assertEquals(204,
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated
Laptop\"}")));
+ }
+
+ @Test
+ void testProcessCreateEntityWithNullBodyFails() {
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION,
ODataOperation.CREATE);
+
+ IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> producer.process(exchange));
+ assertTrue(e.getMessage().contains("CREATE operation needs the JSON of
the entity"), e.getMessage());
+
+ wireMock.verify(0,
postRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessUpdateEntityWithEmptyBodyFails() {
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION,
ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+ exchange.getMessage().setBody(new byte[0]);
+
+ IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> producer.process(exchange));
+ assertTrue(e.getMessage().contains("UPDATE operation needs the JSON of
the entity"), e.getMessage());
+
+ wireMock.verify(0,
patchRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ }
+
+ @Test
+ void testProcessJsonArrayResponse() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[{\"ID\":101},{\"ID\":102}]")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ List<?> body = assertInstanceOf(List.class,
exchange.getMessage().getBody());
+ assertEquals(2, body.size());
+ assertNull(exchange.getMessage().getHeader(ODataConstants.COUNT));
+ }
+
+ @Test
+ void testProcessPlainValueResponse() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/plain")
+ .withBody("42")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(new BigDecimal(42), exchange.getMessage().getBody());
+ }
}
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 6e5da218ad82..958cee6549cd 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
@@ -1963,6 +1963,17 @@ When no HTTP response can be built from the reply of the
route (for example the
now answers with HTTP Status 500 and closes the connection. The body is empty,
or the stack trace of the exception with
`muteException=false`. Before, no response was sent and the client waited
until it timed out.
+=== camel-odata - the body of CREATE and UPDATE, and a response that is not a
JSON object
+
+`CREATE` and `UPDATE` send JSON text given as a byte array, a stream or a file
as it is, as they do for a `String`.
+Before, a byte array was sent as a JSON array of numbers, and a stream or a
file failed.
+
+`CREATE` and `UPDATE` now fail with an `IllegalArgumentException` when the
body is null or empty. Before, they sent a
+`POST` or `PATCH` request with no content. Set the body to `{}` to send an
empty JSON object.
+
+A response that is a JSON array or a plain value is now the body as it is
parsed (a `List`, a number, a `String` or a
+boolean). Before, the producer failed with `Expected an OData JSON object
response`.
+
=== camel-telegram - webhook requests are verified with a secret token
In webhook mode, Camel now registers the webhook with a secret token, which
Telegram sends in the
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
index ff100ccf25b1..b2f7ad921456 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
@@ -109,6 +109,7 @@ public interface ODataEndpointBuilderFactory {
* The option is a:
* <code>org.apache.camel.component.odata.ODataOperation</code> type.
*
+ * Default: READ_SET
* Group: producer
*
* @param operation the value to set
@@ -124,6 +125,7 @@ public interface ODataEndpointBuilderFactory {
* The option will be converted to a
* <code>org.apache.camel.component.odata.ODataOperation</code> type.
*
+ * Default: READ_SET
* Group: producer
*
* @param operation the value to set