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 fe77c8e2bc3e CAMEL-26144: camel-cxfrs - reply with an unchanged
request body and send a whole List body (#28923)
fe77c8e2bc3e is described below
commit fe77c8e2bc3ea582f4c2d98b7d0d516822ee1a72
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:47:34 2026 +0100
CAMEL-26144: camel-cxfrs - reply with an unchanged request body and send a
whole List body (#28923)
* CAMEL-26144: camel-cxfrs - reply with an unchanged request body and s…
…end a whole List body
With the Default binding a route that did not set a reply body replied with
the request MessageContentsList, which failed with HTTP 500 "No message body
writer has been found". Now a MessageContentsList of one element is the
response entity and an empty one replies with no content, unless the
resource
method returns a collection. The Simple binding is unchanged.
The HTTP API producer sent only the first element of a List body. Now a List
is sent as a whole; of a MessageContentsList the first element is still
sent,
and an empty one sends no body instead of failing.
* CAMEL-26144: camel-cxfrs - send a primitive array body as a whole
The HTTP API producer cast any array body other than byte[] to Object[] to
take its first element, so a primitive array such as int[] failed with a
ClassCastException. Now a primitive array is sent as a whole, like byte[].
An Object[] still sends its first element, as a MessageContentsList does,
and the docs say so.
* CAMEL-26144: camel-cxfrs - document that a MessageContentsList of sev…
…eral parameters still fails
The component page and the upgrade guide now say that an unchanged request
MessageContentsList of
more than one parameter still fails with HTTP 500, and the route must set
the reply body.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/components/cxfrs.json | 4 +-
.../apache/camel/catalog/docs/cxfrs-component.adoc | 11 ++-
.../apache/camel/component/cxf/jaxrs/cxfrs.json | 4 +-
.../src/main/docs/cxfrs-component.adoc | 11 ++-
.../camel/component/cxf/jaxrs/CxfRsEndpoint.java | 12 ++-
.../component/cxf/jaxrs/DefaultCxfRsBinding.java | 40 +++++++--
.../component/cxf/jaxrs/SimpleCxfRsBinding.java | 3 +-
.../cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java | 99 ++++++++++++++++++++++
.../cxf/jaxrs/DefaultCxfRsBindingTest.java | 89 +++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 15 ++++
10 files changed, 268 insertions(+), 20 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
index abaef59b426f..e92f191a6a67 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
@@ -52,8 +52,8 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "list" ], "javaType":
"org.apache.cxf.message.MessageContentsList", "dependsOn": [
"performInvocation" ], "byOption": [ { "option": "bindingStyle", "value":
"SimpleConsumer", "carriers": [ "unknown" ] }, { "option": "bindingStyle",
"value": "Custom", "carriers": [ "unknown" ] } ], "description": "Default: a
MessageContentsList of the parameters of the resource method, with its name in
the CamelCxfOperationName header. SimpleConsumer: the request entity [...]
- "consumerReply": { "carriers": [ "null", "text", "bytes", "stream",
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom",
"carriers": [ "any" ] } ], "description": "The response entity: a
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a
List fails without a provider for it. With the Default binding the request
MessageContentsList fails: set the reply body" },
- "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file",
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false",
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description":
"The request entity, written for the Content-Type header: text, bytes, a
stream, a file or an object a JAX-RS provider writes; of a List only the first
element is sent, and a Map fails. A GET sends no body. With
httpClientAPI=false: the parameters of the resource meth [...]
+ "consumerReply": { "carriers": [ "null", "text", "bytes", "stream",
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom",
"carriers": [ "any" ] } ], "description": "The response entity: a
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a
List fails without a provider for it. With the Default binding an unchanged
request MessageContentsList replies with its [...]
+ "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file",
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false",
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description":
"The request entity, written for the Content-Type header: text, bytes, a
stream, a file or an object a JAX-RS provider writes, such as a List as a whole
(of a MessageContentsList or an Object[] only the first element is sent); a Map
fails. A GET sends no body. With ht [...]
"producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option":
"httpClientAPI", "value": "false", "carriers": [ "unknown" ] } ],
"description": "A jakarta.ws.rs.core.Response whose entity is the response
stream, read once (it converts to a String or an InputStream); the entity read
as the class in the CamelCxfRsResponseClass header when that is set. With
httpClientAPI=false: the return value of the resource method, null for a void
one" }
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
index 94a2892c2e33..b67f7f19e031 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
@@ -101,7 +101,11 @@ The `Default` binding style is rather low-level, requiring
the user to
manually process the `MessageContentsList` object coming into the route.
Thus, it tightly couples the route logic with the method signature and
parameter indices of the JAX-RS operation. Somewhat inelegant, difficult
-and error-prone.
+and error-prone. When a route with the `Default` binding style does not set
+a reply body, the request `MessageContentsList` of one parameter is the
+response entity, and one without parameters replies with no content (204),
+unless the resource method returns a collection. A `MessageContentsList` of
+several parameters fails with HTTP 500, so set the reply body in the route.
In contrast, the `SimpleConsumer` binding style performs the following
mappings, to *make the request data more accessible* to you
@@ -425,6 +429,11 @@ let the producer use the http centric client API by using
the URI option
You can turn the response object to the type class specified with the
message
header `CamelCxfRsResponseClass`.
+The message body is the request entity (a GET sends none): a `List` or an
+array of primitives (such as `byte[]` or `int[]`) is sent as a whole, and of a
+`MessageContentsList` (such as the body from a `cxfrs` consumer with the
+`Default` binding style) or an `Object[]` only the first element is sent, as
+they carry the parameters of a method call.
._Java-only: Java test API (ProducerTemplate with inline Processor)_
[source,java]
diff --git
a/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
b/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
index abaef59b426f..e92f191a6a67 100644
---
a/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
+++
b/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
@@ -52,8 +52,8 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "list" ], "javaType":
"org.apache.cxf.message.MessageContentsList", "dependsOn": [
"performInvocation" ], "byOption": [ { "option": "bindingStyle", "value":
"SimpleConsumer", "carriers": [ "unknown" ] }, { "option": "bindingStyle",
"value": "Custom", "carriers": [ "unknown" ] } ], "description": "Default: a
MessageContentsList of the parameters of the resource method, with its name in
the CamelCxfOperationName header. SimpleConsumer: the request entity [...]
- "consumerReply": { "carriers": [ "null", "text", "bytes", "stream",
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom",
"carriers": [ "any" ] } ], "description": "The response entity: a
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a
List fails without a provider for it. With the Default binding the request
MessageContentsList fails: set the reply body" },
- "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file",
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false",
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description":
"The request entity, written for the Content-Type header: text, bytes, a
stream, a file or an object a JAX-RS provider writes; of a List only the first
element is sent, and a Map fails. A GET sends no body. With
httpClientAPI=false: the parameters of the resource meth [...]
+ "consumerReply": { "carriers": [ "null", "text", "bytes", "stream",
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom",
"carriers": [ "any" ] } ], "description": "The response entity: a
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a
List fails without a provider for it. With the Default binding an unchanged
request MessageContentsList replies with its [...]
+ "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file",
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false",
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description":
"The request entity, written for the Content-Type header: text, bytes, a
stream, a file or an object a JAX-RS provider writes, such as a List as a whole
(of a MessageContentsList or an Object[] only the first element is sent); a Map
fails. A GET sends no body. With ht [...]
"producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option":
"httpClientAPI", "value": "false", "carriers": [ "unknown" ] } ],
"description": "A jakarta.ws.rs.core.Response whose entity is the response
stream, read once (it converts to a String or an InputStream); the entity read
as the class in the CamelCxfRsResponseClass header when that is set. With
httpClientAPI=false: the return value of the resource method, null for a void
one" }
},
"properties": {
diff --git
a/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc
b/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc
index 94a2892c2e33..b67f7f19e031 100644
--- a/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc
+++ b/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc
@@ -101,7 +101,11 @@ The `Default` binding style is rather low-level, requiring
the user to
manually process the `MessageContentsList` object coming into the route.
Thus, it tightly couples the route logic with the method signature and
parameter indices of the JAX-RS operation. Somewhat inelegant, difficult
-and error-prone.
+and error-prone. When a route with the `Default` binding style does not set
+a reply body, the request `MessageContentsList` of one parameter is the
+response entity, and one without parameters replies with no content (204),
+unless the resource method returns a collection. A `MessageContentsList` of
+several parameters fails with HTTP 500, so set the reply body in the route.
In contrast, the `SimpleConsumer` binding style performs the following
mappings, to *make the request data more accessible* to you
@@ -425,6 +429,11 @@ let the producer use the http centric client API by using
the URI option
You can turn the response object to the type class specified with the
message
header `CamelCxfRsResponseClass`.
+The message body is the request entity (a GET sends none): a `List` or an
+array of primitives (such as `byte[]` or `int[]`) is sent as a whole, and of a
+`MessageContentsList` (such as the body from a `cxfrs` consumer with the
+`Default` binding style) or an `Object[]` only the first element is sent, as
+they carry the parameters of a method call.
._Java-only: Java test API (ProducerTemplate with inline Processor)_
[source,java]
diff --git
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
index aede2015b59e..a50c8e0c89e2 100644
---
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
+++
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
@@ -98,8 +98,10 @@ import static
org.apache.camel.component.cxf.common.message.CxfConstants.SCHEME_
description = "The response entity: a
jakarta.ws.rs.core.Response, or text, bytes, a"
+ " stream, a file, or an
object a JAX-RS provider writes for the"
+ " Content-Type (a JAXB
bean as XML); a Map or a List fails without a"
- + " provider for it. With
the Default binding the request"
- + " MessageContentsList
fails: set the reply body"),
+ + " provider for it. With
the Default binding an unchanged request"
+ + " MessageContentsList
replies with its single element, or no content"
+ + " when empty (unless the
method returns a collection); one of several"
+ + " parameters fails: set
the reply body"),
producerIn = @BodyType(value = {
Carrier.NULL, Carrier.TEXT, Carrier.BYTES, Carrier.STREAM,
Carrier.FILE,
Carrier.POJO },
@@ -108,8 +110,10 @@ import static
org.apache.camel.component.cxf.common.message.CxfConstants.SCHEME_
Carrier.LIST, Carrier.TEXT, Carrier.VALUE,
Carrier.POJO, Carrier.NULL }),
description = "The request entity, written
for the Content-Type header: text, bytes, a"
- + " stream, a file or an
object a JAX-RS provider writes; of a List only"
- + " the first element is sent,
and a Map fails. A GET sends no body. With"
+ + " stream, a file or an
object a JAX-RS provider writes, such as a List as"
+ + " a whole (of a
MessageContentsList or an Object[] only the first element"
+ + " is sent); a"
+ + " Map fails. A GET sends no
body. With"
+ " httpClientAPI=false: the
parameters of the resource method named by the"
+ " CamelCxfOperationName
header, as a List or the one parameter"),
producerOut = @BodyType(value = Carrier.POJO,
diff --git
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
index 296d548ba070..7435e3868182 100644
---
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
+++
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.cxf.jaxrs;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -47,6 +48,7 @@ import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.HttpHeaderHelper;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.jaxrs.model.OperationResourceInfo;
import org.apache.cxf.jaxrs.model.OperationResourceInfoStack;
import org.apache.cxf.message.MessageContentsList;
import org.apache.cxf.security.LoginSecurityContext;
@@ -70,6 +72,28 @@ public class DefaultCxfRsBinding implements CxfRsBinding,
HeaderFilterStrategyAw
Exchange camelExchange,
org.apache.cxf.message.Exchange cxfExchange)
throws Exception {
+ Object o = doPopulateCxfRsResponseFromExchange(camelExchange,
cxfExchange);
+ if (o instanceof MessageContentsList mcl && mcl.size() <= 1 &&
!isCollectionReturnType(cxfExchange)) {
+ // the request parameters are still the body (the route did not
set a reply), and CXF has no
+ // message body writer for a MessageContentsList: reply with its
single element, or no content
+ return mcl.isEmpty() ? null : mcl.get(0);
+ }
+ return o;
+ }
+
+ private static boolean
isCollectionReturnType(org.apache.cxf.message.Exchange cxfExchange) {
+ // a resource method that returns a collection writes the
MessageContentsList as that collection
+ OperationResourceInfo ori = cxfExchange != null ?
cxfExchange.get(OperationResourceInfo.class) : null;
+ return ori != null &&
Collection.class.isAssignableFrom(ori.getMethodToInvoke().getReturnType());
+ }
+
+ /**
+ * Populates the CXF exchange from the Camel response message and returns
its body as is.
+ */
+ protected Object doPopulateCxfRsResponseFromExchange(
+ Exchange camelExchange,
+ org.apache.cxf.message.Exchange cxfExchange)
+ throws Exception {
// Need to check if the exchange has the exception
if (camelExchange.isFailed() && camelExchange.getException() != null) {
throw camelExchange.getException();
@@ -209,8 +233,9 @@ public class DefaultCxfRsBinding implements CxfRsBinding,
HeaderFilterStrategyAw
/**
* This method call Message.getBody({@link MessageContentsList}) to allow
an appropriate converter to kick in even
- * through we only read the first element off the MessageContextList. If
that returns null, we check the body to see
- * if it is a List or an array and then return the first element. If that
fails, we will simply return the object.
+ * through we only read the first element off the MessageContextList (no
body when it is empty). Any other List is
+ * the request entity as a whole. If the body is an array of objects, we
return its first element; an array of
+ * primitives (such as byte[]) is the request entity as a whole.
Otherwise, we will simply return the object.
*/
@Override
public Object bindCamelMessageBodyToRequestBody(Message camelMessage,
Exchange camelExchange)
@@ -218,16 +243,13 @@ public class DefaultCxfRsBinding implements CxfRsBinding,
HeaderFilterStrategyAw
Object request = camelMessage.getBody(MessageContentsList.class);
if (request instanceof MessageContentsList mcl) {
- return mcl.get(0);
+ return mcl.isEmpty() ? null : mcl.get(0);
}
request = camelMessage.getBody();
- if (request instanceof List) {
- request = ((List<?>) request).get(0);
- } else if (request instanceof byte[] byteArray) {
- return byteArray;
- } else if (request != null && request.getClass().isArray()) {
- request = ((Object[]) request)[0];
+ if (request instanceof Object[] array && array.length > 0) {
+ // like a MessageContentsList, an array of objects carries the
entity first
+ request = array[0];
}
return request;
diff --git
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
index fd9f6c108573..5422d6a3135b 100644
---
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
+++
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
@@ -154,7 +154,8 @@ public class SimpleCxfRsBinding extends DefaultCxfRsBinding
{
@Override
public Object populateCxfRsResponseFromExchange(org.apache.camel.Exchange
camelExchange, Exchange cxfExchange)
throws Exception {
- Object base = super.populateCxfRsResponseFromExchange(camelExchange,
cxfExchange);
+ // the request MessageContentsList is not unwrapped: buildResponse
replies without an entity for it
+ Object base = super.doPopulateCxfRsResponseFromExchange(camelExchange,
cxfExchange);
return buildResponse(camelExchange, base);
}
diff --git
a/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java
new file mode 100644
index 000000000000..63d3db9085c8
--- /dev/null
+++
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.cxf.jaxrs;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.cxf.jaxrs.response.MyResponse;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * With the Default binding a route that does not set a reply body replies
with the request entity instead of failing on
+ * the request MessageContentsList.
+ */
+public class CxfRsConsumerUnchangedBodyTest extends CamelTestSupport {
+ @RegisterExtension
+ static final AvailablePortFinder.Port PORT = AvailablePortFinder.find();
+
+ private static final String PUT_REQUEST =
"<Customer><name>Mary</name><id>123</id></Customer>";
+ private static final String TEST_URL
+ = "http://localhost:" + PORT.getPort() +
"/CxfRsConsumerUnchangedBodyTest/customerservice/customers";
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ from("cxfrs://http://localhost:" + PORT.getPort() +
"/CxfRsConsumerUnchangedBodyTest"
+ +
"?resourceClasses=org.apache.camel.component.cxf.jaxrs.testbean.CustomerService")
+ // the route does not change the body
+ .log("${header.operationName}")
+ .to("mock:result");
+ }
+ };
+ }
+
+ @Test
+ public void testUnchangedEntityIsTheReply() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedMessageCount(1);
+
+ HttpPut put = new HttpPut(TEST_URL);
+ put.setEntity(new StringEntity(PUT_REQUEST,
ContentType.parse("text/xml; charset=ISO-8859-1")));
+ put.addHeader("Accept", "text/xml");
+
+ try (CloseableHttpClient httpclient =
HttpClientBuilder.create().build()) {
+ MyResponse httpResponse = httpclient.execute(put,
+ response -> new MyResponse(response.getCode(),
EntityUtils.toString(response.getEntity())));
+ assertEquals(200, httpResponse.status(), httpResponse.toString());
+ assertTrue(httpResponse.content().contains("<name>Mary</name>"),
httpResponse.content());
+ assertTrue(httpResponse.content().contains("<id>123</id>"),
httpResponse.content());
+ }
+
+ mock.assertIsSatisfied();
+ }
+
+ @Test
+ public void testUnchangedEmptyParametersOfCollectionMethod() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedMessageCount(1);
+
+ // getCustomers() returns a List, so the empty MessageContentsList is
written as an empty collection
+ HttpGet get = new HttpGet(TEST_URL);
+
+ try (CloseableHttpClient httpclient =
HttpClientBuilder.create().build()) {
+ MyResponse httpResponse = httpclient.execute(get,
+ response -> new MyResponse(response.getCode(),
EntityUtils.toString(response.getEntity())));
+ assertEquals(200, httpResponse.status(), httpResponse.toString());
+
assertTrue(httpResponse.content().contains("<Customers></Customers>"),
httpResponse.content());
+ }
+
+ mock.assertIsSatisfied();
+ }
+}
diff --git
a/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
index 25466ddd5229..ccc8f4e8c251 100644
---
a/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
+++
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
@@ -25,8 +25,10 @@ import java.util.Map;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
import org.apache.camel.Message;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.support.DefaultExchange;
@@ -34,11 +36,16 @@ import org.apache.camel.support.DefaultHeaderFilterStrategy;
import org.apache.camel.support.DefaultMessage;
import org.apache.camel.support.ExchangeHelper;
import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.MessageContentsList;
import org.apache.cxf.message.MessageImpl;
import org.junit.jupiter.api.Test;
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;
public class DefaultCxfRsBindingTest {
@@ -104,4 +111,86 @@ public class DefaultCxfRsBindingTest {
assertNull(camelMessage.getHeader("zeroSizeList"), "We should get
nothing here");
}
+ @Test
+ public void testResponseUnwrapsSingleElementMessageContentsList() throws
Exception {
+ DefaultCxfRsBinding binding = new DefaultCxfRsBinding();
+
+ assertEquals("Mary", binding.populateCxfRsResponseFromExchange(
+ replyExchange(new MessageContentsList("Mary")), new
ExchangeImpl()));
+ // no content when the resource method has no parameter
+ assertNull(binding.populateCxfRsResponseFromExchange(
+ replyExchange(new MessageContentsList()), new ExchangeImpl()));
+ // several parameters are left as is
+ MessageContentsList several = new MessageContentsList("123", "Mary");
+ assertSame(several,
binding.populateCxfRsResponseFromExchange(replyExchange(several), new
ExchangeImpl()));
+ // a List the route sets as reply is the entity
+ List<String> list = List.of("Mary");
+ assertSame(list,
binding.populateCxfRsResponseFromExchange(replyExchange(list), new
ExchangeImpl()));
+ }
+
+ @Test
+ public void testSimpleBindingResponseHasNoEntityForMessageContentsList()
throws Exception {
+ SimpleCxfRsBinding binding = new SimpleCxfRsBinding();
+ binding.setHeaderFilterStrategy(new DefaultHeaderFilterStrategy());
+
+ Object answer = binding.populateCxfRsResponseFromExchange(
+ replyExchange(new MessageContentsList("123")), new
ExchangeImpl());
+ Response response = assertInstanceOf(Response.class, answer);
+ assertFalse(response.hasEntity());
+ }
+
+ @Test
+ public void testRequestBodyOfList() throws Exception {
+ DefaultCxfRsBinding binding = new DefaultCxfRsBinding();
+ Exchange exchange = new DefaultExchange(context);
+
+ // all the elements of a List are sent
+ List<String> list = List.of("a", "b", "c");
+ exchange.getIn().setBody(list);
+ assertSame(list,
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+ List<String> single = List.of("a");
+ exchange.getIn().setBody(single);
+ assertSame(single,
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+ // the entity of a MessageContentsList is its first element
+ exchange.getIn().setBody(new MessageContentsList("a", "b"));
+ assertEquals("a",
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+ exchange.getIn().setBody(new MessageContentsList());
+ assertNull(binding.bindCamelMessageBodyToRequestBody(exchange.getIn(),
exchange));
+ }
+
+ @Test
+ public void testRequestBodyOfArray() throws Exception {
+ DefaultCxfRsBinding binding = new DefaultCxfRsBinding();
+ Exchange exchange = new DefaultExchange(context);
+
+ // a primitive array is the entity as a whole
+ int[] ints = { 1, 2, 3 };
+ exchange.getIn().setBody(ints);
+ assertSame(ints,
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+ long[] longs = { 1L, 2L };
+ exchange.getIn().setBody(longs);
+ assertSame(longs,
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+ byte[] bytes = { 1, 2 };
+ exchange.getIn().setBody(bytes);
+ assertSame(bytes,
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+ // the entity of an Object[] is its first element, as of a
MessageContentsList
+ exchange.getIn().setBody(new Object[] { "a", "b" });
+ assertEquals("a",
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+ exchange.getIn().setBody(new String[] { "a", "b" });
+ assertEquals("a",
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+ }
+
+ private Exchange replyExchange(Object body) {
+ Exchange exchange = new DefaultExchange(context,
ExchangePattern.InOut);
+ exchange.getMessage().setBody(body);
+ return exchange;
+ }
+
}
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 31e006dc2fbf..e173917f8172 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
@@ -1938,6 +1938,21 @@ It is now written with all its decimals, so a record
that is unmarshalled and ma
`Double` that is the result of a calculation is written with all its digits
too (`0.1 + 0.2` as `0.30000000000000004`,
before `0.3`). Set `precision` to get a fixed number of decimals, as before.
+=== camel-cxfrs - an unchanged request body as reply, and a List body of the
HTTP API producer
+
+With the `Default` binding style, a route that does not set a reply body
replied with the request
+`MessageContentsList`, which failed with HTTP 500 "No message body writer has
been found for class
+MessageContentsList" (or was written as a one element array by a JSON provider
that writes any object). Now a
+`MessageContentsList` reply of one element is the response entity, and an
empty one replies with no content (204).
+When the resource method returns a collection, the `MessageContentsList` is
still written as that collection. A
+`MessageContentsList` of several parameters (a resource method with more than
one parameter) still fails with HTTP 500:
+set the reply body in the route.
+
+The producer with the HTTP centric client API (`httpClientAPI=true`) sent only
the first element of a `List` body. Now
+a `List` body is sent as a whole as the request entity, also a `List` of one
element. Of a `MessageContentsList` (such
+as the body from a `cxfrs` consumer with the `Default` binding style) the
first element is still sent. To send one
+element as before, set that element as the body.
+
=== camel-stax - the text mode of xtokenize gives the text as an XML parser
reads it
With mode `t`, `xtokenize` returned the characters between the tags of the
element as they are written: entity and