Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.awspring.cloud.sqs.support.converter.legacy.LegacyJackson2SqsMessagingMessageConverter;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
Expand All @@ -42,6 +43,10 @@
import software.amazon.awssdk.services.sqs.model.Message;
import software.amazon.awssdk.services.sqs.model.MessageAttributeValue;
import software.amazon.awssdk.services.sqs.model.MessageSystemAttributeName;
import tools.jackson.core.JacksonException;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;

/**
* A {@link HeaderMapper} implementation for SQS {@link Message}s. Enables creating additional SQS related headers from
Expand All @@ -59,6 +64,11 @@ public class SqsHeaderMapper implements ContextAwareHeaderMapper<Message> {

private static final Logger logger = LoggerFactory.getLogger(SqsHeaderMapper.class);

private static final TypeReference<Map<String, SnsNotification.MessageAttribute>> SNS_MESSAGE_ATTRIBUTES_TYPE = new TypeReference<>() {
};

private final JsonMapper jsonMapper = new JsonMapper();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonMapper should be the same one that is passed in autoconfiguration.


private BiFunction<Message, MessageHeaderAccessor, MessageHeaders> additionalHeadersFunction = ((message,
accessor) -> accessor.toMessageHeaders());

Expand Down Expand Up @@ -163,6 +173,7 @@ public MessageHeaders toHeaders(Message source) {
accessor.copyHeadersIfAbsent(getMessageAttributesAsHeaders(source));
accessor.copyHeadersIfAbsent(createDefaultHeaders(source));
accessor.copyHeadersIfAbsent(createAdditionalHeaders(source));
accessor.copyHeadersIfAbsent(getSnsMessageAttributesAsHeaders(source));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs on every SQS message, whether it's an SNS notification or not. SqsHeaderMapper is the default mapper for all SQS traffic, so this adds an SNS-envelope check (and a JSON parse on match) to the hot path for everyone, even deployments that never receive SNS messages.

Custom SnsAwareSqsHeaderMapper should be implemented and wired via existing hooks such as AbstractMessagingMessageConverter.setHeaderMapper and AbstractMessagingMessageConverter.configureHeaderMapper.

Container options can be used then to configure this custom mapper for Queues which will have messages sent via SnsTemplate. This makes it cheaper for performance.


MessageHeaders messageHeaders = accessor.toMessageHeaders();
logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId());
Expand Down Expand Up @@ -191,6 +202,36 @@ private Map<String, Object> getMessageAttributesAsHeaders(Message source) {
.collect(Collectors.toMap(Map.Entry::getKey, this::getValue));
}

private Map<String, Object> getSnsMessageAttributesAsHeaders(Message source) {
String body = source.body();
if (body == null || !body.contains("\"MessageAttributes\"")) {
return Map.of();
}
try {
JsonNode jsonNode = jsonMapper.readTree(body);
if (!isSnsNotification(jsonNode)) {
return Map.of();
}
JsonNode messageAttributes = jsonNode.get("MessageAttributes");
if (messageAttributes == null || !messageAttributes.isObject()) {
return Map.of();
}
Map<String, SnsNotification.MessageAttribute> attributes = jsonMapper.convertValue(messageAttributes,
SNS_MESSAGE_ATTRIBUTES_TYPE);
return attributes.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> getValue(entry.getValue())));
}
catch (JacksonException | IllegalArgumentException e) {
logger.trace("Could not map SNS message attributes for message " + source.messageId(), e);
return Map.of();
}
}

private boolean isSnsNotification(JsonNode jsonNode) {
JsonNode type = jsonNode.get("Type");
return type != null && "Notification".equals(type.asString()) && jsonNode.has("Message");
}

private Object getValue(Map.Entry<String, MessageAttributeValue> entry) {
MessageAttributeValue value = entry.getValue();
String dataType = value.dataType();
Expand All @@ -207,6 +248,19 @@ private Object getValue(Map.Entry<String, MessageAttributeValue> entry) {
};
}

private Object getValue(SnsNotification.MessageAttribute value) {
String dataType = value.getType();
Assert.notNull(dataType, "dataType must not be null");
String baseDataType = dataType.contains(".") ? dataType.substring(0, dataType.indexOf('.')) : dataType;

return switch (baseDataType) {
case MessageAttributeDataTypes.NUMBER -> getNumberValue(value.getValue(), dataType);
case MessageAttributeDataTypes.BINARY -> SdkBytes
.fromByteArray(Base64.getDecoder().decode(value.getValue()));
default -> value.getValue();
};
}

private Map<String, String> getMessageSystemAttributesAsHeaders(Message source) {
return source
.attributes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,69 @@ void shouldAddNumberMessageAttributes() {
assertThat(headers.get(headerName)).isEqualTo(headerValue);
}

@Test
void shouldAddSnsMessageAttributes() {
SqsHeaderMapper mapper = new SqsHeaderMapper();
Message message = Message.builder().body("""
{
"Type": "Notification",
"Message": "payload",
"MessageAttributes": {
"stringAttribute": { "Type": "String", "Value": "myString" },
"numberAttribute": { "Type": "Number.java.lang.Integer", "Value": "10" },
"binaryAttribute": { "Type": "Binary", "Value": "bXlCaW5hcnk=" }
}
}
""").messageId(UUID.randomUUID().toString()).build();

MessageHeaders headers = mapper.toHeaders(message);

assertThat(headers.get("stringAttribute")).isEqualTo("myString");
assertThat(headers.get("numberAttribute")).isEqualTo(10);
assertThat(headers.get("binaryAttribute")).isEqualTo(SdkBytes.fromUtf8String("myBinary"));
}

@Test
void shouldPreferSqsMessageAttributesOverSnsMessageAttributes() {
SqsHeaderMapper mapper = new SqsHeaderMapper();
Message message = Message.builder().body("""
{
"Type": "Notification",
"Message": "payload",
"MessageAttributes": {
"attribute": { "Type": "String", "Value": "snsValue" }
}
}
""")
.messageAttributes(
Map.of("attribute",
MessageAttributeValue.builder().dataType(MessageAttributeDataTypes.STRING)
.stringValue("sqsValue").build()))
.messageId(UUID.randomUUID().toString()).build();

MessageHeaders headers = mapper.toHeaders(message);

assertThat(headers.get("attribute")).isEqualTo("sqsValue");
}

@Test
void shouldIgnoreMessageAttributesInNonSnsPayload() {
SqsHeaderMapper mapper = new SqsHeaderMapper();
Message message = Message.builder().body("""
{
"Type": "ApplicationEvent",
"Message": "payload",
"MessageAttributes": {
"attribute": { "Type": "String", "Value": "value" }
}
}
""").messageId(UUID.randomUUID().toString()).build();

MessageHeaders headers = mapper.toHeaders(message);

assertThat(headers).doesNotContainKey("attribute");
}

@Test
void shouldCreateMessageWithSystemAttributesFromHeaders() {
MessageHeaders headers = new MessageHeaders(
Expand Down