com.solace.spring.cloud.stream.binder.util.XMLMessageMapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-cloud-stream-binder-solace Show documentation
Show all versions of spring-cloud-stream-binder-solace Show documentation
A Spring Cloud Stream Binder implementation using the Solace Java API (JCSMP)
The newest version!
package com.solace.spring.cloud.stream.binder.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.solace.spring.cloud.stream.binder.messaging.SolaceBinderHeaderMeta;
import com.solace.spring.cloud.stream.binder.messaging.SolaceBinderHeaders;
import com.solace.spring.cloud.stream.binder.messaging.SolaceHeaderMeta;
import com.solace.spring.cloud.stream.binder.properties.SolaceConsumerProperties;
import com.solacesystems.common.util.ByteArray;
import com.solacesystems.jcsmp.*;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.binder.BinderHeaders;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.acks.AcknowledgmentCallback;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.MimeType;
import org.springframework.util.SerializationUtils;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
@Slf4j
public class XMLMessageMapper {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final MessageBuilderFactory MESSAGE_BUILDER_FACTORY = new DefaultMessageBuilderFactory();
static final int MESSAGE_VERSION = 1;
static final Encoder DEFAULT_ENCODING = Encoder.BASE64;
private final ObjectWriter stringSetWriter = OBJECT_MAPPER.writerFor(new TypeReference>() {
});
private final ObjectReader stringSetReader = OBJECT_MAPPER.readerFor(new TypeReference>() {
});
private final Set ignoredHeaderProperties = ConcurrentHashMap.newKeySet();
public BytesXMLMessage mapError(BytesXMLMessage inputMessage, SolaceConsumerProperties consumerProperties) {
BytesXMLMessage errorMessage = JCSMPFactory.onlyInstance().createMessage(inputMessage);
if (consumerProperties.getErrorMsgDmqEligible() != null) {
errorMessage.setDMQEligible(consumerProperties.getErrorMsgDmqEligible());
}
if (consumerProperties.getErrorMsgTtl() != null) {
errorMessage.setTimeToLive(consumerProperties.getErrorMsgTtl());
}
if (DeliveryMode.DIRECT.equals(errorMessage.getDeliveryMode())) {
errorMessage.setDeliveryMode(DeliveryMode.PERSISTENT);
}
return errorMessage;
}
public XMLMessage map(Message> message, Collection excludedHeaders, boolean convertNonSerializableHeadersToString, DeliveryMode deliveryMode) {
return map(message.getPayload(), message.getHeaders(), message.getHeaders().getId(), excludedHeaders, convertNonSerializableHeadersToString, deliveryMode);
}
// exposed for testing
@SneakyThrows
XMLMessage map(Object payload, Map headers, UUID messageId, Collection excludedHeaders, boolean convertNonSerializableHeadersToString, DeliveryMode deliveryMode) {
XMLMessage xmlMessage;
SDTMap metadata = map(headers, excludedHeaders, convertNonSerializableHeadersToString);
metadata.putInteger(SolaceBinderHeaders.MESSAGE_VERSION, MESSAGE_VERSION);
if (payload instanceof byte[]) {
BytesMessage bytesMessage = JCSMPFactory.onlyInstance().createMessage(BytesMessage.class);
bytesMessage.setData((byte[]) payload);
xmlMessage = bytesMessage;
} else if (payload instanceof String) {
TextMessage textMessage = JCSMPFactory.onlyInstance().createMessage(TextMessage.class);
textMessage.setText((String) payload);
xmlMessage = textMessage;
} else if (payload instanceof SDTStream) {
StreamMessage streamMessage = JCSMPFactory.onlyInstance().createMessage(StreamMessage.class);
streamMessage.setStream((SDTStream) payload);
xmlMessage = streamMessage;
} else if (payload instanceof SDTMap) {
MapMessage mapMessage = JCSMPFactory.onlyInstance().createMessage(MapMessage.class);
mapMessage.setMap((SDTMap) payload);
xmlMessage = mapMessage;
} else if (payload instanceof Serializable) {
BytesMessage bytesMessage = JCSMPFactory.onlyInstance().createMessage(BytesMessage.class);
bytesMessage.setData(SerializationUtils.serialize(payload));
metadata.putBoolean(SolaceBinderHeaders.SERIALIZED_PAYLOAD, true);
xmlMessage = bytesMessage;
} else {
String msg = String.format("Invalid payload received. Expected %s. Received: %s", String.join(", ", byte[].class.getSimpleName(), String.class.getSimpleName(), SDTStream.class.getSimpleName(), SDTMap.class.getSimpleName(), Serializable.class.getSimpleName()), payload.getClass().getName());
SolaceMessageConversionException exception = new SolaceMessageConversionException(msg);
log.warn(msg, exception);
throw exception;
}
Object contentType = headers.get(MessageHeaders.CONTENT_TYPE);
if (contentType != null) {
// derived from StaticMessageHeaderAccessor.getContentType(Message>)
xmlMessage.setHTTPContentType(contentType instanceof MimeType ? contentType.toString() : MimeType.valueOf(contentType.toString()).toString());
}
// Copy Solace properties from Spring Message to JCSMP XMLMessage
for (Map.Entry> header : SolaceHeaderMeta.META.entrySet()) {
if (!header.getValue().isWritable()) {
continue;
}
Object value = headers.get(header.getKey());
if (value != null) {
if (!header.getValue().getType().isInstance(value)) {
String msg = String.format("Message %s has an invalid value type for header %s. Expected %s but received %s.", messageId, header.getKey(), header.getValue().getType(), value.getClass());
SolaceMessageConversionException exception = new SolaceMessageConversionException(msg);
log.warn(msg, exception);
throw exception;
}
} else if (header.getValue().hasOverriddenDefaultValue()) {
value = header.getValue().getDefaultValueOverride();
} else {
continue;
}
try {
header.getValue().getWriteAction().accept(xmlMessage, value);
} catch (Exception e) {
String msg = String.format("Could not set %s property from header %s of message %s", XMLMessage.class.getSimpleName(), header.getKey(), messageId);
SolaceMessageConversionException exception = new SolaceMessageConversionException(msg, e);
log.warn(msg, exception);
throw exception;
}
}
xmlMessage.setProperties(metadata);
xmlMessage.setDeliveryMode(deliveryMode);
return xmlMessage;
}
public Message> map(XMLMessage xmlMessage, AcknowledgmentCallback acknowledgmentCallback, SolaceConsumerProperties solaceConsumerProperties) {
return map(xmlMessage, acknowledgmentCallback, false, solaceConsumerProperties);
}
public Message> map(XMLMessage xmlMessage, AcknowledgmentCallback acknowledgmentCallback, boolean setRawMessageHeader, SolaceConsumerProperties solaceConsumerProperties) {
try {
return injectRootMessageHeaders(mapInternal(xmlMessage, solaceConsumerProperties), acknowledgmentCallback, setRawMessageHeader ? xmlMessage : null).build();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@SneakyThrows
private AbstractIntegrationMessageBuilder> mapInternal(XMLMessage xmlMessage, SolaceConsumerProperties solaceConsumerProperties) {
SDTMap metadata = xmlMessage.getProperties();
List excludedHeaders = solaceConsumerProperties.getHeaderExclusions();
Object payload;
if (xmlMessage instanceof BytesMessage) {
payload = ((BytesMessage) xmlMessage).getData();
if (metadata != null && metadata.containsKey(SolaceBinderHeaders.SERIALIZED_PAYLOAD)) {
if (metadata.getBoolean(SolaceBinderHeaders.SERIALIZED_PAYLOAD)) {
payload = SerializationUtils.deserialize((byte[]) payload);
}
}
} else if (xmlMessage instanceof TextMessage) {
payload = ((TextMessage) xmlMessage).getText();
} else if (xmlMessage instanceof MapMessage) {
payload = ((MapMessage) xmlMessage).getMap();
} else if (xmlMessage instanceof StreamMessage) {
payload = ((StreamMessage) xmlMessage).getStream();
} else if (xmlMessage instanceof XMLContentMessage) {
payload = ((XMLContentMessage) xmlMessage).getXMLContent();
} else {
String msg = String.format("Invalid message format received. Expected %s. Received: %s", String.join(", ", BytesMessage.class.getSimpleName(), TextMessage.class.getSimpleName(), MapMessage.class.getSimpleName(), StreamMessage.class.getSimpleName(), XMLContentMessage.class.getSimpleName()), xmlMessage.getClass());
SolaceMessageConversionException exception = new SolaceMessageConversionException(msg);
log.warn(msg, exception);
throw exception;
}
boolean isNullPayload = payload == null;
if (isNullPayload) {
//Set empty payload equivalent to null
if (xmlMessage instanceof BytesMessage) {
payload = new byte[0];
} else if (xmlMessage instanceof TextMessage || xmlMessage instanceof XMLContentMessage) {
payload = "";
} else if (xmlMessage instanceof MapMessage) {
payload = JCSMPFactory.onlyInstance().createMap();
} else if (xmlMessage instanceof StreamMessage) {
payload = JCSMPFactory.onlyInstance().createStream();
}
}
AbstractIntegrationMessageBuilder> builder = MESSAGE_BUILDER_FACTORY.withPayload(payload).copyHeaders(map(metadata, excludedHeaders)).setHeaderIfAbsent(MessageHeaders.CONTENT_TYPE, xmlMessage.getHTTPContentType());
if (isNullPayload) {
if (log.isDebugEnabled()) {
log.debug("Null payload detected, setting Spring header " + SolaceBinderHeaders.NULL_PAYLOAD);
}
builder.setHeader(SolaceBinderHeaders.NULL_PAYLOAD, isNullPayload);
}
for (Map.Entry> header : SolaceHeaderMeta.META.entrySet()) {
if (!header.getValue().isReadable()) {
continue;
}
if (excludedHeaders != null && excludedHeaders.contains(header.getKey())) {
continue;
}
if (ignoredHeaderProperties.contains(header.getKey())) {
continue;
}
try {
builder.setHeaderIfAbsent(header.getKey(), header.getValue().getReadAction().apply(xmlMessage));
} catch (UnsupportedOperationException e) {
if (log.isDebugEnabled()) {
log.debug(String.format("Ignoring Solace header %s. Error: %s", header.getKey(), e.getMessage()));
}
ignoredHeaderProperties.add(header.getKey());
}
}
return builder;
}
private AbstractIntegrationMessageBuilder injectRootMessageHeaders(AbstractIntegrationMessageBuilder builder, AcknowledgmentCallback acknowledgmentCallback, Object sourceData) {
return builder.setHeader(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, acknowledgmentCallback).setHeaderIfAbsent(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, new AtomicInteger(0)).setHeader(IntegrationMessageHeaderAccessor.SOURCE_DATA, sourceData);
}
@SneakyThrows
SDTMap map(Map headers, Collection excludedHeaders, boolean convertNonSerializableHeadersToString) {
SDTMap metadata = JCSMPFactory.onlyInstance().createMap();
Set serializedHeaders = new HashSet<>();
for (Map.Entry header : headers.entrySet()) {
if (header.getKey().equalsIgnoreCase(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK) || header.getKey().equalsIgnoreCase(BinderHeaders.TARGET_DESTINATION) || header.getKey().equalsIgnoreCase(SolaceBinderHeaders.CONFIRM_CORRELATION) || SolaceHeaderMeta.META.containsKey(header.getKey()) || SolaceBinderHeaderMeta.META.containsKey(header.getKey())) {
continue;
}
if (excludedHeaders != null && excludedHeaders.contains(header.getKey())) {
continue;
}
addSDTMapObject(metadata, serializedHeaders, header.getKey(), header.getValue(), convertNonSerializableHeadersToString);
}
if (headers.containsKey(SolaceBinderHeaders.PARTITION_KEY)) {
Object partitionKeyObj = headers.get(SolaceBinderHeaders.PARTITION_KEY);
if (partitionKeyObj instanceof String partitionKey) {
metadata.putString(XMLMessage.MessageUserPropertyConstants.QUEUE_PARTITION_KEY, partitionKey);
} else {
String msg = String.format("Incorrect type specified for header '%s'. Expected [%s] but actual type is [%s]", SolaceBinderHeaders.PARTITION_KEY, String.class, partitionKeyObj.getClass());
SolaceMessageConversionException exception = new SolaceMessageConversionException(new IllegalArgumentException(msg));
log.warn(msg, exception);
throw exception;
}
}
if (!serializedHeaders.isEmpty()) {
String var1 = stringSetWriter.writeValueAsString(serializedHeaders);
metadata.putString(SolaceBinderHeaders.SERIALIZED_HEADERS, var1);
metadata.putString(SolaceBinderHeaders.SERIALIZED_HEADERS_ENCODING, DEFAULT_ENCODING.getName());
}
return metadata;
}
@SneakyThrows
MessageHeaders map(SDTMap metadata, Collection excludedHeaders) {
if (metadata == null) {
return new MessageHeaders(Collections.emptyMap());
}
final Collection exclusionList = excludedHeaders != null ? excludedHeaders : Collections.emptyList();
Map headers = new HashMap<>();
// Deserialize headers
if (!exclusionList.contains(SolaceBinderHeaders.SERIALIZED_HEADERS) && metadata.containsKey(SolaceBinderHeaders.SERIALIZED_HEADERS)) {
Encoder encoder = null;
if (metadata.containsKey(SolaceBinderHeaders.SERIALIZED_HEADERS_ENCODING)) {
String encoding = metadata.getString(SolaceBinderHeaders.SERIALIZED_HEADERS_ENCODING);
encoder = Encoder.getByName(encoding);
if (encoder == null) {
String msg = String.format("%s encoding is not supported", encoding);
SolaceMessageConversionException exception = new SolaceMessageConversionException(msg);
log.warn(msg, exception);
throw exception;
}
}
Set serializedHeaders = stringSetReader.readValue(metadata.getString(SolaceBinderHeaders.SERIALIZED_HEADERS));
for (String headerName : serializedHeaders) {
if (metadata.containsKey(headerName)) {
byte[] serializedValue;
if (encoder != null) {
serializedValue = encoder.decode(metadata.getString(headerName));
} else {
serializedValue = metadata.getBytes(headerName);
}
Object value = SerializationUtils.deserialize(serializedValue);
if (value instanceof ByteArray) { // Just in case...
value = ((ByteArray) value).getBuffer();
}
headers.put(headerName, value);
}
}
}
metadata.keySet().stream().filter(h -> !exclusionList.contains(h)).filter(h -> !headers.containsKey(h)).filter(h -> !SolaceBinderHeaderMeta.META.containsKey(h)).filter(h -> !SolaceHeaderMeta.META.containsKey(h)).forEach(h -> {
Object value = null;
try {
value = metadata.get(h);
} catch (SDTException e) {
throw new RuntimeException(e);
}
if (value instanceof ByteArray) {
value = ((ByteArray) value).getBuffer();
}
headers.put(h, value);
});
if (!exclusionList.contains(SolaceBinderHeaders.MESSAGE_VERSION) && metadata.containsKey(SolaceBinderHeaders.MESSAGE_VERSION)) {
int messageVersion = metadata.getInteger(SolaceBinderHeaders.MESSAGE_VERSION);
headers.put(SolaceBinderHeaders.MESSAGE_VERSION, messageVersion);
}
return new MessageHeaders(headers);
}
/**
* Wrapper function which converts Serializable objects to byte[] if they aren't naturally supported by the SDTMap
*/
@SneakyThrows
private void addSDTMapObject(SDTMap sdtMap, Set serializedHeaders, String key, Object object, boolean convertNonSerializableHeadersToString) {
try {
sdtMap.putObject(key, object);
} catch (IllegalArgumentException | SDTException e) {
if (object instanceof Serializable) {
String var1 = DEFAULT_ENCODING.encode(SerializationUtils.serialize(object));
sdtMap.putString(key, var1);
serializedHeaders.add(key);
} else if (convertNonSerializableHeadersToString && object != null) {
if (log.isDebugEnabled()) {
log.debug(String.format("Irreversibly converting header %s to String", key));
}
sdtMap.putString(key, object.toString());
} else {
throw e;
}
}
}
public void resetIgnoredProperties(String flowReceiverId) {
if (ignoredHeaderProperties.isEmpty()) {
return;
}
if (log.isDebugEnabled()) {
log.debug(String.format("Clearing ignored properties %s on flow receiver container %s", ignoredHeaderProperties, flowReceiverId));
}
ignoredHeaderProperties.clear();
}
enum Encoder {
BASE64("base64", Base64.getEncoder()::encodeToString, Base64.getDecoder()::decode);
@Getter
private final String name;
private final Function encodeFnc;
private final Function decodeFnc;
private static final Map nameMap = new HashMap<>();
static {
Arrays.stream(Encoder.values()).forEach(e -> nameMap.put(e.getName(), e));
}
Encoder(String name, Function encodeFnc, Function decodeFnc) {
this.name = name;
this.encodeFnc = encodeFnc;
this.decodeFnc = decodeFnc;
}
public String encode(byte[] input) {
return input != null ? encodeFnc.apply(input) : null;
}
public byte[] decode(String input) {
return input != null ? decodeFnc.apply(input) : null;
}
public static Encoder getByName(String name) {
return nameMap.get(name);
}
}
}