GH-51: Add headers mapping to channel adapters

Fixes spring-projects/spring-integration-aws#51
This commit is contained in:
Artem Bilan
2018-04-06 16:51:47 -04:00
parent 2b57f34801
commit 6a999b2873
14 changed files with 464 additions and 29 deletions

View File

@@ -289,6 +289,9 @@ An XML variant may look like:
queue="foo"/>
````
Starting with _version 2.0_, the `SqsMessageHandler` can be configured with the `HeaderMapper` to map message headers to the SQS message attributes.
See `SqsHeaderMapper` implementation for more information and also consult with [Amazon SQS Message Attributes][] about value types and restrictions.
### Inbound Channel Adapter
The SQS Inbound Channel Adapter is a `message-driven` implementation for the `MessageProducer` and is represented with
@@ -466,6 +469,9 @@ The XML variant may look like:
body-expression="payload.toUpperCase()"/>
````
Starting with _version 2.0_, the `SnsMessageHandler` can be configured with the `HeaderMapper` to map message headers to the SNS message attributes.
See `SnsHeaderMapper` implementation for more information and also consult with [[Amazon SNS Message Attributes]][] about value types and restrictions.
## Metadata Store for Amazon DynamoDB
The `DynamoDbMetaDataStore`, a `ConcurrentMetadataStore` implementation, is provided to keep the metadata for Spring Integration components in the distributed Amazon DynamoDB store.
@@ -530,6 +536,9 @@ When `CheckpointMode.manual` is used the `Checkpointer` instance is populated to
The consumer group is included to the metadata store `key`.
When records are consumed, they are filtered by the last stored `lastCheckpoint` under the key as `[CONSUMER_GROUP]:[STREAM]:[SHARD_ID]`.
Starting with _version 2.0_, the `KinesisMessageDrivenChannelAdapter` can be configured with the `InboundMessageMapper` to extract message headers embedded into the record data (if any).
See `EmbeddedJsonHeadersMessageMapper` implementation for more information.
### Outbound Channel Adapter
The `KinesisMessageHandler` is an `AbstractMessageHandler` to perform put record to the Kinesis stream.
@@ -570,6 +579,9 @@ public MessageHandler kinesisMessageHandler(AmazonKinesis amazonKinesis,
}
````
Starting with _version 2.0_, the `KinesisMessageHandler` can be configured with the `OutboundMessageMapper` to embed message headers into the record data alongside with the payload.
See `EmbeddedJsonHeadersMessageMapper` implementation for more information.
For testing application with the Kinesis Channel Adapters you can use [Kinesalite][] NPM module.
What you need in your application is to configure Kinesis client properly:
@@ -605,3 +617,5 @@ Also you can use for you testing purpose a copy of `org.springframework.integrat
[Dynalite]: https://github.com/mhart/dynalite
[Kinesis Client Library]: https://github.com/awslabs/amazon-kinesis-client
[Kinesalite]: https://github.com/mhart/kinesalite
[Amazon SQS Message Attributes]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-attributes.html
[Amazon SNS Message Attributes]: https://docs.aws.amazon.com/sns/latest/dg/SNSMessageAttributes.html

View File

@@ -3,7 +3,7 @@ buildscript {
maven { url 'http://repo.spring.io/plugins-release' }
}
dependencies {
classpath 'io.spring.gradle:dependency-management-plugin:1.0.4.RELEASE'
classpath 'io.spring.gradle:dependency-management-plugin:1.0.5.RELEASE'
classpath 'io.spring.gradle:spring-io-plugin:0.0.8.RELEASE'
}
}
@@ -33,9 +33,9 @@ repositories {
ext {
assertjVersion = '3.9.1'
servletApiVersion = '3.1.0'
log4jVersion = '2.10.0'
log4jVersion = '2.11.0'
springCloudAwsVersion = '2.0.0.M4'
springIntegrationVersion = '5.0.3.RELEASE'
springIntegrationVersion = '5.0.4.RELEASE'
idPrefix = 'aws'

View File

@@ -44,6 +44,7 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.mapping.InboundMessageMapper;
import org.springframework.integration.metadata.ConcurrentMetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
@@ -136,6 +137,8 @@ public class KinesisMessageDrivenChannelAdapter extends MessageProducerSupport i
private boolean resetCheckpoints;
private InboundMessageMapper<byte[]> embeddedHeadersMapper;
private volatile boolean active;
private volatile int consumerInvokerMaxCapacity;
@@ -258,6 +261,15 @@ public class KinesisMessageDrivenChannelAdapter extends MessageProducerSupport i
this.idleBetweenPolls = Math.max(250, idleBetweenPolls);
}
/**
* Specify an {@link InboundMessageMapper} to extract message headers embedded into the record data.
* @param embeddedHeadersMapper the {@link InboundMessageMapper} to use.
* @since 2.0
*/
public void setEmbeddedHeadersMapper(InboundMessageMapper<byte[]> embeddedHeadersMapper) {
this.embeddedHeadersMapper = embeddedHeadersMapper;
}
@Override
protected void onInit() {
super.onInit();
@@ -931,9 +943,26 @@ public class KinesisMessageDrivenChannelAdapter extends MessageProducerSupport i
for (Record record : records) {
Object payload = record.getData().array();
if (KinesisMessageDrivenChannelAdapter.this.converter != null) {
Message<?> messageToUse = null;
if (KinesisMessageDrivenChannelAdapter.this.embeddedHeadersMapper != null) {
try {
messageToUse =
KinesisMessageDrivenChannelAdapter.this.embeddedHeadersMapper
.toMessage((byte[]) payload);
payload = messageToUse.getPayload();
}
catch (Exception e) {
logger.warn("Could not parse embedded headers. Remain payload untouched.", e);
}
}
if (payload instanceof byte[] &&
KinesisMessageDrivenChannelAdapter.this.converter != null) {
payload = KinesisMessageDrivenChannelAdapter.this.converter.convert((byte[]) payload);
}
AbstractIntegrationMessageBuilder<Object> messageBuilder = getMessageBuilderFactory()
.withPayload(payload)
.setHeader(AwsHeaders.RECEIVED_STREAM, this.shardOffset.getStream())
@@ -944,6 +973,10 @@ public class KinesisMessageDrivenChannelAdapter extends MessageProducerSupport i
messageBuilder.setHeader(AwsHeaders.CHECKPOINTER, this.checkpointer);
}
if (messageToUse != null) {
messageBuilder.copyHeadersIfAbsent(messageToUse.getHeaders());
}
performSend(messageBuilder, record);
if (CheckpointMode.record.equals(KinesisMessageDrivenChannelAdapter.this.checkpointMode)) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import org.springframework.integration.aws.support.AwsRequestFailureException;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.handler.AbstractMessageProducingHandler;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.DefaultErrorMessageStrategy;
import org.springframework.integration.support.ErrorMessageStrategy;
@@ -44,11 +45,13 @@ import com.amazonaws.handlers.AsyncHandler;
* Utilizes common logic ({@link AsyncHandler}, {@link ErrorMessageStrategy},
* {@code failureChannel} etc.) and message pre- and post-processing,
*
* @param <H> the headers container type.
*
* @author Artem Bilan
*
* @since 2.0
*/
public abstract class AbstractAwsMessageHandler extends AbstractMessageProducingHandler {
public abstract class AbstractAwsMessageHandler<H> extends AbstractMessageProducingHandler {
protected static final long DEFAULT_SEND_TIMEOUT = 10000;
@@ -66,6 +69,8 @@ public abstract class AbstractAwsMessageHandler extends AbstractMessageProducing
private String failureChannelName;
private HeaderMapper<H> headerMapper;
public void setAsyncHandler(AsyncHandler<? extends AmazonWebServiceRequest, ?> asyncHandler) {
this.asyncHandler = asyncHandler;
}
@@ -141,6 +146,22 @@ public abstract class AbstractAwsMessageHandler extends AbstractMessageProducing
return this.errorMessageStrategy;
}
/**
* Specify a {@link HeaderMapper} to map outbound headers.
* @param headerMapper the {@link HeaderMapper} to map outbound headers.
*/
public void setHeaderMapper(HeaderMapper<H> headerMapper) {
doSetHeaderMapper(headerMapper);
}
protected final void doSetHeaderMapper(HeaderMapper<H> headerMapper) {
this.headerMapper = headerMapper;
}
protected HeaderMapper<H> getHeaderMapper() {
return this.headerMapper;
}
protected EvaluationContext getEvaluationContext() {
return this.evaluationContext;
}
@@ -213,7 +234,7 @@ public abstract class AbstractAwsMessageHandler extends AbstractMessageProducing
};
}
protected abstract Future<?> handleMessageToAws(Message<?> message);
protected abstract Future<?> handleMessageToAws(Message<?> message) throws Exception;
protected abstract void additionalOnSuccessHeaders(AbstractIntegrationMessageBuilder<?> messageBuilder,
AmazonWebServiceRequest request, Object result);

View File

@@ -25,8 +25,12 @@ import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.mapping.OutboundMessageMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.MutableMessage;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -50,19 +54,21 @@ import com.amazonaws.services.kinesis.model.PutRecordsResult;
* @see AmazonKinesisAsync#putRecords(PutRecordsRequest)
* @see com.amazonaws.handlers.AsyncHandler
*/
public class KinesisMessageHandler extends AbstractAwsMessageHandler {
public class KinesisMessageHandler extends AbstractAwsMessageHandler<Void> {
private final AmazonKinesisAsync amazonKinesis;
private Converter<Object, byte[]> converter = new SerializingConverter();
private volatile Expression streamExpression;
private Expression streamExpression;
private volatile Expression partitionKeyExpression;
private Expression partitionKeyExpression;
private volatile Expression explicitHashKeyExpression;
private Expression explicitHashKeyExpression;
private volatile Expression sequenceNumberExpression;
private Expression sequenceNumberExpression;
private OutboundMessageMapper<byte[]> embeddedHeadersMapper;
public KinesisMessageHandler(AmazonKinesisAsync amazonKinesis) {
Assert.notNull(amazonKinesis, "'amazonKinesis' must not be null.");
@@ -123,11 +129,33 @@ public class KinesisMessageHandler extends AbstractAwsMessageHandler {
this.sequenceNumberExpression = sequenceNumberExpression;
}
/**
* Specify a {@link OutboundMessageMapper} for embedding message headers into the record data
* together with payload.
* @param embeddedHeadersMapper the {@link OutboundMessageMapper} to embed headers into the record data.
* @since 2.0
* @see org.springframework.integration.support.json.EmbeddedJsonHeadersMessageMapper
*/
public void setEmbeddedHeadersMapper(OutboundMessageMapper<byte[]> embeddedHeadersMapper) {
this.embeddedHeadersMapper = embeddedHeadersMapper;
}
/**
* Unsupported operation. Use {@link #setEmbeddedHeadersMapper} instead.
* @param headerMapper is not used.
* @see #setEmbeddedHeadersMapper
*/
@Override
protected Future<?> handleMessageToAws(Message<?> message) {
public void setHeaderMapper(HeaderMapper<Void> headerMapper) {
throw new UnsupportedOperationException("Kinesis doesn't support headers.\n" +
"Consider to use 'OutboundMessageMapper<byte[]>' for embedding headers into the record data.");
}
@Override
protected Future<?> handleMessageToAws(Message<?> message) throws Exception {
if (message.getPayload() instanceof PutRecordsRequest) {
AsyncHandler<PutRecordsRequest, PutRecordsResult> asyncHandler =
obtainAsyncHandler(message, (PutRecordsRequest) message.getPayload());
obtainAsyncHandler(message, (PutRecordsRequest) message.getPayload());
return this.amazonKinesis.putRecordsAsync((PutRecordsRequest) message.getPayload(), asyncHandler);
}
@@ -144,8 +172,9 @@ public class KinesisMessageHandler extends AbstractAwsMessageHandler {
}
}
private PutRecordRequest buildPutRecordRequest(Message<?> message) {
String stream = message.getHeaders().get(AwsHeaders.STREAM, String.class);
private PutRecordRequest buildPutRecordRequest(Message<?> message) throws Exception {
MessageHeaders messageHeaders = message.getHeaders();
String stream = messageHeaders.get(AwsHeaders.STREAM, String.class);
if (!StringUtils.hasText(stream) && this.streamExpression != null) {
stream = this.streamExpression.getValue(getEvaluationContext(), message, String.class);
}
@@ -153,7 +182,7 @@ public class KinesisMessageHandler extends AbstractAwsMessageHandler {
"Consider configuring this handler with a 'stream'( or 'streamExpression') or supply an " +
"'aws_stream' message header.");
String partitionKey = message.getHeaders().get(AwsHeaders.PARTITION_KEY, String.class);
String partitionKey = messageHeaders.get(AwsHeaders.PARTITION_KEY, String.class);
if (!StringUtils.hasText(partitionKey) && this.partitionKeyExpression != null) {
partitionKey = this.partitionKeyExpression.getValue(getEvaluationContext(), message, String.class);
}
@@ -166,25 +195,38 @@ public class KinesisMessageHandler extends AbstractAwsMessageHandler {
? this.explicitHashKeyExpression.getValue(getEvaluationContext(), message, String.class)
: null);
String sequenceNumber = message.getHeaders().get(AwsHeaders.SEQUENCE_NUMBER, String.class);
String sequenceNumber = messageHeaders.get(AwsHeaders.SEQUENCE_NUMBER, String.class);
if (!StringUtils.hasText(sequenceNumber) && this.sequenceNumberExpression != null) {
sequenceNumber = this.sequenceNumberExpression.getValue(getEvaluationContext(), message, String.class);
}
Object payload = message.getPayload();
ByteBuffer data;
ByteBuffer data = null;
Message<?> messageToEmbed = null;
if (payload instanceof ByteBuffer) {
data = (ByteBuffer) payload;
if (this.embeddedHeadersMapper != null) {
messageToEmbed = new MutableMessage<>(data.array(), messageHeaders);
}
}
else {
byte[] bytes =
payload instanceof byte[]
? (byte[]) payload
: this.converter.convert(payload);
if (this.embeddedHeadersMapper != null) {
messageToEmbed = new MutableMessage<>(bytes, messageHeaders);
}
else {
data = ByteBuffer.wrap(bytes);
}
}
data = ByteBuffer.wrap(bytes);
if (messageToEmbed != null) {
data = ByteBuffer.wrap(this.embeddedHeadersMapper.fromMessage(messageToEmbed));
}
return new PutRecordRequest()

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.integration.aws.outbound;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import org.springframework.cloud.aws.core.env.ResourceIdResolver;
@@ -25,6 +27,8 @@ import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.aws.support.SnsBodyBuilder;
import org.springframework.integration.aws.support.SnsHeaderMapper;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
@@ -32,6 +36,7 @@ import org.springframework.util.Assert;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.sns.AmazonSNSAsync;
import com.amazonaws.services.sns.model.MessageAttributeValue;
import com.amazonaws.services.sns.model.PublishRequest;
import com.amazonaws.services.sns.model.PublishResult;
@@ -78,7 +83,7 @@ import com.amazonaws.services.sns.model.PublishResult;
* @see PublishRequest
* @see SnsBodyBuilder
*/
public class SnsMessageHandler extends AbstractAwsMessageHandler {
public class SnsMessageHandler extends AbstractAwsMessageHandler<Map<String, MessageAttributeValue>> {
private final AmazonSNSAsync amazonSns;
@@ -93,6 +98,7 @@ public class SnsMessageHandler extends AbstractAwsMessageHandler {
public SnsMessageHandler(AmazonSNSAsync amazonSns) {
Assert.notNull(amazonSns, "amazonSns must not be null.");
this.amazonSns = amazonSns;
doSetHeaderMapper(new SnsHeaderMapper());
}
public void setTopicArn(String topicArn) {
@@ -185,6 +191,11 @@ public class SnsMessageHandler extends AbstractAwsMessageHandler {
else {
publishRequest.setMessage(getConversionService().convert(snsMessage, String.class));
}
HeaderMapper<Map<String, MessageAttributeValue>> headerMapper = getHeaderMapper();
if (headerMapper != null) {
mapHeaders(message, publishRequest, headerMapper);
}
}
AsyncHandler<PublishRequest, PublishResult> asyncHandler = obtainAsyncHandler(message, publishRequest);
@@ -192,6 +203,16 @@ public class SnsMessageHandler extends AbstractAwsMessageHandler {
}
private void mapHeaders(Message<?> message, PublishRequest publishRequest,
HeaderMapper<Map<String, MessageAttributeValue>> headerMapper) {
HashMap<String, MessageAttributeValue> messageAttributes = new HashMap<>();
headerMapper.fromHeaders(message.getHeaders(), messageAttributes);
if (!messageAttributes.isEmpty()) {
publishRequest.setMessageAttributes(messageAttributes);
}
}
@Override
protected void additionalOnSuccessHeaders(AbstractIntegrationMessageBuilder<?> messageBuilder,
AmazonWebServiceRequest request, Object result) {

View File

@@ -16,6 +16,8 @@
package org.springframework.integration.aws.outbound;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import org.springframework.cloud.aws.core.env.ResourceIdResolver;
@@ -23,8 +25,10 @@ import org.springframework.cloud.aws.messaging.support.destination.DynamicQueueU
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.aws.support.SqsHeaderMapper;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.GenericMessageConverter;
@@ -36,6 +40,7 @@ import org.springframework.util.StringUtils;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.sqs.AmazonSQSAsync;
import com.amazonaws.services.sqs.model.MessageAttributeValue;
import com.amazonaws.services.sqs.model.SendMessageBatchRequest;
import com.amazonaws.services.sqs.model.SendMessageBatchResult;
import com.amazonaws.services.sqs.model.SendMessageRequest;
@@ -52,7 +57,7 @@ import com.amazonaws.services.sqs.model.SendMessageResult;
* @see com.amazonaws.handlers.AsyncHandler
*/
public class SqsMessageHandler extends AbstractAwsMessageHandler {
public class SqsMessageHandler extends AbstractAwsMessageHandler<Map<String, MessageAttributeValue>> {
private final AmazonSQSAsync amazonSqs;
@@ -76,6 +81,7 @@ public class SqsMessageHandler extends AbstractAwsMessageHandler {
public SqsMessageHandler(AmazonSQSAsync amazonSqs, ResourceIdResolver resourceIdResolver) {
this.amazonSqs = amazonSqs;
this.destinationResolver = new DynamicQueueUrlDestinationResolver(amazonSqs, resourceIdResolver);
doSetHeaderMapper(new SqsHeaderMapper());
}
public void setQueue(String queue) {
@@ -187,12 +193,27 @@ public class SqsMessageHandler extends AbstractAwsMessageHandler {
this.messageDeduplicationIdExpression.getValue(getEvaluationContext(), message, String.class);
sendMessageRequest.setMessageDeduplicationId(messageDeduplicationId);
}
HeaderMapper<Map<String, MessageAttributeValue>> headerMapper = getHeaderMapper();
if (headerMapper != null) {
mapHeaders(message, sendMessageRequest, headerMapper);
}
}
AsyncHandler<SendMessageRequest, SendMessageResult> asyncHandler =
obtainAsyncHandler(message, sendMessageRequest);
return this.amazonSqs.sendMessageAsync(sendMessageRequest, asyncHandler);
}
private void mapHeaders(Message<?> message, SendMessageRequest sendMessageRequest,
HeaderMapper<Map<String, MessageAttributeValue>> headerMapper) {
HashMap<String, MessageAttributeValue> messageAttributes = new HashMap<>();
headerMapper.fromHeaders(message.getHeaders(), messageAttributes);
if (!messageAttributes.isEmpty()) {
sendMessageRequest.setMessageAttributes(messageAttributes);
}
}
@Override
protected void additionalOnSuccessHeaders(AbstractIntegrationMessageBuilder<?> messageBuilder,
AmazonWebServiceRequest request, Object result) {

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed 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.springframework.integration.aws.support;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.aws.messaging.core.MessageAttributeDataTypes;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.util.PatternMatchUtils;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.NumberUtils;
/**
* Base {@link HeaderMapper} implementation for common logic in SQS and SNS
* around message attributes mapping.
*
* The {@link #toHeaders(Map)} is not supported.
*
* @param <A> the target message attribute type.
*
* @author Artem Bilan
*
* @since 2.0
*/
public abstract class AbstractMessageAttributesHeaderMapper<A> implements HeaderMapper<Map<String, A>> {
private static final Log logger = LogFactory.getLog(SqsHeaderMapper.class);
private volatile String[] outboundHeaderNames = {
"!" + MessageHeaders.ID,
"!" + MessageHeaders.TIMESTAMP,
"!" + AwsHeaders.MESSAGE_ID,
"!" + AwsHeaders.QUEUE,
"!" + AwsHeaders.TOPIC,
"*" };
/**
* Provide the header names that should be mapped to a AWS request object attributes
* (for outbound adapters) from a Spring Integration Message's headers.
* The values can also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched.
* Also supports negated ('!') patterns. First match wins (positive or negative).
* To match the names starting with {@code !} symbol,
* you have to escape it prepending with the {@code \} symbol in the pattern definition.
* Defaults to map all ({@code *}) if the type is supported by SQS.
* The {@link MessageHeaders#ID}, {@link MessageHeaders#TIMESTAMP}, {@link AwsHeaders#MESSAGE_ID},
* {@link AwsHeaders#QUEUE} and {@link AwsHeaders#TOPIC} are ignored by default.
* @param outboundHeaderNames The inbound header names.
*/
public void setOutboundHeaderNames(String... outboundHeaderNames) {
Assert.notNull(outboundHeaderNames, "'outboundHeaderNames' must not be null.");
Assert.noNullElements(outboundHeaderNames, "'outboundHeaderNames' must not contains null elements.");
Arrays.sort(outboundHeaderNames);
this.outboundHeaderNames = outboundHeaderNames;
}
@Override
public void fromHeaders(MessageHeaders headers, Map<String, A> target) {
for (Map.Entry<String, Object> messageHeader : headers.entrySet()) {
String messageHeaderName = messageHeader.getKey();
Object messageHeaderValue = messageHeader.getValue();
if (Boolean.TRUE.equals(PatternMatchUtils.smartMatch(messageHeaderName, this.outboundHeaderNames))) {
if (messageHeaderValue instanceof UUID
|| messageHeaderValue instanceof MimeType
|| messageHeaderValue instanceof Boolean
|| messageHeaderValue instanceof String) {
target.put(messageHeaderName, getStringMessageAttribute(messageHeaderValue.toString()));
}
else if (messageHeaderValue instanceof Number) {
target.put(messageHeaderName, getNumberMessageAttribute(messageHeaderValue));
}
else if (messageHeaderValue instanceof ByteBuffer) {
target.put(messageHeaderName, getBinaryMessageAttribute((ByteBuffer) messageHeaderValue));
}
else if (messageHeaderValue instanceof byte[]) {
target.put(messageHeaderName,
getBinaryMessageAttribute(ByteBuffer.wrap((byte[]) messageHeaderValue)));
}
else {
if (logger.isWarnEnabled()) {
logger.warn(
String.format("Message header with name '%s' and type '%s' cannot be sent as" +
" message attribute because it is not supported by SQS.",
messageHeaderName,
messageHeaderValue.getClass().getName()));
}
}
}
}
}
private A getBinaryMessageAttribute(ByteBuffer messageHeaderValue) {
return buildMessageAttribute(MessageAttributeDataTypes.BINARY, messageHeaderValue);
}
private A getStringMessageAttribute(String messageHeaderValue) {
return buildMessageAttribute(MessageAttributeDataTypes.STRING, messageHeaderValue);
}
private A getNumberMessageAttribute(Object messageHeaderValue) {
Assert.isTrue(NumberUtils.STANDARD_NUMBER_TYPES.contains(messageHeaderValue.getClass()),
"Only standard number types are accepted as message header.");
return buildMessageAttribute(MessageAttributeDataTypes.NUMBER + "." + messageHeaderValue.getClass().getName(),
messageHeaderValue);
}
protected abstract A buildMessageAttribute(String dataType, Object value);
@Override
public Map<String, Object> toHeaders(Map<String, A> source) {
throw new UnsupportedOperationException("The mapping from AWS Response Message is not supported");
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed 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.springframework.integration.aws.support;
import java.nio.ByteBuffer;
import com.amazonaws.services.sns.model.MessageAttributeValue;
/**
* The {@link AbstractMessageAttributesHeaderMapper} implementation for the mapping
* from headers to SNS message attributes.
* <p>
* On the Inbound side, the SNS message is fully mapped from the JSON to the message payload.
* Only important HTTP headers are mapped to the message headers.
*
* @author Artem Bilan
*
* @since 2.0
*/
public class SnsHeaderMapper extends AbstractMessageAttributesHeaderMapper<MessageAttributeValue> {
@Override
protected MessageAttributeValue buildMessageAttribute(String dataType, Object value) {
MessageAttributeValue messageAttributeValue = new MessageAttributeValue()
.withDataType(dataType);
if (value instanceof ByteBuffer) {
return messageAttributeValue.withBinaryValue((ByteBuffer) value);
}
else {
return messageAttributeValue.withStringValue(value.toString());
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed 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.springframework.integration.aws.support;
import java.nio.ByteBuffer;
import org.springframework.messaging.MessageHeaders;
import com.amazonaws.services.sqs.model.MessageAttributeValue;
/**
* The {@link AbstractMessageAttributesHeaderMapper} implementation for the mapping
* from headers to SQS message attributes.
* <p>
* The {@link org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer} maps
* all the SQS message attributes to the {@link MessageHeaders}.
*
* @author Artem Bilan
*
* @since 2.0
*/
public class SqsHeaderMapper extends AbstractMessageAttributesHeaderMapper<MessageAttributeValue> {
@Override
protected MessageAttributeValue buildMessageAttribute(String dataType, Object value) {
MessageAttributeValue messageAttributeValue = new MessageAttributeValue()
.withDataType(dataType);
if (value instanceof ByteBuffer) {
return messageAttributeValue.withBinaryValue((ByteBuffer) value);
}
else {
return messageAttributeValue.withStringValue(value.toString());
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.aws.kinesis;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import java.util.Date;
import java.util.HashSet;
@@ -42,6 +43,7 @@ import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.metadata.ConcurrentMetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.json.EmbeddedJsonHeadersMessageMapper;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@@ -95,11 +97,13 @@ public class KinesisIntegrationTests {
this.kinesisSendChannel.send(
MessageBuilder.withPayload(now)
.setHeader(AwsHeaders.STREAM, TEST_STREAM)
.setHeader("foo", "BAR")
.build());
Message<?> receive = this.kinesisReceiveChannel.receive(10_000);
assertThat(receive).isNotNull();
assertThat(receive.getPayload()).isEqualTo(now);
assertThat(receive.getHeaders()).contains(entry("foo", "BAR"));
Message<?> errorMessage = this.errorChannel.receive(10_000);
assertThat(errorMessage).isNotNull();
@@ -141,6 +145,7 @@ public class KinesisIntegrationTests {
public MessageHandler kinesisMessageHandler() {
KinesisMessageHandler kinesisMessageHandler = new KinesisMessageHandler(KINESIS_LOCAL_RUNNING.getKinesis());
kinesisMessageHandler.setPartitionKey("1");
kinesisMessageHandler.setEmbeddedHeadersMapper(new EmbeddedJsonHeadersMessageMapper("foo"));
return kinesisMessageHandler;
}
@@ -156,6 +161,7 @@ public class KinesisIntegrationTests {
adapter.setErrorChannel(errorChannel());
adapter.setErrorMessageStrategy(new KinesisMessageHeaderErrorMessageStrategy());
adapter.setCheckpointStore(checkpointStore());
adapter.setEmbeddedHeadersMapper(new EmbeddedJsonHeadersMessageMapper("foo"));
return adapter;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2017 the original author or authors.
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.integration.aws.outbound;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
@@ -38,6 +39,7 @@ import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.support.json.EmbeddedJsonHeadersMessageMapper;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@@ -77,7 +79,7 @@ public class KinesisMessageHandlerTests {
@Test
@SuppressWarnings("unchecked")
public void testKinesisMessageHandler() {
public void testKinesisMessageHandler() throws Exception {
Message<?> message = MessageBuilder.withPayload("message").build();
try {
this.kinesisSendChannel.send(message);
@@ -101,6 +103,7 @@ public class KinesisMessageHandlerTests {
message = MessageBuilder.fromMessage(message)
.setHeader(AwsHeaders.PARTITION_KEY, "fooKey")
.setHeader(AwsHeaders.SEQUENCE_NUMBER, "10")
.setHeader("foo", "bar")
.build();
this.kinesisSendChannel.send(message);
@@ -119,7 +122,12 @@ public class KinesisMessageHandlerTests {
assertThat(putRecordRequest.getPartitionKey()).isEqualTo("fooKey");
assertThat(putRecordRequest.getSequenceNumberForOrdering()).isEqualTo("10");
assertThat(putRecordRequest.getExplicitHashKey()).isNull();
assertThat(putRecordRequest.getData()).isEqualTo(ByteBuffer.wrap("message".getBytes()));
Message<?> messageToCheck = new EmbeddedJsonHeadersMessageMapper()
.toMessage(putRecordRequest.getData().array());
assertThat(messageToCheck.getHeaders()).contains(entry("foo", "bar"));
assertThat(messageToCheck.getPayload()).isEqualTo("message".getBytes());
AsyncHandler<?, ?> asyncHandler = asyncHandlerArgumentCaptor.getValue();
@@ -195,6 +203,7 @@ public class KinesisMessageHandlerTests {
}
});
kinesisMessageHandler.setEmbeddedHeadersMapper(new EmbeddedJsonHeadersMessageMapper("foo"));
return kinesisMessageHandler;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2017 the original author or authors.
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,8 @@ import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -33,12 +35,14 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.aws.support.AwsHeaders;
import org.springframework.integration.aws.support.SnsBodyBuilder;
import org.springframework.integration.aws.support.SnsHeaderMapper;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.PollableChannel;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.test.annotation.DirtiesContext;
@@ -47,6 +51,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.sns.AmazonSNSAsync;
import com.amazonaws.services.sns.model.MessageAttributeValue;
import com.amazonaws.services.sns.model.PublishRequest;
import com.amazonaws.services.sns.model.PublishResult;
@@ -78,6 +83,7 @@ public class SnsMessageHandlerTests {
Message<?> message = MessageBuilder.withPayload(payload)
.setHeader("topic", "topic")
.setHeader("subject", "subject")
.setHeader("foo", "bar")
.build();
this.sendToSnsChannel.send(message);
@@ -96,6 +102,13 @@ public class SnsMessageHandlerTests {
assertThat(publishRequest.getMessage())
.isEqualTo("{\"default\":\"foo\",\"sms\":\"{\\\"foo\\\" : \\\"bar\\\"}\"}");
Map<String, MessageAttributeValue> messageAttributes = publishRequest.getMessageAttributes();
assertThat(messageAttributes).doesNotContainKey(MessageHeaders.ID);
assertThat(messageAttributes).doesNotContainKey(MessageHeaders.TIMESTAMP);
assertThat(messageAttributes).containsKey("foo");
assertThat(messageAttributes.get("foo").getStringValue()).isEqualTo("bar");
assertThat(reply.getHeaders().get(AwsHeaders.MESSAGE_ID)).isEqualTo("111");
assertThat(reply.getHeaders().get(AwsHeaders.TOPIC)).isEqualTo("topic");
assertThat(reply.getPayload()).isSameAs(payload);
@@ -135,6 +148,9 @@ public class SnsMessageHandlerTests {
snsMessageHandler.setSubjectExpression(PARSER.parseExpression("headers.subject"));
snsMessageHandler.setBodyExpression(PARSER.parseExpression("payload"));
snsMessageHandler.setOutputChannel(resultChannel());
SnsHeaderMapper headerMapper = new SnsHeaderMapper();
headerMapper.setOutboundHeaderNames("foo");
snsMessageHandler.setHeaderMapper(headerMapper);
return snsMessageHandler;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -39,6 +41,7 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -47,6 +50,7 @@ import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.sqs.AmazonSQSAsync;
import com.amazonaws.services.sqs.model.GetQueueUrlRequest;
import com.amazonaws.services.sqs.model.GetQueueUrlResult;
import com.amazonaws.services.sqs.model.MessageAttributeValue;
import com.amazonaws.services.sqs.model.SendMessageRequest;
/**
@@ -105,8 +109,18 @@ public class SqsMessageHandlerTests {
verify(this.amazonSqs, times(3))
.sendMessageAsync(sendMessageRequestArgumentCaptor.capture(), any(AsyncHandler.class));
assertThat(sendMessageRequestArgumentCaptor.getValue().getQueueUrl())
SendMessageRequest sendMessageRequestArgumentCaptorValue = sendMessageRequestArgumentCaptor.getValue();
assertThat(sendMessageRequestArgumentCaptorValue.getQueueUrl())
.isEqualTo("http://queue-url.com/baz");
Map<String, MessageAttributeValue> messageAttributes =
sendMessageRequestArgumentCaptorValue.getMessageAttributes();
assertThat(messageAttributes).doesNotContainKey(MessageHeaders.ID);
assertThat(messageAttributes).doesNotContainKey(MessageHeaders.TIMESTAMP);
assertThat(messageAttributes).containsKey("foo");
assertThat(messageAttributes.get("foo").getStringValue()).isEqualTo("baz");
}
@Configuration