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 99dcc30dfbc4 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 (#29143)
99dcc30dfbc4 is described below
commit 99dcc30dfbc491ae1facd3afcc96b3e96cd8c9d0
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 4 13:05:09 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 (#29143)
* 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
* CAMEL-26567: camel-jms / camel-sjms - text that is not a String in a Map
body goes in the MapMessage as a String
A MapMessage takes only primitive wrappers, String and byte[], so a Map
with a StringBuilder or a Groovy GString value failed with a
MessageFormatException.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../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 | 27 ++++-
.../java/com/example/external/ExternalText.java | 53 ++++++++
.../apache/camel/component/jms/JmsBindingTest.java | 16 +++
.../component/jms/JmsCharSequenceBodyTest.java | 135 +++++++++++++++++++++
.../component/jms/JmsObjectMessageEnabledTest.java | 13 ++
.../camel-sjms/src/main/docs/sjms-component.adoc | 2 +-
.../camel/component/sjms/jms/JmsBinding.java | 27 ++++-
.../java/com/example/external/ExternalText.java | 53 ++++++++
.../sjms/producer/CharSequenceBodyTest.java | 112 +++++++++++++++++
.../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, 470 insertions(+), 12 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..df0b4d06ecd2 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) {
@@ -835,7 +853,10 @@ public class JmsBinding {
for (Entry<?, ?> entry : map.entrySet()) {
String keyString = CamelContextHelper.convertTo(context,
String.class, entry.getKey());
if (keyString != null) {
- message.setObject(keyString, entry.getValue());
+ Object value = entry.getValue();
+ // JMS takes only primitive wrappers, String and byte[] in a
MapMessage, so text such as a
+ // StringBuilder or a Groovy GString goes in as a String
+ message.setObject(keyString, value instanceof CharSequence cs
? cs.toString() : value);
}
}
}
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..a618bcd4d7cb
--- /dev/null
+++
b/components/camel-jms/src/test/java/org/apache/camel/component/jms/JmsCharSequenceBodyTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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 java.util.LinkedHashMap;
+import java.util.Map;
+
+import jakarta.jms.MapMessage;
+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 testTextInAMapIsSentAsString() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:map");
+ mock.expectedMessageCount(1);
+
+ // a Map with text that is not a String, as a Groovy script that
returns [status: "not enough ${sku}"]
+ Map<String, Object> body = new LinkedHashMap<>();
+ body.put("status", new ExternalText("not enough CAMEL-CAP"));
+ body.put("note", new StringBuilder("back order"));
+ body.put("count", 3);
+ template.sendBody("jms:queue:JmsCharSequenceBodyTest.map", body);
+
+ MockEndpoint.assertIsSatisfied(context);
+ JmsMessage in =
mock.getReceivedExchanges().get(0).getIn(JmsMessage.class);
+ assertInstanceOf(MapMessage.class, in.getJmsMessage());
+ Map<?, ?> received = in.getBody(Map.class);
+ assertEquals("not enough CAMEL-CAP", received.get("status"));
+ assertEquals("back order", received.get("note"));
+ assertEquals(3, received.get("count"));
+ }
+
+ @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");
+
+ from("jms:queue:JmsCharSequenceBodyTest.map").to("mock:map");
+
+ // 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..bea673b2193d 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) {
@@ -614,7 +632,10 @@ public class JmsBinding {
for (Map.Entry<?, ?> entry : map.entrySet()) {
String keyString = CamelContextHelper.convertTo(context,
String.class, entry.getKey());
if (keyString != null) {
- message.setObject(keyString, entry.getValue());
+ Object value = entry.getValue();
+ // JMS takes only primitive wrappers, String and byte[] in a
MapMessage, so text such as a
+ // StringBuilder or a Groovy GString goes in as a String
+ message.setObject(keyString, value instanceof CharSequence cs
? cs.toString() : value);
}
}
}
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..f1455e4110da
--- /dev/null
+++
b/components/camel-sjms/src/test/java/org/apache/camel/component/sjms/producer/CharSequenceBodyTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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 java.util.LinkedHashMap;
+import java.util.Map;
+
+import jakarta.jms.MapMessage;
+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_MAP_DESTINATION_NAME =
"test.map.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 testTextInAMapIsSentAsString() throws Exception {
+ MessageConsumer mc = createQueueConsumer(TEST_MAP_DESTINATION_NAME);
+
+ // a Map with text that is not a String, as a Groovy script that
returns [status: "not enough ${sku}"]
+ Map<String, Object> body = new LinkedHashMap<>();
+ body.put("status", new ExternalText("not enough CAMEL-CAP"));
+ body.put("note", new StringBuilder("back order"));
+ body.put("count", 3);
+ template.sendBody("sjms:queue:" + TEST_MAP_DESTINATION_NAME, body);
+
+ Message message = mc.receive(5000);
+ MapMessage map = assertInstanceOf(MapMessage.class, message);
+ assertEquals("not enough CAMEL-CAP", map.getObject("status"));
+ assertEquals("back order", map.getObject("note"));
+ assertEquals(3, map.getObject("count"));
+ 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 99695579e3e7..d4239acd4a97 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
@@ -3486,6 +3486,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