This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26567 in repository https://gitbox.apache.org/repos/asf/camel.git
commit de3d04715d6c932d80f94dff1c318dfff3f69842 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Nov 4 09:41:40 2026 +0100 CAMEL-26567: camel-jms / camel-sjms - text that is not a String is sent as a TextMessage, and the ObjectMessage error names the class of the body In the local-model benchmark (connect-service-jms-request-reply, step 3) a Groovy script set the reply of a JMS request-reply route to a GString ("not enough ${sku}"), which is text but not a String. JmsBinding chose the JMS message type with body instanceof String, so the GString, which is Serializable, was sent as an ObjectMessage. ObjectMessage is disabled by default since 4.14, and the error only said to set objectMessageEnabled=true. The two runs that did so failed again: the deserialization filter blocks org.codehaus.groovy.runtime.GStringImpl. A StringBuilder body has the same problem. So does a GString that a Groovy bean method returns, or that a script sets as the body itself, which the camel-groovy fix for the result of a script (CAMEL-26566) does not cover. JmsBinding in camel-jms and camel-sjms now sends any CharSequence as a TextMessage, and the consumer gets a String. The jmsMessageType option (and in camel-jms the CamelJmsMessageType header) still forces a type, so jmsMessageType=Object sends such a body as an ObjectMessage as before. When a body would be sent as an ObjectMessage and objectMessageEnabled is false, the error now names the class of the body. It says to convert the body to a String (convertBodyTo with type String) or to marshal it to send it as text, and still names objectMessageEnabled=true for a body that should be a serialized Java object. camel-amqp, camel-activemq, camel-activemq6 and camel-sjms2 inherit the change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_016Y4dDi5iQNgtHrDdz2NpwF --- .../apache/camel/catalog/docs/jms-component.adoc | 6 +- .../apache/camel/catalog/docs/sjms-component.adoc | 2 +- .../apache/camel/catalog/docs/sjms2-component.adoc | 2 +- .../camel-jms/src/main/docs/jms-component.adoc | 6 +- .../org/apache/camel/component/jms/JmsBinding.java | 22 ++++- .../java/com/example/external/ExternalText.java | 53 ++++++++++ .../apache/camel/component/jms/JmsBindingTest.java | 16 +++ .../component/jms/JmsCharSequenceBodyTest.java | 108 +++++++++++++++++++++ .../component/jms/JmsObjectMessageEnabledTest.java | 13 +++ .../camel-sjms/src/main/docs/sjms-component.adoc | 2 +- .../camel/component/sjms/jms/JmsBinding.java | 22 ++++- .../java/com/example/external/ExternalText.java | 53 ++++++++++ .../sjms/producer/CharSequenceBodyTest.java | 88 +++++++++++++++++ .../producer/SjmsObjectMessageEnabledTest.java | 13 +++ .../camel-sjms2/src/main/docs/sjms2-component.adoc | 2 +- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 13 +++ 16 files changed, 411 insertions(+), 10 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jms-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jms-component.adoc index 1e8d3052db61..3575cfc16a11 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jms-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jms-component.adoc @@ -615,12 +615,16 @@ following JMS message types: |Body Type |JMS Message |Comment |`String` |`jakarta.jms.TextMessage` | +|`CharSequence` |`jakarta.jms.TextMessage` |Other text, such as a `StringBuilder` or a Groovy GString, is converted +to `String`. + |`org.w3c.dom.Node` |`jakarta.jms.TextMessage` |The DOM will be converted to `String`. |`Map` |`jakarta.jms.MapMessage` | -|`java.io.Serializable` |`jakarta.jms.ObjectMessage` | +|`java.io.Serializable` |`jakarta.jms.ObjectMessage` |Only with `objectMessageEnabled=true`. Otherwise sending fails, +and the error names the class of the body. |`byte[]` |`jakarta.jms.BytesMessage` | diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms-component.adoc index a55591e95bac..d12792d5ff25 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms-component.adoc @@ -299,7 +299,7 @@ The body is mapped to and from a JMS message as follows (unless the option `jmsM [width="100%",cols="1,1,2",options="header"] |=== | Camel body | JMS message | Body when consumed -| `String` or XML `Node` | `TextMessage` | `String` +| `String`, other text (a `CharSequence` such as a `StringBuilder`) or XML `Node` | `TextMessage` | `String` | `byte[]`, `InputStream`, `Reader`, `ByteBuffer`, `File`, `Path` or `StreamCache` | `BytesMessage` | `byte[]` | `Map` (of simple values) | `MapMessage` | `Map` | `null` | `Message` | `null` diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms2-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms2-component.adoc index 68638507fce9..9fa42cf85235 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms2-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sjms2-component.adoc @@ -303,7 +303,7 @@ The body is mapped to and from a JMS message as follows (unless the option `jmsM [width="100%",cols="1,1,2",options="header"] |=== | Camel body | JMS message | Body when consumed -| `String` or XML `Node` | `TextMessage` | `String` +| `String`, other text (a `CharSequence` such as a `StringBuilder`) or XML `Node` | `TextMessage` | `String` | `byte[]`, `InputStream`, `Reader`, `ByteBuffer`, `File`, `Path` or `StreamCache` | `BytesMessage` | `byte[]` | `Map` (of simple values) | `MapMessage` | `Map` | `null` | `Message` | `null` diff --git a/components/camel-jms/src/main/docs/jms-component.adoc b/components/camel-jms/src/main/docs/jms-component.adoc index 1e8d3052db61..3575cfc16a11 100644 --- a/components/camel-jms/src/main/docs/jms-component.adoc +++ b/components/camel-jms/src/main/docs/jms-component.adoc @@ -615,12 +615,16 @@ following JMS message types: |Body Type |JMS Message |Comment |`String` |`jakarta.jms.TextMessage` | +|`CharSequence` |`jakarta.jms.TextMessage` |Other text, such as a `StringBuilder` or a Groovy GString, is converted +to `String`. + |`org.w3c.dom.Node` |`jakarta.jms.TextMessage` |The DOM will be converted to `String`. |`Map` |`jakarta.jms.MapMessage` | -|`java.io.Serializable` |`jakarta.jms.ObjectMessage` | +|`java.io.Serializable` |`jakarta.jms.ObjectMessage` |Only with `objectMessageEnabled=true`. Otherwise sending fails, +and the error names the class of the body. |`byte[]` |`jakarta.jms.BytesMessage` | diff --git a/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsBinding.java b/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsBinding.java index 063764cb9c48..f4ebe88b999f 100644 --- a/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsBinding.java +++ b/components/camel-jms/src/main/java/org/apache/camel/component/jms/JmsBinding.java @@ -149,6 +149,23 @@ public class JmsBinding { + " Set objectMessageEnabled=true on the JMS endpoint or component to enable it."); } + /** + * The error for a body that would be sent as an ObjectMessage. It names the class of the body, and says how to send + * the body as text or JSON, so a body that is meant as text is not sent as a serialized Java object. + */ + private static IllegalStateException objectMessageDisabledForBody(Object body) { + if (body == null) { + return objectMessageDisabled("creating ObjectMessage"); + } + return new IllegalStateException( + "JMS ObjectMessage is disabled by default for security reasons (creating ObjectMessage for the message body" + + " of class: " + body.getClass().getName() + ")." + + " To send the body as text, convert it to a String before sending (convertBodyTo" + + " with type String), or marshal it (for example to JSON)." + + " To send it as a serialized Java object, set objectMessageEnabled=true on the JMS" + + " endpoint or component."); + } + /** * Applies the configured (or default) deserialization filter to the class of an object returned by * {@link jakarta.jms.ObjectMessage#getObject()}. Throws {@link SecurityException} if the class is rejected. @@ -691,7 +708,8 @@ public class JmsBinding { JmsMessageType type = null; // let body determine the type - if (body instanceof Node || body instanceof String) { + if (body instanceof Node || body instanceof CharSequence) { + // any text (such as a StringBuilder or a Groovy GString) is sent as text, not as a serialized Java object type = Text; } else if (body instanceof byte[] || body instanceof ByteBuffer) { type = Bytes; @@ -770,7 +788,7 @@ public class JmsBinding { } case Object: { if (!isObjectMessageEnabled()) { - throw objectMessageDisabled("creating ObjectMessage"); + throw objectMessageDisabledForBody(body); } ObjectMessage message = session.createObjectMessage(); if (body != null) { diff --git a/components/camel-jms/src/test/java/com/example/external/ExternalText.java b/components/camel-jms/src/test/java/com/example/external/ExternalText.java new file mode 100644 index 000000000000..d227e8dcf74f --- /dev/null +++ b/components/camel-jms/src/test/java/com/example/external/ExternalText.java @@ -0,0 +1,53 @@ +/* + * 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 com.example.external; + +import java.io.Serializable; + +/** + * Text that is not a String, as a Groovy GString: a Serializable CharSequence in a package outside the default + * JmsBinding deserialization allow list ({@code java.**}, {@code javax.**}, {@code org.apache.camel.**}). + */ +public class ExternalText implements CharSequence, Serializable { + private static final long serialVersionUID = 1L; + + private final String text; + + public ExternalText(String text) { + this.text = text; + } + + @Override + public int length() { + return text.length(); + } + + @Override + public char charAt(int index) { + return text.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return text.subSequence(start, end); + } + + @Override + public String toString() { + return text; + } +} diff --git a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsBindingTest.java b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsBindingTest.java index b8aadf5e637c..5f205a10c92c 100644 --- a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsBindingTest.java +++ b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsBindingTest.java @@ -28,6 +28,7 @@ import jakarta.jms.JMSException; import jakarta.jms.Message; import jakarta.jms.ObjectMessage; +import com.example.external.ExternalText; import com.example.external.NotAllowedPayload; import org.apache.activemq.artemis.jms.client.ActiveMQTextMessage; import org.apache.camel.Exchange; @@ -187,6 +188,21 @@ public class JmsBindingTest { assertDoesNotThrow(() -> jmsBindingUnderTest.extractBodyFromJms(null, message)); } + @Test + public void testCharSequenceBodyIsText() throws Exception { + try (DefaultCamelContext camelContext = new DefaultCamelContext()) { + Exchange exchange = new DefaultExchange(camelContext); + assertEquals(JmsMessageType.Text, jmsBindingUnderTest.getJMSMessageTypeForBody(exchange, "Hello")); + assertEquals(JmsMessageType.Text, + jmsBindingUnderTest.getJMSMessageTypeForBody(exchange, new StringBuilder("Hello"))); + // text that is not a String, as a Groovy GString + assertEquals(JmsMessageType.Text, + jmsBindingUnderTest.getJMSMessageTypeForBody(exchange, new ExternalText("Hello"))); + // a Serializable body that is not text is still an ObjectMessage + assertEquals(JmsMessageType.Object, jmsBindingUnderTest.getJMSMessageTypeForBody(exchange, 123)); + } + } + @Test public void testStandardJmsHeaderInAnyCase() throws Exception { Message message = mock(Message.class); diff --git a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsCharSequenceBodyTest.java b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsCharSequenceBodyTest.java new file mode 100644 index 000000000000..c2da066fc170 --- /dev/null +++ b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsCharSequenceBodyTest.java @@ -0,0 +1,108 @@ +/* + * 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.jms; + +import jakarta.jms.TextMessage; + +import com.example.external.ExternalText; +import org.apache.camel.CamelContext; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.infra.core.CamelContextExtension; +import org.apache.camel.test.infra.core.DefaultCamelContextExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +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.assertInstanceOf; + +/** + * A body that is text but not a String (a StringBuilder, or a Groovy GString that a script returns) is sent as a + * TextMessage. It was sent as an ObjectMessage, which is disabled by default. + */ +public class JmsCharSequenceBodyTest extends AbstractJMSTest { + + @Order(2) + @RegisterExtension + public static CamelContextExtension camelContextExtension = new DefaultCamelContextExtension(); + + protected CamelContext context; + protected ProducerTemplate template; + + @Override + protected String getComponentName() { + return "jms"; + } + + @Test + public void testStringBuilderIsSentAsText() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedBodiesReceived("Hello World"); + mock.message(0).body().isInstanceOf(String.class); + + template.sendBody("jms:queue:JmsCharSequenceBodyTest", new StringBuilder("Hello World")); + + MockEndpoint.assertIsSatisfied(context); + JmsMessage in = mock.getReceivedExchanges().get(0).getIn(JmsMessage.class); + assertInstanceOf(TextMessage.class, in.getJmsMessage()); + } + + @Test + public void testTextReply() { + Object reply = template.requestBody("jms:queue:JmsCharSequenceBodyTest.reply?requestTimeout=5000", "CAMEL-CAP"); + assertEquals("not enough CAMEL-CAP", reply); + } + + @Test + public void testTextReplyWithObjectMessageEnabled() { + // the reply is text, so it is not a serialized Java object that the deserialization filter blocks + Object reply = template.requestBody( + "jms:queue:JmsCharSequenceBodyTest.enabled?objectMessageEnabled=true&requestTimeout=5000", "CAMEL-CAP"); + assertEquals("not enough CAMEL-CAP", reply); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("jms:queue:JmsCharSequenceBodyTest").to("mock:result"); + + // a reply that is text but not a String, as a route that sets the body with a Groovy GString + from("jms:queue:JmsCharSequenceBodyTest.reply") + .process(e -> e.getMessage().setBody(new ExternalText("not enough " + e.getMessage().getBody()))); + + from("jms:queue:JmsCharSequenceBodyTest.enabled?objectMessageEnabled=true") + .process(e -> e.getMessage().setBody(new ExternalText("not enough " + e.getMessage().getBody()))); + } + }; + } + + @Override + public CamelContextExtension getCamelContextExtension() { + return camelContextExtension; + } + + @BeforeEach + void setUpRequirements() { + context = camelContextExtension.getContext(); + template = camelContextExtension.getProducerTemplate(); + } +} diff --git a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsObjectMessageEnabledTest.java b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsObjectMessageEnabledTest.java index e43d97494000..315d295430f1 100644 --- a/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsObjectMessageEnabledTest.java +++ b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsObjectMessageEnabledTest.java @@ -77,6 +77,19 @@ public class JmsObjectMessageEnabledTest extends AbstractJMSTest { "Expected guidance to enable objectMessageEnabled=true, got: " + cause.getMessage()); } + @Test + public void testProducerErrorNamesTheBodyClass() { + CamelExecutionException ex = assertThrows(CamelExecutionException.class, + () -> template.sendBody("activemq:queue:JmsObjectMessageEnabledTest.body", new MyOrder("beer", 10))); + Throwable cause = rootCause(ex); + assertTrue(cause.getMessage().contains(MyOrder.class.getName()), + "Expected the class of the body, got: " + cause.getMessage()); + assertTrue(cause.getMessage().contains("convertBodyTo"), + "Expected guidance to convert the body to a String, got: " + cause.getMessage()); + assertTrue(cause.getMessage().contains("objectMessageEnabled=true"), + "Expected guidance to enable objectMessageEnabled=true, got: " + cause.getMessage()); + } + @Test public void testConsumerRefusesObjectMessageByDefault() { // Send an ObjectMessage directly to the queue with a JmsTemplate (bypassing Camel) diff --git a/components/camel-sjms/src/main/docs/sjms-component.adoc b/components/camel-sjms/src/main/docs/sjms-component.adoc index a55591e95bac..d12792d5ff25 100644 --- a/components/camel-sjms/src/main/docs/sjms-component.adoc +++ b/components/camel-sjms/src/main/docs/sjms-component.adoc @@ -299,7 +299,7 @@ The body is mapped to and from a JMS message as follows (unless the option `jmsM [width="100%",cols="1,1,2",options="header"] |=== | Camel body | JMS message | Body when consumed -| `String` or XML `Node` | `TextMessage` | `String` +| `String`, other text (a `CharSequence` such as a `StringBuilder`) or XML `Node` | `TextMessage` | `String` | `byte[]`, `InputStream`, `Reader`, `ByteBuffer`, `File`, `Path` or `StreamCache` | `BytesMessage` | `byte[]` | `Map` (of simple values) | `MapMessage` | `Map` | `null` | `Message` | `null` diff --git a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/jms/JmsBinding.java b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/jms/JmsBinding.java index f4b2c2652260..cfc46b6e1036 100644 --- a/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/jms/JmsBinding.java +++ b/components/camel-sjms/src/main/java/org/apache/camel/component/sjms/jms/JmsBinding.java @@ -134,6 +134,23 @@ public class JmsBinding { + " Set objectMessageEnabled=true on the SJMS endpoint or component to enable it."); } + /** + * The error for a body that would be sent as an ObjectMessage. It names the class of the body, and says how to send + * the body as text or JSON, so a body that is meant as text is not sent as a serialized Java object. + */ + private static IllegalStateException objectMessageDisabledForBody(Object body) { + if (body == null) { + return objectMessageDisabled("creating ObjectMessage"); + } + return new IllegalStateException( + "JMS ObjectMessage is disabled by default for security reasons (creating ObjectMessage for the message body" + + " of class: " + body.getClass().getName() + ")." + + " To send the body as text, convert it to a String before sending (convertBodyTo" + + " with type String), or marshal it (for example to JSON)." + + " To send it as a serialized Java object, set objectMessageEnabled=true on the SJMS" + + " endpoint or component."); + } + /** * Applies the configured (or default) deserialization filter to the class of an object returned by * {@link jakarta.jms.ObjectMessage#getObject()}. Throws {@link SecurityException} if the class is rejected. @@ -506,7 +523,8 @@ public class JmsBinding { Exchange exchange, Object body, Map<String, Object> headers, Session session, CamelContext context) { JmsMessageType type = null; // let body determine the type - if (body instanceof Node || body instanceof String) { + if (body instanceof Node || body instanceof CharSequence) { + // any text (such as a StringBuilder or a Groovy GString) is sent as text, not as a serialized Java object type = JmsMessageType.Text; } else if (body instanceof byte[] || body instanceof WrappedFile || body instanceof File || body instanceof Reader || body instanceof InputStream || body instanceof ByteBuffer || body instanceof StreamCache) { @@ -560,7 +578,7 @@ public class JmsBinding { } case Object: { if (!isObjectMessageEnabled()) { - throw objectMessageDisabled("creating ObjectMessage"); + throw objectMessageDisabledForBody(body); } ObjectMessage message = session.createObjectMessage(); if (body != null) { diff --git a/components/camel-sjms/src/test/java/com/example/external/ExternalText.java b/components/camel-sjms/src/test/java/com/example/external/ExternalText.java new file mode 100644 index 000000000000..d227e8dcf74f --- /dev/null +++ b/components/camel-sjms/src/test/java/com/example/external/ExternalText.java @@ -0,0 +1,53 @@ +/* + * 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 com.example.external; + +import java.io.Serializable; + +/** + * Text that is not a String, as a Groovy GString: a Serializable CharSequence in a package outside the default + * JmsBinding deserialization allow list ({@code java.**}, {@code javax.**}, {@code org.apache.camel.**}). + */ +public class ExternalText implements CharSequence, Serializable { + private static final long serialVersionUID = 1L; + + private final String text; + + public ExternalText(String text) { + this.text = text; + } + + @Override + public int length() { + return text.length(); + } + + @Override + public char charAt(int index) { + return text.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return text.subSequence(start, end); + } + + @Override + public String toString() { + return text; + } +} diff --git a/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/CharSequenceBodyTest.java b/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/CharSequenceBodyTest.java new file mode 100644 index 000000000000..f70eb7c7eace --- /dev/null +++ b/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/CharSequenceBodyTest.java @@ -0,0 +1,88 @@ +/* + * 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.sjms.producer; + +import jakarta.jms.Message; +import jakarta.jms.MessageConsumer; +import jakarta.jms.TextMessage; + +import com.example.external.ExternalText; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.sjms.support.JmsTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * A body that is text but not a String (a StringBuilder, or a Groovy GString that a script returns) is sent as a + * TextMessage. It was sent as an ObjectMessage, which is disabled by default. + */ +public class CharSequenceBodyTest extends JmsTestSupport { + + private static final String TEST_DESTINATION_NAME = "test.foo.CharSequenceBodyTest"; + private static final String TEST_OTHER_DESTINATION_NAME = "test.other.CharSequenceBodyTest"; + private static final String TEST_REPLY_DESTINATION_NAME = "test.reply.CharSequenceBodyTest"; + + @Override + protected boolean useJmx() { + return false; + } + + @Test + public void testStringBuilderIsSentAsText() throws Exception { + MessageConsumer mc = createQueueConsumer(TEST_DESTINATION_NAME); + + template.sendBody("sjms:queue:" + TEST_DESTINATION_NAME, new StringBuilder("Hello World")); + + Message message = mc.receive(5000); + TextMessage text = assertInstanceOf(TextMessage.class, message); + assertEquals("Hello World", text.getText()); + mc.close(); + } + + @Test + public void testOtherTextIsSentAsText() throws Exception { + MessageConsumer mc = createQueueConsumer(TEST_OTHER_DESTINATION_NAME); + + // text that is not a String, as a Groovy GString + template.sendBody("sjms:queue:" + TEST_OTHER_DESTINATION_NAME, new ExternalText("Bye World")); + + Message message = mc.receive(5000); + TextMessage text = assertInstanceOf(TextMessage.class, message); + assertEquals("Bye World", text.getText()); + mc.close(); + } + + @Test + public void testTextReply() { + Object reply = template.requestBody("sjms:queue:" + TEST_REPLY_DESTINATION_NAME + "?requestTimeout=5000", "CAMEL-CAP"); + assertEquals("not enough CAMEL-CAP", reply); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + // a reply that is text but not a String, as a route that sets the body with a Groovy GString + from("sjms:queue:" + TEST_REPLY_DESTINATION_NAME) + .process(e -> e.getMessage().setBody(new ExternalText("not enough " + e.getMessage().getBody()))); + } + }; + } +} diff --git a/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/SjmsObjectMessageEnabledTest.java b/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/SjmsObjectMessageEnabledTest.java index 050876a9f6c2..bea3796a71f6 100644 --- a/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/SjmsObjectMessageEnabledTest.java +++ b/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/SjmsObjectMessageEnabledTest.java @@ -59,6 +59,19 @@ public class SjmsObjectMessageEnabledTest extends JmsTestSupport { "Expected guidance to enable objectMessageEnabled=true, got: " + cause.getMessage()); } + @Test + public void testProducerErrorNamesTheBodyClass() { + CamelExecutionException ex = assertThrows(CamelExecutionException.class, + () -> template.sendBody("sjms:queue:" + DISABLED_QUEUE, new MyOrder("beer", 10))); + Throwable cause = rootCause(ex); + assertTrue(cause.getMessage().contains(MyOrder.class.getName()), + "Expected the class of the body, got: " + cause.getMessage()); + assertTrue(cause.getMessage().contains("convertBodyTo"), + "Expected guidance to convert the body to a String, got: " + cause.getMessage()); + assertTrue(cause.getMessage().contains("objectMessageEnabled=true"), + "Expected guidance to enable objectMessageEnabled=true, got: " + cause.getMessage()); + } + @Test public void testProducerAcceptsSerializableBodyWhenEnabled() throws Exception { // when objectMessageEnabled=true, sending a Serializable body via Object message type should succeed diff --git a/components/camel-sjms2/src/main/docs/sjms2-component.adoc b/components/camel-sjms2/src/main/docs/sjms2-component.adoc index 68638507fce9..9fa42cf85235 100644 --- a/components/camel-sjms2/src/main/docs/sjms2-component.adoc +++ b/components/camel-sjms2/src/main/docs/sjms2-component.adoc @@ -303,7 +303,7 @@ The body is mapped to and from a JMS message as follows (unless the option `jmsM [width="100%",cols="1,1,2",options="header"] |=== | Camel body | JMS message | Body when consumed -| `String` or XML `Node` | `TextMessage` | `String` +| `String`, other text (a `CharSequence` such as a `StringBuilder`) or XML `Node` | `TextMessage` | `String` | `byte[]`, `InputStream`, `Reader`, `ByteBuffer`, `File`, `Path` or `StreamCache` | `BytesMessage` | `byte[]` | `Map` (of simple values) | `MapMessage` | `Map` | `null` | `Message` | `null` 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 435bf93c39fe..e4adc9de9fff 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 @@ -3454,6 +3454,19 @@ To send such a body, implement `java.io.Serializable` (and enable `objectMessage `jmsMessageType` option, or marshal the body (for example to JSON) before sending it. This applies to the components that build on camel-jms (`camel-amqp`, `camel-activemq`, `camel-activemq6`) and to `camel-sjms2`. +=== camel-jms, camel-sjms - text that is not a String is sent as a TextMessage + +A body that is text but not a `String` (a `CharSequence` such as a `StringBuilder` or a Groovy GString) is now sent as +a `jakarta.jms.TextMessage`, as a `String` is, and a consumer gets it as a `String`. Before, it was sent as a +`jakarta.jms.ObjectMessage`, as it is `Serializable`. That failed, as `objectMessageEnabled` is `false` by default. +With `objectMessageEnabled=true` the consumer got the serialized object, or failed on the deserialization filter when +the class is not allowed (a GString is not). To send such a body as an `ObjectMessage`, set `jmsMessageType=Object`. + +When a body would be sent as an `ObjectMessage` and `objectMessageEnabled` is `false`, the error now names the class +of the body. It also says how to send the body as text: convert it to a `String` first, or marshal it (for example to +JSON). This applies to the components that build on camel-jms (`camel-amqp`, `camel-activemq`, `camel-activemq6`) and +to `camel-sjms2`. + === camel-pulsar - the producer continues the sequence ids of the broker The `pulsar` producer no longer passes the default `initialSequenceId=-1` to the Pulsar client, so the client
