This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26143-cxf-pojo-bodies in repository https://gitbox.apache.org/repos/asf/camel.git
commit f6dc4ec9598e22889dcba1190d63e79d1cb187d0 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 2 17:33:05 2026 +0100 CAMEL-26143: camel-cxf - fix byte[] reply, POJO parameter splitting and bodies sent as empty messages A byte[] reply in POJO and CXF_MESSAGE mode was cast to Object[] and gave a ClassCastException SOAP fault; only an Object[] is now taken as the list of values. The POJO producer split a body that is not a List or an array like the Split EIP (a String with a comma became several parameters, a byte[] one per byte); now only a Collection or an Object[] holds the parameters, any other body is the one parameter. In CXF_MESSAGE mode a body that is not a SOAPMessage or a Source was sent as an empty envelope; it is now converted to a DOMSource, and fails when it cannot be converted. PAYLOAD and RAW consumer replies that do not convert are now a SOAP fault instead of an empty response. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/components/cxf.json | 4 +- .../apache/camel/catalog/docs/cxf-component.adoc | 7 + .../org/apache/camel/component/cxf/jaxws/cxf.json | 4 +- .../src/main/docs/cxf-component.adoc | 7 + .../camel/component/cxf/jaxws/CxfEndpoint.java | 37 +++-- .../camel/component/cxf/jaxws/CxfProducer.java | 25 +-- .../component/cxf/jaxws/DefaultCxfBinding.java | 43 ++++- .../camel/component/cxf/jaxws/BodyTypeService.java | 25 +++ .../component/cxf/jaxws/CxfBodyTypesTest.java | 174 +++++++++++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 17 ++ 10 files changed, 300 insertions(+), 43 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxf.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxf.json index d757fadf9c2a..ebf7b925a243 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxf.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxf.json @@ -43,8 +43,8 @@ }, "bodyTypes": { "consumerOut": { "carriers": [ "list", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo" ], "javaType": "org.apache.camel.component.cxf.common.CxfPayload" }, { "option": "dataFormat", "value": "RAW", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ], "javaType": "jakarta.xml.soap.SOAPMessage" } ], "description": "POJO: a [...] - "consumerReply": { "carriers": [ "text", "value", "pojo", "list", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "null" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ] } ], "de [...] - "producerIn": { "carriers": [ "list", "text", "value", "pojo", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ] } ], "description": "POJO: [...] + "consumerReply": { "carriers": [ "text", "value", "pojo", "list", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "null" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo", "text", " [...] + "producerIn": { "carriers": [ "list", "text", "value", "pojo", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo", "text", "bytes", "stream" ] [...] "producerOut": { "carriers": [ "list", "null", "unchanged" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo" ], "javaType": "org.apache.camel.component.cxf.common.CxfPayload" }, { "option": "dataFormat", "value": "RAW", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ], "javaType": "jakarta.xml.soap.SOAPMessage" } ], "descripti [...] }, "properties": { diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxf-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxf-component.adoc index 747329da3c22..b918eaed603f 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxf-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxf-component.adoc @@ -134,6 +134,10 @@ If you require just the SOAP XML as a String, you can set the message body with `message.getSOAPPart()`, and the Camel converter can do the rest of the work for you. +The message body that a producer sends, and the reply of a consumer, is a `SOAPMessage` or a `javax.xml.transform.Source` +of the whole SOAP envelope. Any other body, such as the envelope as a `String`, `byte[]`, `InputStream` or DOM +`Document`, is converted to a `DOMSource`; a body that cannot be converted fails the exchange (a SOAP fault for a consumer). + === Streaming Support in PAYLOAD mode The Camel CXF component now supports streaming of incoming @@ -650,6 +654,9 @@ header, then add the method parameters to a list, and initialize the message with this parameter list. The response message's body is a messageContentsList, you can get the result from that list. +A `List` (or another `Collection`) or an `Object[]` body holds the parameters. Any other body, such as a `String` +or a `byte[]`, is the one parameter of the operation: a `String` with a comma is not split into several parameters. + If you don't specify the operation name in the message header, `CxfProducer` will try to use the `defaultOperationName` from `CxfEndpoint`, if there is no `defaultOperationName` set on diff --git a/components/camel-cxf/camel-cxf-soap/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxws/cxf.json b/components/camel-cxf/camel-cxf-soap/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxws/cxf.json index d757fadf9c2a..ebf7b925a243 100644 --- a/components/camel-cxf/camel-cxf-soap/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxws/cxf.json +++ b/components/camel-cxf/camel-cxf-soap/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxws/cxf.json @@ -43,8 +43,8 @@ }, "bodyTypes": { "consumerOut": { "carriers": [ "list", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo" ], "javaType": "org.apache.camel.component.cxf.common.CxfPayload" }, { "option": "dataFormat", "value": "RAW", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ], "javaType": "jakarta.xml.soap.SOAPMessage" } ], "description": "POJO: a [...] - "consumerReply": { "carriers": [ "text", "value", "pojo", "list", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "null" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ] } ], "de [...] - "producerIn": { "carriers": [ "list", "text", "value", "pojo", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ] } ], "description": "POJO: [...] + "consumerReply": { "carriers": [ "text", "value", "pojo", "list", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "null" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file", "null" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo", "text", " [...] + "producerIn": { "carriers": [ "list", "text", "value", "pojo", "null" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo", "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "RAW", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "text", "bytes", "stream", "file" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo", "text", "bytes", "stream" ] [...] "producerOut": { "carriers": [ "list", "null", "unchanged" ], "byOption": [ { "option": "dataFormat", "value": "PAYLOAD", "carriers": [ "pojo" ], "javaType": "org.apache.camel.component.cxf.common.CxfPayload" }, { "option": "dataFormat", "value": "RAW", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "MESSAGE", "carriers": [ "stream" ] }, { "option": "dataFormat", "value": "CXF_MESSAGE", "carriers": [ "pojo" ], "javaType": "jakarta.xml.soap.SOAPMessage" } ], "descripti [...] }, "properties": { diff --git a/components/camel-cxf/camel-cxf-soap/src/main/docs/cxf-component.adoc b/components/camel-cxf/camel-cxf-soap/src/main/docs/cxf-component.adoc index 747329da3c22..b918eaed603f 100644 --- a/components/camel-cxf/camel-cxf-soap/src/main/docs/cxf-component.adoc +++ b/components/camel-cxf/camel-cxf-soap/src/main/docs/cxf-component.adoc @@ -134,6 +134,10 @@ If you require just the SOAP XML as a String, you can set the message body with `message.getSOAPPart()`, and the Camel converter can do the rest of the work for you. +The message body that a producer sends, and the reply of a consumer, is a `SOAPMessage` or a `javax.xml.transform.Source` +of the whole SOAP envelope. Any other body, such as the envelope as a `String`, `byte[]`, `InputStream` or DOM +`Document`, is converted to a `DOMSource`; a body that cannot be converted fails the exchange (a SOAP fault for a consumer). + === Streaming Support in PAYLOAD mode The Camel CXF component now supports streaming of incoming @@ -650,6 +654,9 @@ header, then add the method parameters to a list, and initialize the message with this parameter list. The response message's body is a messageContentsList, you can get the result from that list. +A `List` (or another `Collection`) or an `Object[]` body holds the parameters. Any other body, such as a `String` +or a `byte[]`, is the one parameter of the operation: a `String` with a comma is not split into several parameters. + If you don't specify the operation name in the message header, `CxfProducer` will try to use the `defaultOperationName` from `CxfEndpoint`, if there is no `defaultOperationName` set on diff --git a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfEndpoint.java b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfEndpoint.java index 184931119705..518b0093f2e8 100644 --- a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfEndpoint.java +++ b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfEndpoint.java @@ -164,15 +164,18 @@ import static org.apache.camel.component.cxf.common.message.CxfConstants.SCHEME_ Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE, Carrier.NULL }), @BodyType.When(option = "dataFormat", value = "CXF_MESSAGE", - carriers = Carrier.POJO) }, + carriers = { + Carrier.POJO, Carrier.TEXT, Carrier.BYTES, + Carrier.STREAM }) }, description = "POJO: the return value of the operation, of the type the service class" - + " declares, or a List or array of it and the out and holder parameters;" - + " another type is a SOAP fault, so the request MessageContentsList only" - + " fits an echo. PAYLOAD: a CxfPayload, or the XML of the soap:Body" - + " contents as text, bytes, a stream or a DOM; a body that does not convert" - + " sends an empty soap:Body. RAW: the whole SOAP envelope as text, bytes, a" - + " stream or a file. CXF_MESSAGE: a SOAPMessage or a javax.xml.transform" - + " Source; text or a stream sends an empty envelope"), + + " declares (a byte array is one value), or a List or an Object array of it" + + " and the out and holder parameters; another type is a SOAP fault, so the" + + " request MessageContentsList only fits an echo. PAYLOAD: a CxfPayload, or" + + " the XML of the soap:Body contents as text, bytes, a stream or a DOM. RAW:" + + " the whole SOAP envelope as text, bytes, a stream or a file. CXF_MESSAGE: a" + + " SOAPMessage, a javax.xml.transform Source, or the whole SOAP envelope as" + + " text, bytes, a stream or a DOM. A body that does not convert is a SOAP" + + " fault"), producerIn = @BodyType(value = { Carrier.LIST, Carrier.TEXT, Carrier.VALUE, Carrier.POJO, Carrier.NULL }, byOption = { @BodyType.When(option = "dataFormat", value = "PAYLOAD", @@ -188,15 +191,17 @@ import static org.apache.camel.component.cxf.common.message.CxfConstants.SCHEME_ Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE }), @BodyType.When(option = "dataFormat", value = "CXF_MESSAGE", - carriers = Carrier.POJO) }, + carriers = { + Carrier.POJO, Carrier.TEXT, Carrier.BYTES, + Carrier.STREAM }) }, description = "POJO: the parameters of the operation (the CamelCxfOperationName header or" - + " defaultOperationName) as a List or an array, or the one parameter as" - + " the body; each of the type the service class declares. A body that is" - + " not a List is split like the split EIP does, so text with a comma or a" - + " byte array is several parameters: wrap it in a List. PAYLOAD: a" - + " CxfPayload, or the XML of the soap:Body contents as text, bytes, a stream," - + " a file or a DOM. RAW: the whole SOAP envelope. CXF_MESSAGE: a SOAPMessage" - + " or a javax.xml.transform Source"), + + " defaultOperationName) as a List (or another Collection) or an Object array," + + " or the one parameter as the body; each of the type the service class" + + " declares. Any other body, such as text or a byte array, is the one" + + " parameter. PAYLOAD: a CxfPayload, or the XML of the soap:Body contents as" + + " text, bytes, a stream, a file or a DOM. RAW: the whole SOAP envelope." + + " CXF_MESSAGE: a SOAPMessage, a javax.xml.transform Source, or the whole SOAP" + + " envelope as text, bytes, a stream or a DOM; another body fails"), producerOut = @BodyType(value = { Carrier.LIST, Carrier.NULL, Carrier.UNCHANGED }, byOption = { @BodyType.When(option = "dataFormat", value = "PAYLOAD", carriers = Carrier.POJO, diff --git a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfProducer.java b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfProducer.java index ed086aa97fee..2d8277bc568e 100644 --- a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfProducer.java +++ b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/CxfProducer.java @@ -358,24 +358,13 @@ public class CxfProducer extends DefaultAsyncProducer { } if (body instanceof Object[] objectArray) { params = objectArray; - } else if (body instanceof List) { - // Now we just check if the request is List - params = ((List<?>) body).toArray(); + } else if (body instanceof Collection<?> collection) { + // a List (or another collection) holds the parameters + params = collection.toArray(); } else { - // maybe we can iterate the body and that way create a list for the parameters - // then end users do not need to trouble with List - Iterator<?> it = exchange.getIn().getBody(Iterator.class); - if (it != null && it.hasNext()) { - List<?> list = exchange.getContext().getTypeConverter().convertTo(List.class, it); - if (list != null) { - params = list.toArray(); - } - } - if (params == null) { - // now we just use the body as single parameter - params = new Object[1]; - params[0] = exchange.getIn().getBody(); - } + // any other body (such as a String, a byte[] or a POJO) is the one parameter, + // and is not split into several parameters + params = new Object[] { body }; } // make sure we have the right number of parameters checkParameterSize(endpoint, exchange, params); @@ -388,7 +377,7 @@ public class CxfProducer extends DefaultAsyncProducer { params[0] = exchange.getIn().getMandatoryBody(InputStream.class); } else if (endpoint.getDataFormat().dealias() == DataFormat.CXF_MESSAGE) { params = new Object[1]; - params[0] = exchange.getIn().getBody(); + params[0] = DefaultCxfBinding.toCxfMessageBody(exchange.getIn()); } if (LOG.isTraceEnabled()) { diff --git a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/DefaultCxfBinding.java b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/DefaultCxfBinding.java index ee13313ca47f..b125512857a7 100644 --- a/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/DefaultCxfBinding.java +++ b/components/camel-cxf/camel-cxf-soap/src/main/java/org/apache/camel/component/cxf/jaxws/DefaultCxfBinding.java @@ -37,6 +37,7 @@ import java.util.StringJoiner; import java.util.TreeMap; import jakarta.activation.DataHandler; +import jakarta.xml.soap.SOAPMessage; import jakarta.xml.ws.Holder; import javax.security.auth.Subject; @@ -53,6 +54,7 @@ import org.w3c.dom.Node; import org.apache.camel.Exchange; import org.apache.camel.ExchangePattern; import org.apache.camel.ExchangePropertyKey; +import org.apache.camel.InvalidPayloadRuntimeException; import org.apache.camel.attachment.AttachmentMessage; import org.apache.camel.attachment.DefaultAttachment; import org.apache.camel.component.cxf.common.CxfBinding; @@ -596,8 +598,9 @@ public class DefaultCxfBinding implements CxfBinding, HeaderFilterStrategyAware // Create a new MessageContentsList to avoid OOM from the HolderOutInterceptor if (outBody instanceof List) { resList = new MessageContentsList((List<?>) outBody); - } else if (outBody.getClass().isArray()) { - resList = new MessageContentsList((Object[]) outBody); + } else if (outBody instanceof Object[] array) { + // a primitive array such as byte[] is one value, and is wrapped below + resList = new MessageContentsList(array); } else { resList = new MessageContentsList(outBody); } @@ -1204,11 +1207,41 @@ public class DefaultCxfBinding implements CxfBinding, HeaderFilterStrategyAware if (dataFormat == DataFormat.POJO) { answer = out.getBody(); } else if (dataFormat == DataFormat.PAYLOAD) { - answer = out.getBody(CxfPayload.class); + answer = getMandatoryBodyIfPresent(out, CxfPayload.class); } else if (dataFormat.dealias() == DataFormat.RAW) { - answer = out.getBody(InputStream.class); + answer = getMandatoryBodyIfPresent(out, InputStream.class); } else if (dataFormat.dealias() == DataFormat.CXF_MESSAGE) { - answer = out.getBody(); + Object body = out.getBody(); + // a List or an array holds the SOAP message as its first element + answer = body instanceof List || body instanceof Object[] ? body : toCxfMessageBody(out); + } + return answer; + } + + /** + * The body as the SOAP envelope for the CXF_MESSAGE data format: a {@link SOAPMessage} or a {@link Source} as it + * is, and any other body (such as text, bytes, a stream or a DOM document of the envelope) converted to a + * {@link DOMSource}, failing when it cannot be converted. + */ + static Object toCxfMessageBody(org.apache.camel.Message message) { + Object body = message.getBody(); + if (body == null || body instanceof SOAPMessage || body instanceof Source) { + return body; + } + return getMandatoryBodyIfPresent(message, DOMSource.class); + } + + /** + * The body converted to the given type, null for a null body, and failing when a body cannot be converted, so it is + * not sent as an empty message. + */ + private static <T> T getMandatoryBodyIfPresent(org.apache.camel.Message message, Class<T> type) { + if (message.getBody() == null) { + return null; + } + T answer = message.getBody(type); + if (answer == null) { + throw new InvalidPayloadRuntimeException(message.getExchange(), type, message); } return answer; } diff --git a/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/BodyTypeService.java b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/BodyTypeService.java new file mode 100644 index 000000000000..91ebd1e48f3c --- /dev/null +++ b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/BodyTypeService.java @@ -0,0 +1,25 @@ +/* + * 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.jaxws; + +public interface BodyTypeService { + + String echoText(String text); + + byte[] echoBytes(byte[] data); + +} diff --git a/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfBodyTypesTest.java b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfBodyTypesTest.java new file mode 100644 index 000000000000..684cea061f92 --- /dev/null +++ b/components/camel-cxf/camel-cxf-soap/src/test/java/org/apache/camel/component/cxf/jaxws/CxfBodyTypesTest.java @@ -0,0 +1,174 @@ +/* + * 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.jaxws; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.apache.camel.Exchange; +import org.apache.camel.InvalidPayloadRuntimeException; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.cxf.common.message.CxfConstants; +import org.apache.camel.test.AvailablePortFinder; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.cxf.BusFactory; +import org.apache.cxf.frontend.ClientProxyFactoryBean; +import org.apache.cxf.message.MessageContentsList; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The message bodies the cxf producer sends and the cxf consumer replies in each data format. + */ +public class CxfBodyTypesTest extends CamelTestSupport { + + @RegisterExtension + static final AvailablePortFinder.Port PORT = AvailablePortFinder.find(); + + private static final String NS = "http://jaxws.cxf.component.camel.apache.org/"; + + private static final String ECHO_TEXT_REQUEST = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" + + "<soap:Body><ns1:echoText xmlns:ns1=\"" + NS + "\">" + + "<arg0 xmlns=\"" + NS + "\">Hello World</arg0>" + + "</ns1:echoText></soap:Body></soap:Envelope>"; + + private static final String ECHO_TEXT_RESPONSE = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" + + "<soap:Body><ns1:echoTextResponse xmlns:ns1=\"" + NS + "\">" + + "<return xmlns=\"" + NS + "\">echo from envelope</return>" + + "</ns1:echoTextResponse></soap:Body></soap:Envelope>"; + + private static String address(String path) { + return "http://localhost:" + PORT.getPort() + "/CxfBodyTypesTest/" + path; + } + + private static String uri(String path) { + return "cxf://" + address(path) + "?serviceClass=" + BodyTypeService.class.getName(); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from(uri("pojo")).process(CxfBodyTypesTest::echo); + + from(uri("payload") + "&dataFormat=PAYLOAD").setBody(constant(Map.of("foo", "bar"))); + + from(uri("raw") + "&dataFormat=RAW").setBody(constant(Map.of("foo", "bar"))); + + from(uri("message") + "&dataFormat=CXF_MESSAGE") + .process(e -> e.getMessage().setBody( + new ByteArrayInputStream(ECHO_TEXT_RESPONSE.getBytes(StandardCharsets.UTF_8)))); + + from("direct:pojo").to(uri("pojo") + "&defaultOperationName=echoText"); + + from("direct:message").to(uri("pojo") + "&dataFormat=CXF_MESSAGE&defaultOperationName=echoText"); + } + }; + } + + private static void echo(Exchange exchange) { + String operation = exchange.getIn().getHeader(CxfConstants.OPERATION_NAME, String.class); + Object arg = exchange.getIn().getBody(MessageContentsList.class).get(0); + if ("echoBytes".equals(operation)) { + exchange.getMessage().setBody(("echo " + new String((byte[]) arg, StandardCharsets.UTF_8)) + .getBytes(StandardCharsets.UTF_8)); + } else { + exchange.getMessage().setBody("echo " + arg); + } + } + + private static BodyTypeService client(String path) { + ClientProxyFactoryBean proxyFactory = new ClientProxyFactoryBean(); + proxyFactory.getClientFactoryBean().setAddress(address(path)); + proxyFactory.getClientFactoryBean().setServiceClass(BodyTypeService.class); + proxyFactory.getClientFactoryBean().setBus(BusFactory.getDefaultBus()); + return (BodyTypeService) proxyFactory.create(); + } + + @Test + public void testPojoConsumerRepliesByteArray() { + byte[] reply = client("pojo").echoBytes("Hello".getBytes(StandardCharsets.UTF_8)); + + assertEquals("echo Hello", new String(reply, StandardCharsets.UTF_8)); + } + + @Test + public void testPojoProducerSendsTextWithCommaAsOneParameter() { + Object reply = template.requestBody("direct:pojo", "Hello,World"); + + assertEquals("echo Hello,World", ((List<?>) reply).get(0)); + } + + @Test + public void testPojoProducerSendsByteArrayAsOneParameter() { + Object reply = template.requestBodyAndHeader("direct:pojo", "Hello".getBytes(StandardCharsets.UTF_8), + CxfConstants.OPERATION_NAME, "echoBytes"); + + assertArrayEquals("echo Hello".getBytes(StandardCharsets.UTF_8), (byte[]) ((List<?>) reply).get(0)); + } + + @Test + public void testPojoProducerSendsListAsParameters() { + Object reply = template.requestBody("direct:pojo", List.of("Hello,World")); + + assertEquals("echo Hello,World", ((List<?>) reply).get(0)); + } + + @Test + public void testCxfMessageProducerSendsTextEnvelope() { + String reply = template.requestBody("direct:message", ECHO_TEXT_REQUEST, String.class); + + assertTrue(reply.contains("echo Hello World"), "Wrong reply: " + reply); + } + + @Test + public void testCxfMessageProducerFailsForBodyThatIsNotAnEnvelope() { + Exception e = assertThrows(Exception.class, + () -> template.requestBody("direct:message", Map.of("foo", "bar"))); + + assertInstanceOf(InvalidPayloadRuntimeException.class, e.getCause()); + assertTrue(e.getCause().getMessage().contains("DOMSource"), "Wrong exception: " + e.getCause().getMessage()); + } + + @Test + public void testCxfMessageConsumerRepliesStreamEnvelope() { + assertEquals("echo from envelope", client("message").echoText("Hello")); + } + + @Test + public void testPayloadConsumerFailsForReplyThatDoesNotConvert() { + Exception e = assertThrows(Exception.class, () -> client("payload").echoText("Hello")); + + assertTrue(e.getMessage().contains("CxfPayload"), "Wrong fault: " + e.getMessage()); + } + + @Test + public void testRawConsumerFailsForReplyThatDoesNotConvert() { + Exception e = assertThrows(Exception.class, () -> client("raw").echoText("Hello")); + + assertTrue(e.getMessage().contains("InputStream"), "Wrong fault: " + e.getMessage()); + } + +} 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 1bc188419d61..cd783d179fb7 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 @@ -1917,6 +1917,23 @@ the previous output, convert the rows to `List` before marshalling, or set a cus now has all the columns seen so far in the body, so rows that each have keys of their own (sparse or pivoted data) give about N²/2 fields for N rows, where the output was linear in the input before. +=== camel-cxf - message bodies of the POJO producer and of CXF_MESSAGE, and replies that do not convert + +The `cxf` producer in `POJO` mode split a body that is not a `List` or an `Object[]` into several parameters, the way +the Split EIP does: a `String` with a comma (`"Hello,World"`) became two parameters, a `byte[]` one parameter per +byte. Now a `List` (or another `Collection`) or an `Object[]` holds the parameters, and any other body, such as a +`String`, a `byte[]` or a `Map`, is the one parameter. A route that relied on a comma-separated `String` (or another +iterable value) to give several parameters must now set a `List` of the parameters as the body. + +In `CXF_MESSAGE` mode, the producer and the consumer reply sent a body that is not a `SOAPMessage` or a +`javax.xml.transform.Source` as an empty SOAP envelope. Such a body (for example the envelope as a `String`, `byte[]`, +`InputStream` or DOM `Document`) is now converted to a `DOMSource` and sent; a body that cannot be converted fails the +exchange. + +A consumer reply in `PAYLOAD` mode that does not convert to a `CxfPayload`, or in `RAW` mode that does not convert to +an `InputStream`, was sent as an empty `soap:Body` or an empty response. It is now a SOAP fault. A `null` reply is +unchanged. + === camel-github2 - the commit consumer routes the commits beyond the newest 100 The `commit` consumer read only the 100 newest commits of the branch at each poll, so when more than 100 commits were
