From 16ef806598de5d6d971a98663dbc44cdff77ef56 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 13 Mar 2015 18:20:31 +0200 Subject: [PATCH] INTEXT-147: Add SnsInboundChannelAdapter JIRA: https://jira.spring.io/browse/INTEXT-147 INTEXT-147: Documentation Upgrade dependencies --- README.md | 181 +++++++++++++++- build.gradle | 34 ++- gradle/wrapper/gradle-wrapper.jar | Bin 52141 -> 52271 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- .../aws/config/xml/AWSNamespaceHandler.java | 10 +- .../aws/config/xml/AmazonWSParserUtils.java | 2 + .../xml/SnsInboundChannelAdapterParser.java | 81 +++++++ .../aws/inbound/SnsInboundChannelAdapter.java | 203 ++++++++++++++++++ .../aws/outbound/SqsMessageHandler.java | 5 +- .../integration/aws/support/AwsHeaders.java | 4 + .../config/xml/spring-integration-aws-1.0.xsd | 80 ++++++- ...boundChannelAdapterParserTests-context.xml | 23 ++ .../SnsInboundChannelAdapterParserTests.java | 80 +++++++ ...rivenChannelAdapterParserTests-context.xml | 2 +- .../SqsMessageHandlerParserTests-context.xml | 2 +- .../SnsInboundChannelAdapterTests.java | 181 ++++++++++++++++ .../aws/inbound/notificationMessage.json | 26 +++ .../aws/inbound/subscriptionConfirmation.json | 12 ++ .../aws/inbound/unsubscribeConfirmation.json | 12 ++ 19 files changed, 915 insertions(+), 27 deletions(-) create mode 100644 src/main/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParser.java create mode 100644 src/main/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapter.java create mode 100644 src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests-context.xml create mode 100644 src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests.java create mode 100644 src/test/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapterTests.java create mode 100644 src/test/java/org/springframework/integration/aws/inbound/notificationMessage.json create mode 100644 src/test/java/org/springframework/integration/aws/inbound/subscriptionConfirmation.json create mode 100644 src/test/java/org/springframework/integration/aws/inbound/unsubscribeConfirmation.json diff --git a/README.md b/README.md index 451b8e9..340ab47 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ This guide intends to explain briefly the various adapters available for [Amazon * **Amazon Simple Email Service (SES)** * **Amazon Simple Storage Service (S3)** -* **Amazon Simple Queue Service (SQS)** (Development complete, coming soon) +* **Amazon Simple Queue Service (SQS)** +* **Amazon Simple Notification Service (SNS)** * **Amazon DynamoDB** (Analysis ongoing) * **Amazon SimpleDB** (Not initiated) -* **Amazon SNS** (Not initiated) Sample XML Namespace configurations for each adapter as well as sample code snippets are provided wherever necessary. Of the above libraries, *SES* and *SNS* provide outbound adapters only. All other services have inbound @@ -71,6 +71,183 @@ There is no adapter for SES, since [Spring Cloud AWS][] provides implementations `org.springframework.mail.MailSender` - `SimpleEmailServiceMailSender` and `SimpleEmailServiceJavaMailSender`, which can be injected to the ``. +##Amazon Simple Queue Service (SQS) + +The `SQS` adapters are fully based on the [Spring Cloud AWS][] foundation, so for more information about the +background components and core configuration, please, refer to the documentation of that project. + +###Outbound Channel Adapter + +The SQS Outbound Channel Adapter is presented by the `SqsMessageHandler` implementation +(``) and allows to send message to the SQS `queue` with provided `AmazonSQS` +client. An SQS queue can be configured explicitly on the adapter (using +`org.springframework.integration.expression.ValueExpression`) or as a SpEL `Expression`, which is evaluated against +request message as a root object of evaluation context. In addition the `queue` can be extracted from the message +headers under `AwsHeaders.QUEUE`. + +The Java Configuration is pretty simple: + +````java +@SpringBootApplication +public static class MyConfiguration { + + @Autowired + private AmazonSQS amazonSqs; + + @Bean + public QueueMessagingTemplate queueMessagingTemplate() { + return new QueueMessagingTemplate(this.amazonSqs); + } + + @Bean + @ServiceActivator(inputChannel = "sqsSendChannel") + public MessageHandler sqsMessageHandler() { + return new SqsMessageHandler(queueMessagingTemplate()); + } + +} +```` + +An XML variant may look like: + +````xml + + + +```` + +###Inbound Channel Adapter + +The SQS Inbound Channel Adapter is a `message-driven` implementation for the `MessageProducer` and is represented with +`SqsMessageDrivenChannelAdapter`. This channel adapter is based on the +`org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer` to receive messages from the +provided `queues` in async manner and send an enhanced Spring Integration Message to the provided `MessageChannel`. +The enhancements includes `AwsHeaders.MESSAGE_ID`, `AwsHeaders.RECEIPT_HANDLE` and `AwsHeaders.QUEUE` message headers. + +The Java Configuration is pretty simple: + +````java +@SpringBootApplication +public static class MyConfiguration { + + @Autowired + private AmazonSQSAsync amazonSqs; + + @Bean + public PollableChannel inputChannel() { + return new QueueChannel(); + } + + @Bean + public MessageProducer sqsMessageDrivenChannelAdapter() { + SqsMessageDrivenChannelAdapter adapter = new SqsMessageDrivenChannelAdapter(this.amazonSqs, "myQueue"); + adapter.setOutputChannel(inputChannel()); + return adapter; + } +} +```` + +An XML variant may look like: + +````xml + + + +```` + +The `SqsMessageDrivenChannelAdapter` exposes all `SimpleMessageListenerContainer` attributes to configure and one an +important of them is `deleteMessageOnException`, which is `true` by default. Having that to `false`, it is a +responsibility of end-application to delete message or not on exceptions. E.g. in the error flow on the +`error-channel` of this channel adapter. For this purpose a `AwsHeaders.RECEIPT_HANDLE` message header must be used +for the message deletion: + +````java +MessageHeaders headers = message.getHeaders(); +this.amazonSqs.deleteMessageAsync( + new DeleteMessageRequest(headers.get(AwsHeaders.QUEUE), headers.get(AwsHeaders.RECEIPT_HANDLE))); +```` + +##Amazon Simple Notification Service (SNS) + +Amazon SNS is a publish-subscribe messaging system that allows clients to publish notification to a particular topic. +Other interested clients may subscribe using different protocols like HTTP/HTTPS, e-mail or an Amazon SQS queue to +receive the messages. Plus mobile devices can be registered as subscribers from the AWS Management Console. + +Unfortunately [Spring Cloud AWS][] doesn't provide flexible components which can be used from the channel adapter +implementations, but Amazon SNS API is pretty simple, from other side, hence Spring Integration AWS SNS Support is +straightforward and just allows to provide channel adapter foundation for Spring Integration applications. + +Since e-mail, SMS and mobile devices subscription/unsubscription confirmation is out of the Spring Integration +application scope and can be done only from the AWS Management Console, we provide only HTTP/HTTPS SNS endpoint in +face of `SnsInboundChannelAdapter`. The SQS-to-SNS subscription can be done with the simple usage of +`com.amazonaws.services.sns.util.Topics#subscribeQueue()`, which confirms subscription automatically. + +###Inbound Channel Adapter + +The `SnsInboundChannelAdapter` (``) is an extension of +`HttpRequestHandlingMessagingGateway` and must be as a part of Spring MVC application. Its URL must be used from the +AWS Management Console to add this endpoint as a subscriber to the SNS Topic. However before receiving any +notification itself this HTTP endpoint must confirm the subscription. + +See `SnsInboundChannelAdapter` JavaDocs for more information. + +An important option of this adapter to consider is `handleNotificationStatus`. This `boolean` flag indicates if the +adapter should send `SubscriptionConfirmation/UnsubscribeConfirmation` message to the `output-channel` or not. If +that the `AwsHeaders.NOTIFICATION_STATUS` message header is present in the message with the `NotificationStatus` +object, which can be used in the downstream flow to confirm subscription or not. Or "re-confirm" it in case of +`UnsubscribeConfirmation` message. + +In addition the `AwsHeaders#SNS_MESSAGE_TYPE` message header is represent to simplify a routing in the downstream flow. + +The Java Configuration is pretty simple: + +````java +@SpringBootApplication +public static class MyConfiguration { + + @Autowired + private AmazonSNS amazonSns; + + @Bean + public PollableChannel inputChannel() { + return new QueueChannel(); + } + + @Bean + public HttpRequestHandler sqsMessageDrivenChannelAdapter() { + SnsInboundChannelAdapter adapter = new SnsInboundChannelAdapter(amazonSns(), "/mySampleTopic"); + adapter.setRequestChannel(inputChannel()); + adapter.setHandleNotificationStatus(true); + return adapter; + } +} +```` + +An XML variant may look like: + +````xml + +``` + +Note: by default the message `payload` is a `Map` converted from the received Topic JSON message. For the convenient +the `payload-expression` is provided with the `Message` as a root object of the evaluation context. Hence even some +HTTP headers, populated by the `DefaultHttpHeaderMapper`, are available for the evaluation context. + [Spring Cloud AWS]: https://github.com/spring-cloud/spring-cloud-aws [AWS SDK for Java]: http://aws.amazon.com/sdkforjava/ [Amazon Web Services]: http://aws.amazon.com/ diff --git a/build.gradle b/build.gradle index 284cdd3..7f0ce3d 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ apply plugin: 'java' apply from: "${rootProject.projectDir}/publish-maven.gradle" apply plugin: 'eclipse' apply plugin: 'idea' +apply plugin: 'jacoco' group = 'org.springframework.integration' @@ -28,15 +29,15 @@ configurations { ext { commonsIoVersion='2.4' - jacocoVersion = '0.7.2.201409121644' - slf4jVersion = '1.7.8' + servletApiVersion = '3.1.0' + slf4jVersion = '1.7.12' springCloudAwsVersion = '1.0.3.RELEASE' - springIntegrationVersion = '4.1.6.RELEASE' + springIntegrationVersion = '4.2.0.RELEASE' idPrefix = 'aws' linkHomepage = 'https://github.com/spring-projects/spring-integration-aws' - linkCi = 'https://build.spring.io/browse/INTEXT-AWS' + linkCi = 'https://build.spring.io/browse/INTEXT' linkIssue = 'https://jira.spring.io/browse/INTEXT' linkScmUrl = 'https://github.com/spring-projects/spring-integration-aws' linkScmConnection = 'https://github.com/spring-projects/spring-integration-aws.git' @@ -47,19 +48,21 @@ ext.javadocLinks = [ "http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/" ] as String[] +jacoco { + toolVersion = "0.7.2.201409121644" +} dependencies { compile "org.springframework.integration:spring-integration-core:$springIntegrationVersion" + compile ("org.springframework.integration:spring-integration-http:$springIntegrationVersion", optional) + compile("javax.servlet:javax.servlet-api:$servletApiVersion", provided) compile "org.springframework.cloud:spring-cloud-aws-messaging:$springCloudAwsVersion" compile "commons-io:commons-io:$commonsIoVersion" testCompile "org.springframework.integration:spring-integration-test:$springIntegrationVersion" testRuntime "org.slf4j:slf4j-log4j12:$slf4jVersion" - - jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" - } eclipse.project.natures += 'org.springframework.ide.eclipse.core.springnature' @@ -102,9 +105,22 @@ ext.xLintArg = '-Xlint:all,-options' test { // suppress all console output during testing unless running `gradle -i` logging.captureStandardOutput(LogLevel.INFO) - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=*" + maxHeapSize = "1024m" + jacoco { + append = false + destinationFile = file("$buildDir/jacoco.exec") + } } +jacocoTestReport { + reports { + xml.enabled false + csv.enabled false + html.destination "${buildDir}/reports/jacoco/html" + } +} + +build.dependsOn jacocoTestReport task sourcesJar(type: Jar) { classifier = 'sources' from sourceSets.main.allJava @@ -247,6 +263,6 @@ task dist(dependsOn: assemble) { task wrapper(type: Wrapper) { description = 'Generates gradlew[.bat] scripts' - gradleVersion = '2.3' + gradleVersion = '2.5' distributionUrl = "http://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 085a1cdc27db1185342f15a00441734e74fe3735..30d399d8d2bf522ff5de94bf434a7cc43a9a74b5 100644 GIT binary patch delta 8569 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 delta 8406 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 + * + * @author Artem Bilan + */ +public class SnsInboundChannelAdapterParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return SnsInboundChannelAdapter.class; + } + + @Override + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) + throws BeanDefinitionStoreException { + String id = super.resolveId(element, definition, parserContext); + + if (!element.hasAttribute("channel")) { + // the created channel will get the 'id', so the adapter's bean name includes a suffix + id = id + ".adapter"; + } + if (!StringUtils.hasText(id)) { + id = BeanDefinitionReaderUtils.generateBeanName(definition, parserContext.getRegistry()); + } + + return id; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + builder.addConstructorArgReference(element.getAttribute(AmazonWSParserUtils.SNS_REF)) + .addConstructorArgValue(element.getAttribute("path")); + String channelName = element.getAttribute("channel"); + if (!StringUtils.hasText(channelName)) { + channelName = IntegrationNamespaceUtils.createDirectChannel(element, parserContext); + } + builder.addPropertyReference("requestChannel", channelName); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "handle-notification-status"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-timeout", "requestTimeout"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE); + BeanDefinition payloadExpressionDef = + IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("payload-expression", element); + if (payloadExpressionDef != null) { + builder.addPropertyValue("payloadExpression", payloadExpressionDef); + } + } + +} diff --git a/src/main/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapter.java b/src/main/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapter.java new file mode 100644 index 0000000..76b28ea --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapter.java @@ -0,0 +1,203 @@ +/* + * Copyright 2015 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.inbound; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.cloud.aws.messaging.endpoint.NotificationStatus; +import org.springframework.cloud.aws.messaging.endpoint.NotificationStatusHandlerMethodArgumentResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.integration.aws.support.AwsHeaders; +import org.springframework.integration.expression.ValueExpression; +import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway; +import org.springframework.integration.http.inbound.RequestMapping; +import org.springframework.integration.mapping.HeaderMapper; +import org.springframework.integration.support.AbstractIntegrationMessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; +import org.springframework.web.multipart.MultipartResolver; + +import com.amazonaws.services.sns.AmazonSNS; + +/** + * The {@link HttpRequestHandlingMessagingGateway} extension for the Amazon WS SNS HTTP(S) endpoints. + * Accepts all {@code x-amz-sns-message-type}s, converts the received Topic JSON message to the + * {@link Map} using {@link MappingJackson2HttpMessageConverter} and send it to the provided + * {@link #requestChannel} as {@link Message} {@code payload}. + *

+ * The mapped url must be configured inside the Amazon Web Service platform as a subscription. + * Before receiving any notification itself this HTTP endpoint must confirm the subscription. + *

+ * The {@link #handleNotificationStatus} flag (defaults to {@code false}) indicates that + * this endpoint should send the {@code SubscriptionConfirmation/UnsubscribeConfirmation} + * messages to the the provided {@link #requestChannel}. If that, the {@link AwsHeaders#NOTIFICATION_STATUS} + * header is populated with the {@link NotificationStatus} value. In that case it is a responsibility of + * the application to {@link NotificationStatus#confirmSubscription()} or not. + *

+ * By default this endpoint just does {@link NotificationStatus#confirmSubscription()} + * for the {@code SubscriptionConfirmation} message type. + * And does nothing for the {@code UnsubscribeConfirmation}. + *

+ * For the convenience on the underlying message flow routing a {@link AwsHeaders#SNS_MESSAGE_TYPE} + * header is present. + * + * @author Artem Bilan + */ +public class SnsInboundChannelAdapter extends HttpRequestHandlingMessagingGateway { + + private final NotificationStatusResolver notificationStatusResolver; + + private volatile boolean handleNotificationStatus; + + private volatile Expression payloadExpression; + + private EvaluationContext evaluationContext; + + public SnsInboundChannelAdapter(AmazonSNS amazonSns, String... path) { + super(false); + Assert.notNull(amazonSns, "'amazonSns' must not be null."); + Assert.notNull(path, "'path' must not be null."); + Assert.noNullElements(path, "'path' must not contain null elements."); + this.notificationStatusResolver = new NotificationStatusResolver(amazonSns); + RequestMapping requestMapping = new RequestMapping(); + requestMapping.setMethods(HttpMethod.POST); + requestMapping.setHeaders("x-amz-sns-message-type"); + requestMapping.setPathPatterns(path); + super.setRequestMapping(requestMapping); + super.setStatusCodeExpression(new ValueExpression<>(HttpStatus.NO_CONTENT)); + super.setMessageConverters( + Collections.>singletonList(new MappingJackson2HttpMessageConverter())); + super.setRequestPayloadType(HashMap.class); + } + + public void setHandleNotificationStatus(boolean handleNotificationStatus) { + this.handleNotificationStatus = handleNotificationStatus; + } + + @Override + protected void onInit() throws Exception { + super.onInit(); + if (this.payloadExpression != null) { + this.evaluationContext = createEvaluationContext(); + } + } + + @Override + @SuppressWarnings("unchecked") + protected void send(Object object) { + Message message = (Message) object; + HashMap payload = (HashMap) message.getPayload(); + AbstractIntegrationMessageBuilder messageToSendBuilder; + if (this.payloadExpression != null) { + messageToSendBuilder = getMessageBuilderFactory() + .withPayload(this.payloadExpression.getValue(this.evaluationContext, message)) + .copyHeaders(message.getHeaders()); + } + else { + messageToSendBuilder = getMessageBuilderFactory().fromMessage(message); + } + + String type = payload.get("Type"); + if ("SubscriptionConfirmation".equals(type) || "UnsubscribeConfirmation".equals(type)) { + NotificationStatus notificationStatus = this.notificationStatusResolver.resolveNotificationStatus(payload); + if (this.handleNotificationStatus) { + messageToSendBuilder.setHeader(AwsHeaders.NOTIFICATION_STATUS, notificationStatus); + } + else { + if ("SubscriptionConfirmation".equals(type)) { + notificationStatus.confirmSubscription(); + } + return; + } + } + messageToSendBuilder.setHeader(AwsHeaders.SNS_MESSAGE_TYPE, type) + .setHeader(AwsHeaders.MESSAGE_ID, payload.get("MessageId")); + super.send(messageToSendBuilder.build()); + } + + @Override + public void setPayloadExpression(Expression payloadExpression) { + this.payloadExpression = payloadExpression; + } + + @Override + public void setHeaderExpressions(Map headerExpressions) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMessageConverters(List> messageConverters) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMergeWithDefaultConverters(boolean mergeWithDefaultConverters) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHeaderMapper(HeaderMapper headerMapper) { + throw new UnsupportedOperationException(); + } + + @Override + public void setRequestMapping(RequestMapping requestMapping) { + throw new UnsupportedOperationException(); + } + + @Override + public void setRequestPayloadType(Class requestPayloadType) { + throw new UnsupportedOperationException(); + } + + @Override + public void setExtractReplyPayload(boolean extractReplyPayload) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMultipartResolver(MultipartResolver multipartResolver) { + throw new UnsupportedOperationException(); + } + + @Override + public void setStatusCodeExpression(Expression statusCodeExpression) { + throw new UnsupportedOperationException(); + } + + private static class NotificationStatusResolver extends NotificationStatusHandlerMethodArgumentResolver { + + public NotificationStatusResolver(AmazonSNS amazonSns) { + super(amazonSns); + } + + protected NotificationStatus resolveNotificationStatus(HashMap content) { + return (NotificationStatus) super.doResolverArgumentFromNotificationMessage(content); + } + + } + +} diff --git a/src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java b/src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java index 6126de0..c6b6236 100644 --- a/src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java +++ b/src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java @@ -22,7 +22,7 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.aws.support.AwsHeaders; -import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.messaging.Message; import org.springframework.util.Assert; @@ -74,7 +74,7 @@ public class SqsMessageHandler extends AbstractMessageHandler { @Override protected void onInit() throws Exception { super.onInit(); - this.evaluationContext = IntegrationContextUtils.getEvaluationContext(getBeanFactory()); + this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); } @Override @@ -90,4 +90,3 @@ public class SqsMessageHandler extends AbstractMessageHandler { } } - diff --git a/src/main/java/org/springframework/integration/aws/support/AwsHeaders.java b/src/main/java/org/springframework/integration/aws/support/AwsHeaders.java index 7ee6b0e..556664c 100644 --- a/src/main/java/org/springframework/integration/aws/support/AwsHeaders.java +++ b/src/main/java/org/springframework/integration/aws/support/AwsHeaders.java @@ -29,4 +29,8 @@ public abstract class AwsHeaders { public static final String RECEIPT_HANDLE = PREFIX + "receiptHandle"; + public static final String NOTIFICATION_STATUS = PREFIX + "notificationStatus"; + + public static final String SNS_MESSAGE_TYPE = PREFIX + "snsMessageType"; + } diff --git a/src/main/resources/org/springframework/integration/aws/config/xml/spring-integration-aws-1.0.xsd b/src/main/resources/org/springframework/integration/aws/config/xml/spring-integration-aws-1.0.xsd index 5b92d2a..1b3f0f0 100644 --- a/src/main/resources/org/springframework/integration/aws/config/xml/spring-integration-aws-1.0.xsd +++ b/src/main/resources/org/springframework/integration/aws/config/xml/spring-integration-aws-1.0.xsd @@ -522,6 +522,80 @@ + + + + Defines an SNS inbound HTTP-based Channel Adapter - SnsInboundChannelAdapter. + + + + + + + + The 'com.amazonaws.services.sns.AmazonSNS' bean reference. + + + + + + + + + + + + Comma-separated URI paths (e.g., /orderId/{order}). + Ant-style path patterns are also supported (e.g. /myPath/*.do). + + + + + + + Maximum amount of time in milliseconds to wait when sending + a message to the channel if such channel may block. + For example, a Queue Channel can block until space + is available if its maximum capacity has been reached. + + + + + + + Allows you to specify SpEL expression to construct a Message payload. + The root evaluation object is a raw Message as a result of the 'HttpServletRequest' + conversion in the 'HttpRequestHandlingEndpointSupport' super class. + + + + + + + + + + + + The MessagingGateway's 'error-channel' where to send an ErrorMessage in case + of Exception is caused from original message flow. + + + + + + + Flag to indicate if the 'SubscriptionConfirmation' and 'UnsubscribeConfirmation' + SNS messages should sent to the 'channel' or not. If 'true' the + 'AwsHeaders.NOTIFICATION_STATUS' message header is populated with the 'NotificationStatus' + value. In this case it is an application responsibility to 'confirm' subscription or not using + that 'NotificationStatus' object. Defaults to 'false'. + + + + + + @@ -536,8 +610,7 @@ - + @@ -549,8 +622,7 @@ - + diff --git a/src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests-context.xml b/src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests-context.xml new file mode 100644 index 0000000..3f711fc --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests-context.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests.java b/src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests.java new file mode 100644 index 0000000..1a8c9c9 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/config/xml/SnsInboundChannelAdapterParserTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015 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.config.xml; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer; +import org.springframework.integration.aws.inbound.SnsInboundChannelAdapter; +import org.springframework.integration.channel.NullChannel; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.MessageChannel; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.amazonaws.services.sns.AmazonSNS; + +/** + * @author Artem Bilan + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class SnsInboundChannelAdapterParserTests { + + @Autowired + private AmazonSNS amazonSns; + + @Autowired + private MessageChannel errorChannel; + + @Autowired + private NullChannel nullChannel; + + @Autowired + @Qualifier("snsInboundChannelAdapter") + private SnsInboundChannelAdapter snsInboundChannelAdapter; + + + @Test + public void testSqsMessageDrivenChannelAdapterParser() { + assertSame(this.amazonSns, TestUtils.getPropertyValue(this.snsInboundChannelAdapter, + "notificationStatusResolver.amazonSns")); + assertTrue(TestUtils.getPropertyValue(this.snsInboundChannelAdapter, "handleNotificationStatus", Boolean.class)); + assertArrayEquals(new String[] {"/foo"}, + TestUtils.getPropertyValue(this.snsInboundChannelAdapter, "requestMapping.pathPatterns", String[].class)); + assertEquals("payload.Message", + TestUtils.getPropertyValue(this.snsInboundChannelAdapter, "payloadExpression.expression")); + assertFalse(this.snsInboundChannelAdapter.isRunning()); + assertEquals(100, this.snsInboundChannelAdapter.getPhase()); + assertFalse(this.snsInboundChannelAdapter.isAutoStartup()); + assertSame(this.errorChannel, TestUtils.getPropertyValue(this.snsInboundChannelAdapter, "requestChannel")); + assertSame(this.nullChannel, TestUtils.getPropertyValue(this.snsInboundChannelAdapter, "errorChannel")); + assertEquals(2000L, TestUtils.getPropertyValue(this.snsInboundChannelAdapter, "messagingTemplate.sendTimeout")); + } + +} diff --git a/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests-context.xml b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests-context.xml index 6761df8..31a839a 100644 --- a/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests-context.xml +++ b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests-context.xml @@ -5,7 +5,7 @@ xmlns:aws-messaging="http://www.springframework.org/schema/cloud/aws/messaging" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd - http://www.springframework.org/schema/cloud/aws/messaging http://www.springframework.org/schema/cloud/aws/messaging/spring-cloud-aws-messaging-1.0.xsd"> + http://www.springframework.org/schema/cloud/aws/messaging http://www.springframework.org/schema/cloud/aws/messaging/spring-cloud-aws-messaging.xsd"> diff --git a/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests-context.xml b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests-context.xml index cc69062..4c84db0 100644 --- a/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests-context.xml +++ b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests-context.xml @@ -5,7 +5,7 @@ xmlns:aws-messaging="http://www.springframework.org/schema/cloud/aws/messaging" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration/aws http://www.springframework.org/schema/integration/aws/spring-integration-aws.xsd - http://www.springframework.org/schema/cloud/aws/messaging http://www.springframework.org/schema/cloud/aws/messaging/spring-cloud-aws-messaging-1.0.xsd"> + http://www.springframework.org/schema/cloud/aws/messaging http://www.springframework.org/schema/cloud/aws/messaging/spring-cloud-aws-messaging.xsd"> diff --git a/src/test/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapterTests.java b/src/test/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapterTests.java new file mode 100644 index 0000000..40bc528 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapterTests.java @@ -0,0 +1,181 @@ +/* + * Copyright 2015 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.inbound; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.aws.messaging.endpoint.NotificationStatus; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.integration.aws.support.AwsHeaders; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.messaging.Message; +import org.springframework.messaging.PollableChannel; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.StreamUtils; +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.context.WebApplicationContext; + +import com.amazonaws.services.sns.AmazonSNS; + +/** + * @author Artem Bilan + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +@DirtiesContext +public class SnsInboundChannelAdapterTests { + + @Autowired + private WebApplicationContext context; + + @Autowired + private AmazonSNS amazonSns; + + @Autowired + private PollableChannel inputChannel; + + @Value("classpath:org/springframework/integration/aws/inbound/subscriptionConfirmation.json") + private Resource subscriptionConfirmation; + + @Value("classpath:org/springframework/integration/aws/inbound/notificationMessage.json") + private Resource notificationMessage; + + @Value("classpath:org/springframework/integration/aws/inbound/unsubscribeConfirmation.json") + private Resource unsubscribeConfirmation; + + private MockMvc mockMvc; + + @Before + public void setUp() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + @Test + public void testSubscriptionConfirmation() throws Exception { + this.mockMvc.perform( + post("/mySampleTopic") + .header("x-amz-sns-message-type", "SubscriptionConfirmation") + .contentType(MediaType.APPLICATION_JSON) + .content(StreamUtils.copyToByteArray(this.subscriptionConfirmation.getInputStream()))) + .andExpect(status().isNoContent()); + + Message receive = inputChannel.receive(10000); + assertNotNull(receive); + assertTrue(receive.getHeaders().containsKey(AwsHeaders.SNS_MESSAGE_TYPE)); + assertEquals("SubscriptionConfirmation", receive.getHeaders().get(AwsHeaders.SNS_MESSAGE_TYPE)); + + assertTrue(receive.getHeaders().containsKey(AwsHeaders.NOTIFICATION_STATUS)); + NotificationStatus notificationStatus = (NotificationStatus) receive.getHeaders() + .get(AwsHeaders.NOTIFICATION_STATUS); + + notificationStatus.confirmSubscription(); + + verify(this.amazonSns) + .confirmSubscription("arn:aws:sns:eu-west-1:111111111111:mySampleTopic", "111"); + } + + @Test + @SuppressWarnings("unchecked") + public void testNotification() throws Exception { + this.mockMvc.perform( + post("/mySampleTopic") + .header("x-amz-sns-message-type", "Notification") + .contentType(MediaType.APPLICATION_JSON) + .content(StreamUtils.copyToByteArray(this.notificationMessage.getInputStream()))) + .andExpect(status().isNoContent()); + + Message receive = inputChannel.receive(10000); + assertNotNull(receive); + Map payload = (Map) receive.getPayload(); + + assertEquals("foo", payload.get("Subject")); + assertEquals("bar", payload.get("Message")); + } + + @Test + public void testUnsubscribe() throws Exception { + this.mockMvc.perform( + post("/mySampleTopic") + .header("x-amz-sns-message-type", "UnsubscribeConfirmation") + .contentType(MediaType.APPLICATION_JSON) + .content(StreamUtils.copyToByteArray(this.unsubscribeConfirmation.getInputStream()))) + .andExpect(status().isNoContent()); + + Message receive = inputChannel.receive(10000); + assertNotNull(receive); + assertTrue(receive.getHeaders().containsKey(AwsHeaders.SNS_MESSAGE_TYPE)); + assertEquals("UnsubscribeConfirmation", receive.getHeaders().get(AwsHeaders.SNS_MESSAGE_TYPE)); + + assertTrue(receive.getHeaders().containsKey(AwsHeaders.NOTIFICATION_STATUS)); + NotificationStatus notificationStatus = (NotificationStatus) receive.getHeaders() + .get(AwsHeaders.NOTIFICATION_STATUS); + + notificationStatus.confirmSubscription(); + + verify(this.amazonSns) + .confirmSubscription("arn:aws:sns:eu-west-1:111111111111:mySampleTopic", "233"); + } + + @Configuration + @EnableIntegration + public static class ContextConfiguration { + + @Bean + public AmazonSNS amazonSns() { + return Mockito.mock(AmazonSNS.class); + } + + @Bean + public PollableChannel inputChannel() { + return new QueueChannel(); + } + + @Bean + public HttpRequestHandler sqsMessageDrivenChannelAdapter() { + SnsInboundChannelAdapter adapter = new SnsInboundChannelAdapter(amazonSns(), "/mySampleTopic"); + adapter.setRequestChannel(inputChannel()); + adapter.setHandleNotificationStatus(true); + return adapter; + } + + } + +} diff --git a/src/test/java/org/springframework/integration/aws/inbound/notificationMessage.json b/src/test/java/org/springframework/integration/aws/inbound/notificationMessage.json new file mode 100644 index 0000000..dd5c48c --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/inbound/notificationMessage.json @@ -0,0 +1,26 @@ +{ + "Type": "Notification", + "MessageId": "f2c15fec-c617-5b08-b54d-13c4099fec60", + "TopicArn": "arn:aws:sns:eu-west-1:111111111111:mySampleTopic", + "Subject": "foo", + "Message": "bar", + "Timestamp": "2014-06-28T14:12:24.418Z", + "SignatureVersion": "1", + "Signature": "XDvKSAnhxECrAmyIrs0Dsfbp/tnKD1IvoOOYTU28FtbUoxr/CgziuW87yZwTuSNNbHJbdD3BEjHS0vKewm0xBeQ0PToDkgtoORXo5RWnmShDQ2nhkthFhZnNulKtmFtRogjBtCwbz8sPnbOCSk21ruyXNdV2RUbdDalndAW002CWEQmYMxFSN6OXUtMueuT610aX+tqeYP4Z6+8WTWLWjAuVyy7rOI6KHYBcVDhKtskvTOPZ4tiVohtQdQbO2Gjuh1vblRzzwMkfaoFTSWImd4pFXxEsv/fq9aGIlqq9xEryJ0w2huFwI5gxyhvGt0RnTd9YvmAEC+WzdJDOqaDNxg==", + "SigningCertURL": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-e372f8ca30337fdb084e8ac449342c77.pem", + "UnsubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:721324560415:mySampleTopic:9859a6c9-6083-4690-ab02-d1aead3442df", + "MessageAttributes": { + "AWS.SNS.MOBILE.MPNS.Type": { + "Type": "String", + "Value": "token" + }, + "AWS.SNS.MOBILE.WNS.Type": { + "Type": "String", + "Value": "wns/badge" + }, + "AWS.SNS.MOBILE.MPNS.NotificationClass": { + "Type": "String", + "Value": "realtime" + } + } +} diff --git a/src/test/java/org/springframework/integration/aws/inbound/subscriptionConfirmation.json b/src/test/java/org/springframework/integration/aws/inbound/subscriptionConfirmation.json new file mode 100644 index 0000000..8869637 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/inbound/subscriptionConfirmation.json @@ -0,0 +1,12 @@ +{ + "Type": "SubscriptionConfirmation", + "MessageId": "e267b24c-5532-472f-889d-c2cdd2143bbc", + "Token": "111", + "TopicArn": "arn:aws:sns:eu-west-1:111111111111:mySampleTopic", + "Message": "You have chosen to subscribe to the topic arn:aws:sns:eu-west-1:721324560415:mySampleTopic.To confirm the subscription, visit the SubscribeURL included in this message.", + "SubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:eu-west-1:111111111111:mySampleTopic&Token=111", + "Timestamp": "2014-06-28T10:22:18.086Z", + "SignatureVersion": "1", + "Signature": "JLdRUR+uhP4cyVW6bRuUSAkUosFMJyO7g7WCAwEUJoB4y8vQE1uDUWGpbQSEbruVTjPEM8hFsf4/95NftfM0W5IgND1uSnv4P/4AYyL+q0bLOJlquzXrw4w2NX3QShS3y+r/gXzo7p/UP4NOr35MGCEGPqHAEe1Coc5S0eaP3JvKU6xY1tcop6ze2RNHTwzhM43dda2bnjPYogAJzA5uHfmSjs3cMVvPCckj3zdLyvxISp+RgrogdvlNyu9ycND1SxagmbzjkBaqvF/4aiSYFxsEXX4e9zuNuHGmXGWgm1ppYUGLSPPJruCsPUa7Ii1mYvpX7SezuFZlAAXXBk0mHg==", + "SigningCertURL": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-e372f8ca30337fdb084e8ac449342c77.pem" +} diff --git a/src/test/java/org/springframework/integration/aws/inbound/unsubscribeConfirmation.json b/src/test/java/org/springframework/integration/aws/inbound/unsubscribeConfirmation.json new file mode 100644 index 0000000..36317b5 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/inbound/unsubscribeConfirmation.json @@ -0,0 +1,12 @@ +{ + "Type": "UnsubscribeConfirmation", + "MessageId": "7b9a6321-45d5-461a-bc35-9c2d18ed9dbe", + "Token": "233", + "TopicArn": "arn:aws:sns:eu-west-1:111111111111:mySampleTopic", + "Message": "You have chosen to deactivate subscription arn:aws:sns:eu-west-1:111111111111:mySampleTopic:f64111de-e681-4820-a8be-474f64c1bbf8.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.", + "SubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:eu-west-1:721324560415:mySampleTopic&Token=233", + "Timestamp": "2014-06-28T19:33:00.497Z", + "SignatureVersion": "1", + "Signature": "EAb0k1XoD2h+u4j2GB42wEFCVUFKEaS/2W+p+3wK1GfZDZn4LeDLbkxJ1LYF/A1CYL+sCJ4ZmLFI1axm0V/p+fESkNbKQoQotMgma+PtA6KnmRrKEU8O6nUELqeVPWFAoQ9ZsW9FCAXVDXoPxiqHNTH+tC7mzAsvajyp4aTm/POqkRKBl+A/7dHUqfHGup/FJhLNgTAciBZSloa5EuBKxInJQfoZjy3DU8qKXXhmKKRdyVwOGEuReo/njy4c3Phtn0+logu2PZUKqkGTuJZVbapHmcTq+0MqIh05sevLDmTEfBlsmNThhWIyCza/t68RRlqm9cRLjINSWfrv1Xkrpw==", + "SigningCertURL": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-e372f8ca30337fdb084e8ac449342c77.pem" +}