Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@

* Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
* 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)).

## New Features / Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
* <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
Expand Down Expand Up @@ -221,6 +221,13 @@
* 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=
Expand Down Expand Up @@ -458,7 +465,7 @@ public static Read<Solace.Record> read() {
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,7 @@ public class MessageProducerUtils {
*/
public static BytesXMLMessage createBytesXMLMessage(
Comment thread
ngibanel marked this conversation as resolved.
Outdated
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 =
Expand All @@ -64,7 +55,6 @@ public static BytesXMLMessage createBytesXMLMessage(
// Use only a string as correlation key
msg.setCorrelationKey(record.getMessageId());
}
msg.setApplicationMessageId(record.getMessageId());
return msg;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@
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 com.solacesystems.jcsmp.JCSMPFactory;
import com.solacesystems.jcsmp.TextMessage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
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;
Expand Down Expand Up @@ -112,6 +120,16 @@ public abstract static class Builder {
@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.
Expand Down Expand Up @@ -255,13 +273,27 @@ public abstract static class Record {
@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
Expand All @@ -270,6 +302,14 @@ public abstract static class Builder {

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);
Expand All @@ -295,6 +335,19 @@ public abstract Builder setReplicationGroupMessageId(

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);
}
}
}

/**
Expand Down Expand Up @@ -387,6 +440,7 @@ public abstract static class Builder {
*/
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}.
*
Expand All @@ -396,35 +450,17 @@ public static class SolaceRecordMapper {
* @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())
Expand All @@ -438,7 +474,6 @@ public static class SolaceRecordMapper {
msg.getReplicationGroupMessageId() != null
? msg.getReplicationGroupMessageId().toString()
: null)
.setAttachmentBytes(attachmentBytesStream.toByteArray())
.build();
}

Expand All @@ -462,5 +497,118 @@ public static class SolaceRecordMapper {
}
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];
}
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(msg.getBytes());
return out.toByteArray();
Comment thread
ngibanel marked this conversation as resolved.
Outdated
} catch (IOException e) {
LOG.error("Could not read bytes from BytesXMLMessage.", e);
return new byte[0];
}
}

private static byte[] readAttachment(BytesXMLMessage msg) {
if (msg.getAttachmentContentLength() == 0) {
return new byte[0];
}
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(msg.getAttachmentByteBuffer().array());
return out.toByteArray();
Comment thread
ngibanel marked this conversation as resolved.
Outdated
} catch (IOException e) {
LOG.error("Could not read attachment from BytesXMLMessage.", e);
return new byte[0];
}
}
}
}
Loading
Loading