This is an automated email from the ASF dual-hosted git repository.
stankiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new e6d7f23de91 SolaceIO: support binary content (text, bytes) data
payload (#39876)
e6d7f23de91 is described below
commit e6d7f23de9106d62184779524184351df75d3f1b
Author: Nicolas Gibanel <[email protected]>
AuthorDate: Tue Sep 1 08:33:57 2026 +0200
SolaceIO: support binary content (text, bytes) data payload (#39876)
* SolaceIO: support binary content (text, bytes) data payload
* fixup: rename createBytesXMLMessage() to createMessage()
* fixup: simplify and optimize read bytes
---------
Co-authored-by: Nicolas Gibanel <[email protected]>
---
CHANGES.md | 1 +
.../org/apache/beam/sdk/io/solace/SolaceIO.java | 11 +-
.../sdk/io/solace/broker/MessageProducerUtils.java | 16 +-
.../io/solace/broker/SolaceMessageProducer.java | 4 +-
.../org/apache/beam/sdk/io/solace/data/Solace.java | 189 +++++++++++--
.../beam/sdk/io/solace/SolaceIOWriteTest.java | 83 ++++++
.../sdk/io/solace/data/SolaceRecordMapperTest.java | 313 +++++++++++++++++++++
.../beam/sdk/io/solace/data/SolaceRecordTest.java | 44 +++
8 files changed, 618 insertions(+), 43 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 6357802c0ee..5b52e9319c8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -67,6 +67,7 @@
* Support for X source added (Java/Python)
([#X](https://github.com/apache/beam/issues/X)).
* Added `schema_update_options` to `WriteToBigQuery` file loads, allowing
BigQuery load jobs to add nullable fields or relax required fields when
appending data (Python) ([#21141](https://github.com/apache/beam/issues/21141)).
* BigQueryIO now supports reading BigQuery Lakehouse runtime catalog (BigLake
metastore) Iceberg tables with the Storage Read API, using 4-part
`project.catalog.namespace.table` identifiers (or a `TableReference` with a
composite `catalog.namespace` dataset id). Previously such references were
silently mis-parsed (Java)
([#39597](https://github.com/apache/beam/issues/39597)) .
+* SolaceIO now supports reading and writing binary and text content data
payload (Java) ([#39875](https://github.com/apache/beam/issues/39875)).
* ClickHouseIO: support writing `Decimal(P, S)` / `Decimal32/64/128/256`
columns (Java) ([#39840](https://github.com/apache/beam/issues/39840)).
## New Features / Improvements
diff --git
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/SolaceIO.java
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/SolaceIO.java
index c0ecac96fdb..57f65ae6265 100644
---
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/SolaceIO.java
+++
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/SolaceIO.java
@@ -84,7 +84,7 @@ import org.slf4j.LoggerFactory;
* <h3>No-argument {@link SolaceIO#read()} top-level method</h3>
*
* <p>This method returns a PCollection of {@link Solace.Record} objects. It
uses a default mapper
- * ({@link SolaceRecordMapper#map(BytesXMLMessage)}) to map from the received
{@link
+ * ({@link SolaceRecordMapper#toRecord(BytesXMLMessage)}) to map from the
received {@link
* BytesXMLMessage} from Solace, to the {@link Solace.Record} objects.
*
* <p>By default, it also uses a {@link BytesXMLMessage#getSenderTimestamp()}
for watermark
@@ -221,6 +221,13 @@ import org.slf4j.LoggerFactory;
* also use {@link #write(SerializableFunction)} to specify a format function
to convert the input
* type to {@link Solace.Record}.
*
+ * <p>Each record can select its JCSMP payload representation through {@link
+ * Solace.Record.PayloadType}. The default is {@link
Solace.Record.PayloadType#BYTES_XML}, which
+ * preserves the historical behavior of writing the byte array with {@code
+ * BytesXMLMessage.writeBytes}. Use {@code setText(String)} to create a UTF-8
{@link
+ * com.solacesystems.jcsmp.TextMessage}, or select {@link
Solace.Record.PayloadType#BYTES} to
+ * publish the byte array with a JCSMP {@link
com.solacesystems.jcsmp.BytesMessage}.
+ *
* <h3>Writing to a static topic or queue</h3>
*
* <p>The connector uses the <a href=
@@ -460,7 +467,7 @@ public class SolaceIO {
return new Read<Solace.Record>(
Read.Configuration.<Solace.Record>builder()
.setTypeDescriptor(TypeDescriptor.of(Solace.Record.class))
- .setParseFn(SolaceRecordMapper::map)
+ .setParseFn(SolaceRecordMapper::toRecord)
.setTimestampFn(SENDER_TIMESTAMP_FUNCTION)
.setDeduplicateRecords(DEFAULT_DEDUPLICATE_RECORDS)
.setWatermarkIdleDurationThreshold(DEFAULT_WATERMARK_IDLE_DURATION_THRESHOLD)
diff --git
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/MessageProducerUtils.java
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/MessageProducerUtils.java
index dd4610910ff..1b258dfeb62 100644
---
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/MessageProducerUtils.java
+++
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/MessageProducerUtils.java
@@ -40,18 +40,9 @@ public class MessageProducerUtils {
* @param deliveryMode The {@link DeliveryMode} used to publish the message.
* @return A {@link BytesXMLMessage} that can be sent to Solace "as is".
*/
- public static BytesXMLMessage createBytesXMLMessage(
+ public static BytesXMLMessage createMessage(
Solace.Record record, boolean useCorrelationKeyLatency, DeliveryMode
deliveryMode) {
- JCSMPFactory jcsmpFactory = JCSMPFactory.onlyInstance();
- BytesXMLMessage msg = jcsmpFactory.createBytesXMLMessage();
- byte[] payload = record.getPayload();
- msg.writeBytes(payload);
-
- Long senderTimestamp = record.getSenderTimestamp();
- if (senderTimestamp == null) {
- senderTimestamp = System.currentTimeMillis();
- }
- msg.setSenderTimestamp(senderTimestamp);
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
msg.setDeliveryMode(deliveryMode);
if (useCorrelationKeyLatency) {
Solace.CorrelationKey key =
@@ -64,7 +55,6 @@ public class MessageProducerUtils {
// Use only a string as correlation key
msg.setCorrelationKey(record.getMessageId());
}
- msg.setApplicationMessageId(record.getMessageId());
return msg;
}
@@ -100,7 +90,7 @@ public class MessageProducerUtils {
JCSMPSendMultipleEntry entry =
JCSMPFactory.onlyInstance()
.createSendMultipleEntry(
- createBytesXMLMessage(record, useCorrelationKeyLatency,
deliveryMode),
+ createMessage(record, useCorrelationKeyLatency,
deliveryMode),
destinationFn.apply(record));
entries[i] = entry;
}
diff --git
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/SolaceMessageProducer.java
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/SolaceMessageProducer.java
index b3806b5afae..227f72fb2b6 100644
---
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/SolaceMessageProducer.java
+++
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/SolaceMessageProducer.java
@@ -17,8 +17,8 @@
*/
package org.apache.beam.sdk.io.solace.broker;
-import static
org.apache.beam.sdk.io.solace.broker.MessageProducerUtils.createBytesXMLMessage;
import static
org.apache.beam.sdk.io.solace.broker.MessageProducerUtils.createJCSMPSendMultipleEntry;
+import static
org.apache.beam.sdk.io.solace.broker.MessageProducerUtils.createMessage;
import com.solacesystems.jcsmp.BytesXMLMessage;
import com.solacesystems.jcsmp.DeliveryMode;
@@ -50,7 +50,7 @@ public class SolaceMessageProducer implements MessageProducer
{
Destination topicOrQueue,
boolean useCorrelationKeyLatency,
DeliveryMode deliveryMode) {
- BytesXMLMessage msg = createBytesXMLMessage(record,
useCorrelationKeyLatency, deliveryMode);
+ BytesXMLMessage msg = createMessage(record, useCorrelationKeyLatency,
deliveryMode);
Callable<Integer> publish =
() -> {
producer.send(msg, topicOrQueue);
diff --git
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/data/Solace.java
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/data/Solace.java
index e6cd35b63b4..15fe06103fb 100644
---
a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/data/Solace.java
+++
b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/data/Solace.java
@@ -18,12 +18,19 @@
package org.apache.beam.sdk.io.solace.data;
import com.google.auto.value.AutoValue;
+import com.solacesystems.jcsmp.BytesMessage;
import com.solacesystems.jcsmp.BytesXMLMessage;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
+import com.solacesystems.jcsmp.JCSMPFactory;
+import com.solacesystems.jcsmp.TextMessage;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
import org.apache.beam.sdk.schemas.AutoValueSchema;
import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -112,6 +119,16 @@ public class Solace {
@AutoValue
@DefaultSchema(AutoValueSchema.class)
public abstract static class Record {
+ /** Identifies how the record payload is represented in a JCSMP message. */
+ public enum PayloadType {
+ /** The legacy XML-data payload written with {@code
BytesXMLMessage.writeBytes}. */
+ BYTES_XML,
+ /** A text payload written with {@code TextMessage.setText}. */
+ TEXT,
+ /** A binary payload written with {@code BytesMessage.setData}. */
+ BYTES;
+ }
+
/**
* Gets the unique identifier of the message, a string for an
application-specific message
* identifier.
@@ -255,13 +272,27 @@ public class Solace {
@SchemaFieldNumber("12")
public abstract byte[] getAttachmentBytes();
+ /** Gets the JCSMP payload representation used for this record. */
+ @SchemaFieldNumber("13")
+ public abstract PayloadType getPayloadType();
+
+ /** Gets the payload decoded as UTF-8 when this record has type {@link
PayloadType#TEXT}. */
+ public final String getText() {
+ if (getPayloadType() != PayloadType.TEXT) {
+ throw new IllegalStateException(
+ "Text is only available for records with payload type TEXT.");
+ }
+ return decodeUtf8(getPayload());
+ }
+
public static Builder builder() {
return new AutoValue_Solace_Record.Builder()
.setExpiration(0L)
.setPriority(-1)
.setRedelivered(false)
.setTimeToLive(0)
- .setAttachmentBytes(new byte[0]);
+ .setAttachmentBytes(new byte[0])
+ .setPayloadType(PayloadType.BYTES_XML);
}
@AutoValue.Builder
@@ -270,6 +301,14 @@ public class Solace {
public abstract Builder setPayload(byte[] payload);
+ public abstract Builder setPayloadType(PayloadType payloadType);
+
+ /** Sets a UTF-8 text payload and selects {@link PayloadType#TEXT}. */
+ public Builder setText(String text) {
+ byte[] payload = text == null ? new byte[0] :
text.getBytes(StandardCharsets.UTF_8);
+ return setPayloadType(PayloadType.TEXT).setPayload(payload);
+ }
+
public abstract Builder setDestination(@Nullable Destination
destination);
public abstract Builder setExpiration(long expiration);
@@ -295,6 +334,19 @@ public class Solace {
public abstract Record build();
}
+
+ private static String decodeUtf8(byte[] payload) {
+ try {
+ return StandardCharsets.UTF_8
+ .newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ .decode(ByteBuffer.wrap(payload))
+ .toString();
+ } catch (CharacterCodingException e) {
+ throw new IllegalArgumentException("Text payload is not valid UTF-8.",
e);
+ }
+ }
}
/**
@@ -387,6 +439,7 @@ public class Solace {
*/
public static class SolaceRecordMapper {
private static final Logger LOG =
LoggerFactory.getLogger(SolaceRecordMapper.class);
+
/**
* Maps a {@link BytesXMLMessage} (if not null) to a {@link Solace.Record}.
*
@@ -396,35 +449,17 @@ public class Solace {
* @param msg The Solace message to map.
* @return A Solace Record representing the message, or null if the input
message was null.
*/
- public static @Nullable Record map(@Nullable BytesXMLMessage msg) {
+ public static @Nullable Record toRecord(@Nullable BytesXMLMessage msg) {
if (msg == null) {
return null;
}
- ByteArrayOutputStream payloadBytesStream = new ByteArrayOutputStream();
- if (msg.getContentLength() != 0) {
- try {
- payloadBytesStream.write(msg.getBytes());
- } catch (IOException e) {
- LOG.error("Could not write bytes from the BytesXMLMessage to the
Solace.record.", e);
- }
- }
-
- ByteArrayOutputStream attachmentBytesStream = new
ByteArrayOutputStream();
- if (msg.getAttachmentContentLength() != 0) {
- try {
- attachmentBytesStream.write(msg.getAttachmentByteBuffer().array());
- } catch (IOException e) {
- LOG.error(
- "Could not AttachmentByteBuffer from the BytesXMLMessage to the
Solace.record.", e);
- }
- }
-
Destination replyTo = getDestination(msg.getCorrelationId(),
msg.getReplyTo());
Destination destination = getDestination(msg.getCorrelationId(),
msg.getDestination());
- return Record.builder()
+
+ Record.Builder recordBuilder = decodePayload(msg);
+ return recordBuilder
.setMessageId(msg.getApplicationMessageId())
- .setPayload(payloadBytesStream.toByteArray())
.setDestination(destination)
.setExpiration(msg.getExpiration())
.setPriority(msg.getPriority())
@@ -438,7 +473,6 @@ public class Solace {
msg.getReplicationGroupMessageId() != null
? msg.getReplicationGroupMessageId().toString()
: null)
- .setAttachmentBytes(attachmentBytesStream.toByteArray())
.build();
}
@@ -462,5 +496,108 @@ public class Solace {
}
return destinationBuilder.build();
}
+
+ /**
+ * Maps a {@link Record} to a {@link BytesXMLMessage}.
+ *
+ * <p>Only the fields common to both a {@link Record} and a {@link
BytesXMLMessage} are set: the
+ * payload (according to the record's {@link Record.PayloadType}), the
sender timestamp
+ * (defaulting to the current time when the record does not provide one)
and the application
+ * message id. Publishing-specific fields such as delivery mode or
correlation key are not
+ * handled here and must be set by the caller.
+ *
+ * @param record the {@link Record} to map.
+ * @return a JCSMP {@link BytesXMLMessage} carrying the record's common
fields.
+ */
+ public static BytesXMLMessage toMessage(Record record) {
+ BytesXMLMessage msg = encodePayload(record);
+
+ Long senderTimestamp = record.getSenderTimestamp();
+ if (senderTimestamp == null) {
+ senderTimestamp = System.currentTimeMillis();
+ }
+ msg.setSenderTimestamp(senderTimestamp);
+ msg.setApplicationMessageId(record.getMessageId());
+
+ return msg;
+ }
+
+ /**
+ * Reads the payload from a {@link Solace.Record} into a
partially-populated {@link
+ * BytesXMLMessage}.
+ *
+ * @param record the Solace record.
+ * @return a {@link BytesXMLMessage} with the payload set based on the
record's payload type.
+ */
+ private static BytesXMLMessage encodePayload(Record record) {
+ switch (record.getPayloadType()) {
+ case TEXT:
+ TextMessage text =
JCSMPFactory.onlyInstance().createMessage(TextMessage.class);
+ text.setText(record.getText());
+ return text;
+ case BYTES:
+ BytesMessage bytes =
JCSMPFactory.onlyInstance().createMessage(BytesMessage.class);
+ bytes.setData(record.getPayload());
+ return bytes;
+ case BYTES_XML:
+ BytesXMLMessage xml =
JCSMPFactory.onlyInstance().createBytesXMLMessage();
+ xml.writeBytes(record.getPayload());
+ if (record.getAttachmentBytes().length != 0) {
+ xml.writeAttachment(record.getAttachmentBytes());
+ }
+ return xml;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported payload type: " + record.getPayloadType());
+ }
+ }
+
+ /**
+ * Reads the payload from a {@link BytesXMLMessage} into a
partially-populated {@link
+ * Record.Builder}.
+ *
+ * @param msg the JCSMP message.
+ * @return a {@link Record.Builder} with the payload and payload type set
based on the message
+ * type.
+ */
+ private static Record.Builder decodePayload(@NonNull BytesXMLMessage msg) {
+ if (msg instanceof TextMessage) {
+ String text = ((TextMessage) msg).getText();
+ byte[] payload = text == null ? new byte[0] :
text.getBytes(StandardCharsets.UTF_8);
+ return
Record.builder().setPayloadType(Record.PayloadType.TEXT).setPayload(payload);
+ }
+
+ if (msg instanceof BytesMessage) {
+ byte[] data = ((BytesMessage) msg).getData();
+ byte[] payload = data == null ? new byte[0] : data;
+ return
Record.builder().setPayloadType(Record.PayloadType.BYTES).setPayload(payload);
+ }
+
+ // BYTES_XML fallback
+ byte[] payload = readBytes(msg);
+ byte[] attachment = readAttachment(msg);
+ return Record.builder()
+ .setPayloadType(Record.PayloadType.BYTES_XML)
+ .setPayload(payload)
+ .setAttachmentBytes(attachment);
+ }
+
+ private static byte[] readBytes(BytesXMLMessage msg) {
+ if (msg.getContentLength() == 0) {
+ return new byte[0];
+ }
+ return Arrays.copyOf(msg.getBytes(), msg.getContentLength());
+ }
+
+ private static byte[] readAttachment(BytesXMLMessage msg) {
+ if (msg.getAttachmentContentLength() == 0) {
+ return new byte[0];
+ }
+
+ ByteBuffer buffer = msg.getAttachmentByteBuffer();
+ byte[] attachment = new byte[buffer.remaining()];
+ buffer.get(attachment);
+ return attachment;
+ }
}
}
diff --git
a/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/SolaceIOWriteTest.java
b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/SolaceIOWriteTest.java
index e92657c3c3d..3cdc392fa1f 100644
---
a/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/SolaceIOWriteTest.java
+++
b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/SolaceIOWriteTest.java
@@ -20,8 +20,11 @@ package org.apache.beam.sdk.io.solace;
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
import com.solacesystems.jcsmp.DeliveryMode;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.extensions.avro.coders.AvroCoder;
@@ -82,6 +85,31 @@ public class SolaceIOWriteTest {
.via(kv -> SolaceDataUtils.getSolaceRecord(kv.getValue(),
kv.getKey())));
}
+ private PCollection<Record> getRecordsForEachPayloadTypes(Pipeline p) {
+ TestStream.Builder<Record.PayloadType> kvBuilder =
+
TestStream.create(AvroCoder.of(Record.PayloadType.class)).advanceWatermarkTo(Instant.EPOCH);
+
+ for (var payloadType : Record.PayloadType.values()) {
+ kvBuilder =
+
kvBuilder.addElements(payloadType).advanceProcessingTime(Duration.standardSeconds(60));
+ }
+
+ TestStream<Record.PayloadType> testStream =
kvBuilder.advanceWatermarkToInfinity();
+
+ return p.apply("Test stream ", testStream)
+ .apply(
+ "To Record",
+ MapElements.into(TypeDescriptor.of(Record.class))
+ .via(
+ payloadType ->
+ Solace.Record.builder()
+ .setMessageId(payloadType.name().toLowerCase())
+ .setPayloadType(payloadType)
+ .setPayload(
+ ("payload-" +
payloadType.name()).getBytes(StandardCharsets.UTF_8))
+ .build()));
+ }
+
private SolaceOutput getWriteTransform(
SubmissionMode mode,
WriterType writerType,
@@ -172,6 +200,61 @@ public class SolaceIOWriteTest {
pipeline.run();
}
+ @Test
+ public void testWriteMixedPayloadTypesStreaming() throws Exception {
+ PCollection<Record> records = getRecordsForEachPayloadTypes(pipeline);
+
+ ErrorHandler<BadRecord, PCollection<Long>> errorHandler =
+ pipeline.registerBadRecordErrorHandler(new ErrorSinkTransform());
+
+ SolaceOutput output =
+ records.apply(
+ "Write mixed records",
+ SolaceIO.write()
+ .to(Solace.Queue.fromName("queue"))
+ .withSubmissionMode(SubmissionMode.LOWER_LATENCY)
+ .withWriterType(WriterType.STREAMING)
+ .withDeliveryMode(DeliveryMode.PERSISTENT)
+
.withSessionServiceFactory(MockSessionServiceFactory.builder().build())
+ .withErrorHandler(errorHandler));
+
+ var expectedIds =
+ Stream.of(Record.PayloadType.values())
+ .map(payloadType -> payloadType.name().toLowerCase())
+ .collect(Collectors.toList());
+ PAssert.that(getIdsPCollection(output)).containsInAnyOrder(expectedIds);
+ errorHandler.close();
+ PAssert.that(errorHandler.getOutput()).empty();
+ pipeline.run();
+ }
+
+ @Test
+ public void testWriteMixedPayloadTypesBatched() throws Exception {
+ PCollection<Record> records = getRecordsForEachPayloadTypes(pipeline);
+
+ ErrorHandler<BadRecord, PCollection<Long>> errorHandler =
+ pipeline.registerBadRecordErrorHandler(new ErrorSinkTransform());
+ SolaceOutput output =
+ records.apply(
+ "Write mixed records",
+ SolaceIO.write()
+ .to(Solace.Queue.fromName("queue"))
+ .withSubmissionMode(SubmissionMode.HIGHER_THROUGHPUT)
+ .withWriterType(WriterType.BATCHED)
+ .withDeliveryMode(DeliveryMode.PERSISTENT)
+
.withSessionServiceFactory(MockSessionServiceFactory.builder().build())
+ .withErrorHandler(errorHandler));
+
+ var expectedIds =
+ Stream.of(Record.PayloadType.values())
+ .map(payloadType -> payloadType.name().toLowerCase())
+ .collect(Collectors.toList());
+ PAssert.that(getIdsPCollection(output)).containsInAnyOrder(expectedIds);
+ errorHandler.close();
+ PAssert.that(errorHandler.getOutput()).empty();
+ pipeline.run();
+ }
+
@Test
public void testWriteWithFailedRecords() throws Exception {
SubmissionMode mode = SubmissionMode.HIGHER_THROUGHPUT;
diff --git
a/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/data/SolaceRecordMapperTest.java
b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/data/SolaceRecordMapperTest.java
new file mode 100644
index 00000000000..cc6567b4c88
--- /dev/null
+++
b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/data/SolaceRecordMapperTest.java
@@ -0,0 +1,313 @@
+/*
+ * 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.beam.sdk.io.solace.data;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.solacesystems.jcsmp.BytesMessage;
+import com.solacesystems.jcsmp.BytesXMLMessage;
+import com.solacesystems.jcsmp.DeliveryMode;
+import com.solacesystems.jcsmp.JCSMPFactory;
+import com.solacesystems.jcsmp.TextMessage;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import org.apache.beam.sdk.io.solace.broker.MessageProducerUtils;
+import org.apache.beam.sdk.io.solace.data.Solace.Record;
+import org.apache.beam.sdk.io.solace.data.Solace.Record.PayloadType;
+import org.junit.Test;
+
+public class SolaceRecordMapperTest {
+
+ @Test
+ public void testNullMessage() {
+ assertNull(Solace.SolaceRecordMapper.toRecord(null));
+ }
+
+ @Test
+ public void testTextMessage() {
+ TextMessage message =
JCSMPFactory.onlyInstance().createMessage(TextMessage.class);
+ message.setApplicationMessageId("id");
+ message.setText("héllo");
+
+ Record record = Solace.SolaceRecordMapper.toRecord(message);
+
+ assertEquals(Record.PayloadType.TEXT, record.getPayloadType());
+ assertArrayEquals("héllo".getBytes(StandardCharsets.UTF_8),
record.getPayload());
+ assertArrayEquals(new byte[0], record.getAttachmentBytes());
+ }
+
+ @Test
+ public void testBytesMessage() {
+ byte[] payload = new byte[] {0, 1, (byte) 255};
+ BytesMessage message =
JCSMPFactory.onlyInstance().createMessage(BytesMessage.class);
+ message.setApplicationMessageId("id");
+ message.setData(payload);
+
+ Record record = Solace.SolaceRecordMapper.toRecord(message);
+
+ assertEquals(Record.PayloadType.BYTES, record.getPayloadType());
+ assertArrayEquals(payload, record.getPayload());
+ assertArrayEquals(new byte[0], record.getAttachmentBytes());
+ }
+
+ @Test
+ public void testBytesXmlMessageAndAttachment() {
+ Record source =
+ Solace.Record.builder()
+ .setMessageId("id")
+ .setPayload(new byte[] {1, 2})
+ .setAttachmentBytes(new byte[] {3, 4})
+ .build();
+ BytesXMLMessage message =
+ MessageProducerUtils.createMessage(source, false, DeliveryMode.DIRECT);
+ message.setReadOnly();
+
+ Record record = Solace.SolaceRecordMapper.toRecord(message);
+
+ assertEquals(Record.PayloadType.BYTES_XML, record.getPayloadType());
+ assertArrayEquals(new byte[] {1, 2}, Arrays.copyOf(record.getPayload(),
2));
+ assertArrayEquals(new byte[] {3, 4}, record.getAttachmentBytes());
+ }
+
+ @Test
+ public void testNullTextPayload() {
+ TextMessage message =
JCSMPFactory.onlyInstance().createMessage(TextMessage.class);
+ message.setApplicationMessageId("id");
+
+ Record record = Solace.SolaceRecordMapper.toRecord(message);
+
+ assertEquals(Record.PayloadType.TEXT, record.getPayloadType());
+ assertArrayEquals(new byte[0], record.getPayload());
+ }
+
+ @Test
+ public void testNullBytesPayload() {
+ BytesMessage message =
JCSMPFactory.onlyInstance().createMessage(BytesMessage.class);
+ message.setApplicationMessageId("id");
+
+ Record record = Solace.SolaceRecordMapper.toRecord(message);
+
+ assertEquals(Record.PayloadType.BYTES, record.getPayloadType());
+ assertArrayEquals(new byte[0], record.getPayload());
+ }
+
+ @Test
+ public void testEmptyBytesXmlMessage() {
+ BytesXMLMessage message =
JCSMPFactory.onlyInstance().createBytesXMLMessage();
+ message.setApplicationMessageId("id");
+
+ Record record = Solace.SolaceRecordMapper.toRecord(message);
+
+ assertEquals(Record.PayloadType.BYTES_XML, record.getPayloadType());
+ assertArrayEquals(new byte[0], record.getPayload());
+ assertArrayEquals(new byte[0], record.getAttachmentBytes());
+ }
+
+ @Test
+ public void testMapMessageMetadata() {
+ TextMessage message =
JCSMPFactory.onlyInstance().createMessage(TextMessage.class);
+ message.setApplicationMessageId("id");
+ message.setText("hello");
+ message.setExpiration(123L);
+ message.setPriority(7);
+ message.setReplyTo(JCSMPFactory.onlyInstance().createQueue("reply-queue"));
+ message.setSenderTimestamp(456L);
+ message.setTimeToLive(789L);
+
+ Record record = Solace.SolaceRecordMapper.toRecord(message);
+
+ assertEquals("id", record.getMessageId());
+ assertEquals(123L, record.getExpiration());
+ assertEquals(7, record.getPriority());
+ assertEquals(false, record.getRedelivered());
+ assertEquals("reply-queue", record.getReplyTo().getName());
+ assertEquals(Solace.DestinationType.QUEUE, record.getReplyTo().getType());
+ assertEquals(Long.valueOf(456L), record.getSenderTimestamp());
+ assertEquals(789L, record.getTimeToLive());
+ }
+
+ @Test
+ public void testMapTextRecord() {
+ Record record =
+
Record.builder().setMessageId("id").setText("héllo").setSenderTimestamp(1L).build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertTrue(msg instanceof TextMessage);
+ assertEquals("héllo", ((TextMessage) msg).getText());
+ }
+
+ @Test
+ public void testMapTextRecordWithEmptyText() {
+ Record record =
Record.builder().setMessageId("id").setText("").setSenderTimestamp(1L).build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertTrue(msg instanceof TextMessage);
+ assertEquals("", ((TextMessage) msg).getText());
+ }
+
+ @Test
+ public void testMapBytesRecord() {
+ byte[] payload = new byte[] {0, 1, (byte) 255};
+ Record record =
+ Record.builder()
+ .setMessageId("id")
+ .setPayload(payload)
+ .setPayloadType(PayloadType.BYTES)
+ .setSenderTimestamp(1L)
+ .build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertTrue(msg instanceof BytesMessage);
+ assertArrayEquals(payload, ((BytesMessage) msg).getData());
+ }
+
+ @Test
+ public void testMapBytesXmlRecord() {
+ byte[] payload = new byte[] {1, 2, 3};
+ Record record =
+
Record.builder().setMessageId("id").setPayload(payload).setSenderTimestamp(1L).build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertArrayEquals(payload, Arrays.copyOf(msg.getBytes(),
msg.getContentLength()));
+ }
+
+ @Test
+ public void testMapBytesXmlRecordWithAttachment() {
+ byte[] payload = new byte[] {1, 2};
+ byte[] attachment = new byte[] {3, 4};
+ Record record =
+ Record.builder()
+ .setMessageId("id")
+ .setPayload(payload)
+ .setAttachmentBytes(attachment)
+ .setSenderTimestamp(1L)
+ .build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertEquals(attachment.length, msg.getAttachmentContentLength());
+ assertArrayEquals(attachment, msg.getAttachmentByteBuffer().array());
+ }
+
+ @Test
+ public void testMapBytesXmlRecordWithEmptyAttachment() {
+ Record record =
+ Record.builder()
+ .setMessageId("id")
+ .setPayload(new byte[] {1})
+ .setSenderTimestamp(1L)
+ .build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertEquals(0, msg.getAttachmentContentLength());
+ }
+
+ @Test
+ public void testMapRecordMetadata() {
+ Record record =
+
Record.builder().setMessageId("id").setText("hello").setSenderTimestamp(1L).build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertEquals("id", msg.getApplicationMessageId());
+ assertEquals(Long.valueOf(1L), Long.valueOf(msg.getSenderTimestamp()));
+ }
+
+ @Test
+ public void testToMessageDefaultsSenderTimestamp() {
+ Record record =
Record.builder().setMessageId("id").setText("hello").build();
+
+ long before = System.currentTimeMillis();
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+ long after = System.currentTimeMillis();
+
+ assertTrue(msg.getSenderTimestamp() >= before && msg.getSenderTimestamp()
<= after);
+ }
+
+ @Test
+ public void testToMessageDoesNotSetPublishingFields() {
+ Record record =
+
Record.builder().setMessageId("id").setText("hello").setSenderTimestamp(1L).build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(record);
+
+ assertNull(msg.getCorrelationKey());
+ }
+
+ //
---------------------------------------------------------------------------
+ // round-trip
+ //
---------------------------------------------------------------------------
+ @Test
+ public void testRoundTripTextPayload() {
+ Record original =
+
Record.builder().setMessageId("id").setText("héllo").setSenderTimestamp(1L).build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(original);
+ msg.setApplicationMessageId("id");
+ Record decoded = Solace.SolaceRecordMapper.toRecord(msg);
+
+ assertEquals(original.getPayloadType(), decoded.getPayloadType());
+ assertArrayEquals(original.getPayload(), decoded.getPayload());
+ }
+
+ @Test
+ public void testRoundTripBytesPayload() {
+ byte[] payload = new byte[] {10, 20, 30};
+ Record original =
+ Record.builder()
+ .setMessageId("id")
+ .setPayload(payload)
+ .setPayloadType(PayloadType.BYTES)
+ .setSenderTimestamp(1L)
+ .build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(original);
+ msg.setApplicationMessageId("id");
+ Record decoded = Solace.SolaceRecordMapper.toRecord(msg);
+
+ assertEquals(PayloadType.BYTES, decoded.getPayloadType());
+ assertArrayEquals(payload, decoded.getPayload());
+ }
+
+ @Test
+ public void testRoundTripBytesXmlPayloadWithAttachment() {
+ Record original =
+ Record.builder()
+ .setMessageId("id")
+ .setPayload(new byte[] {1, 2})
+ .setAttachmentBytes(new byte[] {3, 4})
+ .setSenderTimestamp(1L)
+ .build();
+
+ BytesXMLMessage msg = Solace.SolaceRecordMapper.toMessage(original);
+ msg.setApplicationMessageId("id");
+ Record decoded = Solace.SolaceRecordMapper.toRecord(msg);
+
+ assertEquals(PayloadType.BYTES_XML, decoded.getPayloadType());
+ assertArrayEquals(new byte[] {1, 2}, Arrays.copyOf(decoded.getPayload(),
2));
+ assertArrayEquals(new byte[] {3, 4}, decoded.getAttachmentBytes());
+ }
+}
diff --git
a/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/data/SolaceRecordTest.java
b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/data/SolaceRecordTest.java
new file mode 100644
index 00000000000..5b521f7d4e6
--- /dev/null
+++
b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/data/SolaceRecordTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.beam.sdk.io.solace.data;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+import org.apache.beam.sdk.io.solace.data.Solace.Record;
+import org.junit.Test;
+
+public class SolaceRecordTest {
+
+ @Test
+ public void testDefaultPayloadType() {
+ Record record = Record.builder().setMessageId("id").setPayload(new
byte[0]).build();
+
+ assertEquals(Record.PayloadType.BYTES_XML, record.getPayloadType());
+ }
+
+ @Test
+ public void testSetTextPayload() {
+ Record record =
Record.builder().setMessageId("id").setText("héllo").build();
+
+ assertEquals(Record.PayloadType.TEXT, record.getPayloadType());
+ assertArrayEquals("héllo".getBytes(StandardCharsets.UTF_8),
record.getPayload());
+ assertEquals("héllo", record.getText());
+ }
+}