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 8002509c386f CAMEL-26190: camel-jgroups - a stream or a file body is
sent as its content, and another body that is not Serializable fails with a
hint (#29018)
8002509c386f is described below
commit 8002509c386f481a627cfb6e1cd4cf1a951c776b
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 07:42:57 2026 +0100
CAMEL-26190: camel-jgroups - a stream or a file body is sent as its
content, and another body that is not Serializable fails with a hint (#29018)
…ontent, and another body that is not Serializable fails with a hint
The producer wrapped the body as is in an ObjectMessage, so a stream, a
stream
cache, a GenericFile or a Path failed with a NotSerializableException of
that
class, with no hint what to do. These bodies are now converted to a byte[]
of
their content. Any other body that is neither Serializable nor a JGroups
SizeStreamable fails with an InvalidPayloadException that says to convert it
first. Serializable bodies (including java.io.File) are sent as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/catalog/components/jgroups.json | 2 +-
.../camel/catalog/docs/jgroups-component.adoc | 7 +
.../apache/camel/component/jgroups/jgroups.json | 2 +-
.../src/main/docs/jgroups-component.adoc | 7 +
.../camel/component/jgroups/JGroupsEndpoint.java | 11 +-
.../camel/component/jgroups/JGroupsProducer.java | 25 ++++
.../component/jgroups/JGroupsProducerBodyTest.java | 165 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 9 ++
8 files changed, 222 insertions(+), 6 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups.json
index 79a55abbdfa9..655872db4bcc 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jgroups.json
@@ -41,7 +41,7 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "unknown" ], "dependsOn": [
"enableViewMessages" ], "description": "The object the sender put in the
message, Java-deserialized: what a jgroups producer sent, such as a String, a
byte array, a Map or a Serializable POJO, if the deserialization filter accepts
its class. With enableViewMessages an org.jgroups.View also arrives on each
change of the members of the cluster" },
- "producerIn": { "carriers": [ "text", "bytes", "value", "map", "list",
"file", "pojo" ], "description": "The body as is, Java-serialized into the
message, so it must be Serializable: a stream or another object that is not
fails with a NotSerializableException. A null body is not sent. The
CamelJGroupsDest header sends it to one member instead of the cluster" },
+ "producerIn": { "carriers": [ "text", "bytes", "value", "map", "list",
"stream", "file", "pojo" ], "description": "The body as is, Java-serialized
into the message, so it must be Serializable. A stream (an InputStream, a
Reader or a stream cache), a file from the file component or a Path is sent as
a byte array of its content, and a java.io.File as is; another object that is
not Serializable fails with an InvalidPayloadException. A null body is not
sent. The CamelJGroupsDest header s [...]
"producerOut": { "carriers": [ "unchanged" ] }
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-component.adoc
index ed05c593ccfa..e75c9ad64acd 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jgroups-component.adoc
@@ -183,6 +183,13 @@ Using `jgroups` component on the producer side of the
route will forward
body of the Camel exchanges to the `JChannel` instance managed by the
endpoint.
+The body is Java-serialized into the JGroups message, so it must be
`Serializable`.
+A stream body (such as an `InputStream`, a `Reader` or a stream cache), a file
+from the `file` component and a `java.nio.file.Path` are read and sent as a
`byte[]`
+of their content. A `java.io.File` is `Serializable` and is sent as is. Any
other
+body that is not `Serializable` fails with an `InvalidPayloadException` that
says to
+convert it first, for example to a `String` or a `byte[]`.
+
[tabs]
====
Java::
diff --git
a/components/camel-jgroups/src/generated/resources/META-INF/org/apache/camel/component/jgroups/jgroups.json
b/components/camel-jgroups/src/generated/resources/META-INF/org/apache/camel/component/jgroups/jgroups.json
index 79a55abbdfa9..655872db4bcc 100644
---
a/components/camel-jgroups/src/generated/resources/META-INF/org/apache/camel/component/jgroups/jgroups.json
+++
b/components/camel-jgroups/src/generated/resources/META-INF/org/apache/camel/component/jgroups/jgroups.json
@@ -41,7 +41,7 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "unknown" ], "dependsOn": [
"enableViewMessages" ], "description": "The object the sender put in the
message, Java-deserialized: what a jgroups producer sent, such as a String, a
byte array, a Map or a Serializable POJO, if the deserialization filter accepts
its class. With enableViewMessages an org.jgroups.View also arrives on each
change of the members of the cluster" },
- "producerIn": { "carriers": [ "text", "bytes", "value", "map", "list",
"file", "pojo" ], "description": "The body as is, Java-serialized into the
message, so it must be Serializable: a stream or another object that is not
fails with a NotSerializableException. A null body is not sent. The
CamelJGroupsDest header sends it to one member instead of the cluster" },
+ "producerIn": { "carriers": [ "text", "bytes", "value", "map", "list",
"stream", "file", "pojo" ], "description": "The body as is, Java-serialized
into the message, so it must be Serializable. A stream (an InputStream, a
Reader or a stream cache), a file from the file component or a Path is sent as
a byte array of its content, and a java.io.File as is; another object that is
not Serializable fails with an InvalidPayloadException. A null body is not
sent. The CamelJGroupsDest header s [...]
"producerOut": { "carriers": [ "unchanged" ] }
},
"properties": {
diff --git a/components/camel-jgroups/src/main/docs/jgroups-component.adoc
b/components/camel-jgroups/src/main/docs/jgroups-component.adoc
index ed05c593ccfa..e75c9ad64acd 100644
--- a/components/camel-jgroups/src/main/docs/jgroups-component.adoc
+++ b/components/camel-jgroups/src/main/docs/jgroups-component.adoc
@@ -183,6 +183,13 @@ Using `jgroups` component on the producer side of the
route will forward
body of the Camel exchanges to the `JChannel` instance managed by the
endpoint.
+The body is Java-serialized into the JGroups message, so it must be
`Serializable`.
+A stream body (such as an `InputStream`, a `Reader` or a stream cache), a file
+from the `file` component and a `java.nio.file.Path` are read and sent as a
`byte[]`
+of their content. A `java.io.File` is `Serializable` and is sent as is. Any
other
+body that is not `Serializable` fails with an `InvalidPayloadException` that
says to
+convert it first, for example to a `String` or a `byte[]`.
+
[tabs]
====
Java::
diff --git
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
index 5be04b8b9910..6dbd0d58c75a 100644
---
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
+++
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
@@ -58,11 +58,14 @@ import org.slf4j.LoggerFactory;
+ " members of the cluster"),
producerIn = @BodyType(value = {
Carrier.TEXT, Carrier.BYTES, Carrier.VALUE, Carrier.MAP,
Carrier.LIST,
- Carrier.FILE, Carrier.POJO },
+ Carrier.STREAM, Carrier.FILE, Carrier.POJO },
description = "The body as is,
Java-serialized into the message, so it must be"
- + " Serializable: a stream or
another object that is not fails with a"
- + " NotSerializableException.
A null body is not sent. The CamelJGroupsDest header"
- + " sends it to one member
instead of the cluster"),
+ + " Serializable. A stream (an
InputStream, a Reader or a stream cache), a"
+ + " file from the file
component or a Path is sent as a byte array of its"
+ + " content, and a
java.io.File as is;"
+ + " another object that is not
Serializable fails with an"
+ + " InvalidPayloadException. A
null body is not sent. The CamelJGroupsDest"
+ + " header sends it to one
member instead of the cluster"),
producerOut = @BodyType(Carrier.UNCHANGED))
public class JGroupsEndpoint extends DefaultEndpoint {
diff --git
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
index e635c33d4efb..7fa042e7d050 100644
---
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
+++
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
@@ -16,11 +16,20 @@
*/
package org.apache.camel.component.jgroups;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.Serializable;
+import java.nio.file.Path;
+
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
import org.apache.camel.support.DefaultProducer;
import org.jgroups.Address;
import org.jgroups.Message;
import org.jgroups.ObjectMessage;
+import org.jgroups.util.SizeStreamable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -73,6 +82,7 @@ public class JGroupsProducer extends DefaultProducer {
public void process(Exchange exchange) throws Exception {
Object body = exchange.getIn().getBody();
if (body != null) {
+ body = toSerializableBody(exchange, body);
Address destinationAddress =
exchange.getIn().getHeader(JGroupsConstants.HEADER_JGROUPS_DEST, Address.class);
Address sourceAddress =
exchange.getIn().getHeader(JGroupsConstants.HEADER_JGROUPS_SRC, Address.class);
@@ -91,4 +101,19 @@ public class JGroupsProducer extends DefaultProducer {
}
}
+ private static Object toSerializableBody(Exchange exchange, Object body)
throws InvalidPayloadException {
+ if (body instanceof Serializable || body instanceof SizeStreamable) {
+ return body;
+ }
+ if (body instanceof StreamCache || body instanceof InputStream || body
instanceof Reader
+ || body instanceof WrappedFile || body instanceof Path) {
+ // a stream or a file has no serialized form, so send its content
+ return exchange.getIn().getMandatoryBody(byte[].class);
+ }
+ throw new InvalidPayloadException(
+ exchange, Serializable.class, exchange.getIn(),
+ "the body is Java-serialized into the JGroups message, so
convert it first, for example to a String"
+ + " or a
byte[]");
+ }
+
}
diff --git
a/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsProducerBodyTest.java
b/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsProducerBodyTest.java
new file mode 100644
index 000000000000..cf82ce4b22f8
--- /dev/null
+++
b/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsProducerBodyTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.jgroups;
+
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.StreamCache;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A body that has no serialized form (a stream, a stream cache, a file from
the file component or a Path) is sent as
+ * its content, and another body that is not Serializable fails with a hint to
convert it.
+ */
+public class JGroupsProducerBodyTest extends CamelTestSupport {
+
+ static final String CLUSTER_NAME = "JGroupsProducerBodyTest";
+
+ static final String CONTENT = "Hello JGroups";
+
+ @TempDir
+ Path inbox;
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("jgroups:" + CLUSTER_NAME +
"?acceptAllObjects=true").to("mock:received");
+
+ from("direct:cached").streamCaching().to("jgroups:" +
CLUSTER_NAME);
+ from("direct:stream").noStreamCaching().to("jgroups:" +
CLUSTER_NAME);
+ from("file:" + inbox +
"?initialDelay=0&delay=10").noAutoStartup().routeId("file")
+ .to("jgroups:" + CLUSTER_NAME);
+ }
+ };
+ }
+
+ @Test
+ public void shouldSendStreamCacheContent() throws Exception {
+ MockEndpoint received = getMockEndpoint("mock:received");
+ received.expectedMessageCount(1);
+
+ template.send("direct:cached", exchange -> {
+ exchange.getIn().setBody(new
ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8)));
+ });
+
+ received.assertIsSatisfied();
+ assertArrayEquals(CONTENT.getBytes(StandardCharsets.UTF_8),
+
received.getReceivedExchanges().get(0).getIn().getBody(byte[].class));
+ }
+
+ @Test
+ public void shouldKeepStreamCacheReadableAfterSend() throws Exception {
+ getMockEndpoint("mock:received").expectedMessageCount(1);
+
+ Object body = template.send("direct:cached", exchange -> {
+ exchange.getIn().setBody(new
ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8)));
+ }).getMessage().getBody();
+
+ MockEndpoint.assertIsSatisfied(context);
+ StreamCache cache = assertInstanceOf(StreamCache.class, body);
+ assertArrayEquals(CONTENT.getBytes(StandardCharsets.UTF_8),
+ context.getTypeConverter().convertTo(byte[].class, cache));
+ }
+
+ @Test
+ public void shouldSendInputStreamContent() throws Exception {
+ MockEndpoint received = getMockEndpoint("mock:received");
+ received.expectedMessageCount(1);
+
+ template.sendBody("direct:stream", new
ByteArrayInputStream(CONTENT.getBytes(StandardCharsets.UTF_8)));
+
+ received.assertIsSatisfied();
+ assertArrayEquals(CONTENT.getBytes(StandardCharsets.UTF_8),
+
received.getReceivedExchanges().get(0).getIn().getBody(byte[].class));
+ }
+
+ @Test
+ public void shouldSendReaderContent() throws Exception {
+ MockEndpoint received = getMockEndpoint("mock:received");
+ received.expectedMessageCount(1);
+
+ template.sendBody("direct:stream", new StringReader(CONTENT));
+
+ received.assertIsSatisfied();
+ assertArrayEquals(CONTENT.getBytes(StandardCharsets.UTF_8),
+
received.getReceivedExchanges().get(0).getIn().getBody(byte[].class));
+ }
+
+ @Test
+ public void shouldSendFileContent() throws Exception {
+ MockEndpoint received = getMockEndpoint("mock:received");
+ received.expectedMessageCount(1);
+
+ Files.writeString(inbox.resolve("hello.txt"), CONTENT);
+ context.getRouteController().startRoute("file");
+
+ received.assertIsSatisfied();
+ assertArrayEquals(CONTENT.getBytes(StandardCharsets.UTF_8),
+
received.getReceivedExchanges().get(0).getIn().getBody(byte[].class));
+ }
+
+ @Test
+ public void shouldSendPathContent() throws Exception {
+ MockEndpoint received = getMockEndpoint("mock:received");
+ received.expectedMessageCount(1);
+
+ Path file = Files.writeString(inbox.resolve("hello.txt"), CONTENT);
+ template.sendBody("direct:stream", file);
+
+ received.assertIsSatisfied();
+ assertArrayEquals(CONTENT.getBytes(StandardCharsets.UTF_8),
+
received.getReceivedExchanges().get(0).getIn().getBody(byte[].class));
+ }
+
+ @Test
+ public void shouldFailWithHintForBodyThatIsNotSerializable() throws
Exception {
+ getMockEndpoint("mock:received").expectedMessageCount(0);
+
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:stream", new
NotSerializableBody()));
+
+ InvalidPayloadException cause =
assertInstanceOf(InvalidPayloadException.class, e.getCause());
+
assertTrue(cause.getMessage().contains(NotSerializableBody.class.getCanonicalName()),
cause.getMessage());
+ assertTrue(cause.getMessage().contains("convert it first"),
cause.getMessage());
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ static final class NotSerializableBody {
+ @Override
+ public String toString() {
+ return "NotSerializableBody";
+ }
+ }
+
+}
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 3994cf3b7d42..158a61a0e8ab 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
@@ -2924,6 +2924,15 @@ header of the incoming message was gone after the
producer. The headers are now
`CamelJcrNodeName` and `CamelJcrNodeType`. Remove these headers before a later
`jcr` producer in the same route
if it must not use them.
+=== camel-jgroups - a stream or a file body is sent as its content
+
+The `jgroups` producer sent the body as is, so a body that is not
`Serializable` failed with a
+`RuntimeException` caused by a `java.io.NotSerializableException`. A stream
body (an `InputStream`, a `Reader` or a
+stream cache), a file from the `file` component and a `java.nio.file.Path` are
now sent as a `byte[]` of their
+content, so the members of the cluster receive a `byte[]`. Any other body that
is not `Serializable` now fails with an
+`InvalidPayloadException` that says to convert it first, before it reaches
JGroups. An `onException` for
+`NotSerializableException` must handle `InvalidPayloadException` instead.
+
=== camel-jira - the producers wait for the answer of Jira
The `addComment`, `addIssueLink`, `addWorkLog`, `attach`, `deleteIssue`,
`transitionIssue` and `watchers` producers