From ec4a93142da5757122a7254ea1814b7a37b72faf Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 12 Feb 2015 16:29:00 +0200 Subject: [PATCH] INTEXT-7: Add SQS Adapters JIRA: https://jira.spring.io/browse/INTEXT-7 Upgrade to Gradle 2.3 Minor Doc Polishing --- build.gradle | 22 +- gradle/wrapper/gradle-wrapper.jar | Bin 51348 -> 52141 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- .../aws/config/xml/AWSNamespaceHandler.java | 5 + .../aws/config/xml/AmazonWSParserUtils.java | 36 ++- .../SqsMessageDrivenChannelAdapterParser.java | 82 +++++ .../xml/SqsOutboundChannelAdapterParser.java | 52 +++ .../aws/config/xml/package-info.java | 4 + .../SqsMessageDrivenChannelAdapter.java | 150 +++++++++ .../integration/aws/inbound/package-info.java | 4 + .../aws/outbound/SqsMessageHandler.java | 89 ++++++ .../aws/outbound/package-info.java | 4 + .../integration/aws/support/AwsHeaders.java | 32 ++ .../integration/aws/support/package-info.java | 4 + .../config/xml/spring-integration-aws-1.0.xsd | 301 +++++++++++++++--- ...rivenChannelAdapterParserTests-context.xml | 43 +++ ...essageDrivenChannelAdapterParserTests.java | 101 ++++++ .../SqsMessageHandlerParserTests-context.xml | 24 ++ .../xml/SqsMessageHandlerParserTests.java | 79 +++++ .../SqsMessageDrivenChannelAdapterTests.java | 110 +++++++ .../aws/outbound/SqsMessageHandlerTests.java | 134 ++++++++ src/test/resources/log4j.properties | 4 +- 22 files changed, 1203 insertions(+), 81 deletions(-) create mode 100644 src/main/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParser.java create mode 100644 src/main/java/org/springframework/integration/aws/config/xml/SqsOutboundChannelAdapterParser.java create mode 100644 src/main/java/org/springframework/integration/aws/config/xml/package-info.java create mode 100644 src/main/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapter.java create mode 100644 src/main/java/org/springframework/integration/aws/inbound/package-info.java create mode 100644 src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java create mode 100644 src/main/java/org/springframework/integration/aws/outbound/package-info.java create mode 100644 src/main/java/org/springframework/integration/aws/support/AwsHeaders.java create mode 100644 src/main/java/org/springframework/integration/aws/support/package-info.java create mode 100644 src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests-context.xml create mode 100644 src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests.java create mode 100644 src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests-context.xml create mode 100644 src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests.java create mode 100644 src/test/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapterTests.java create mode 100644 src/test/java/org/springframework/integration/aws/outbound/SqsMessageHandlerTests.java diff --git a/build.gradle b/build.gradle index f95cfaa..a9d33a2 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ description = 'Spring Integration AWS Support' buildscript { repositories { - maven { url 'http://repo.spring.io/plugins-snapshot' } + maven { url 'http://repo.spring.io/plugins-release' } } } @@ -15,11 +15,10 @@ group = 'org.springframework.integration' repositories { maven { url 'http://repo.spring.io/libs-milestone' } - maven { url 'http://repo.spring.io/plugins-release' } + maven { url 'http://repo.spring.io/libs-snapshot' } } -sourceCompatibility=1.6 -targetCompatibility=1.6 +sourceCompatibility = targetCompatibility = 1.7 // See http://www.gradle.org/docs/current/userguide/dependency_management.html#sub:configurations // and http://www.gradle.org/docs/current/dsl/org.gradle.api.artifacts.ConfigurationContainer.html @@ -29,8 +28,9 @@ configurations { ext { commonsIoVersion='2.4' - jacocoVersion = '0.7.1.201405082137' - springCloudAwsVersion = '1.0.0.RC2' + jacocoVersion = '0.7.2.201409121644' + slf4jVersion = '1.7.8' + springCloudAwsVersion = '1.0.0.BUILD-SNAPSHOT' springIntegrationVersion = '4.1.2.RELEASE' idPrefix = 'aws' @@ -56,15 +56,13 @@ dependencies { 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' - } -} +eclipse.project.natures += 'org.springframework.ide.eclipse.core.springnature' javadoc { group = 'Documentation' @@ -249,6 +247,6 @@ task dist(dependsOn: assemble) { task wrapper(type: Wrapper) { description = 'Generates gradlew[.bat] scripts' - gradleVersion = '1.12' + gradleVersion = '2.3' 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 0087cd3b18659b5577cf6ad3ef61f8eb9416ebba..085a1cdc27db1185342f15a00441734e74fe3735 100644 GIT binary patch delta 20006 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 delta 19193 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 ove~tpA@RwG0)=N%#OK!lL%xx2pdXP%h?w} + * + * @author Artem Bilan + */ +public class SqsMessageDrivenChannelAdapterParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return SqsMessageDrivenChannelAdapter.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.SQS_REF)) + .addConstructorArgValue(element.getAttribute("queues")); + String channelName = element.getAttribute("channel"); + if (!StringUtils.hasText(channelName)) { + channelName = IntegrationNamespaceUtils.createDirectChannel(element, parserContext); + } + builder.addPropertyReference("outputChannel", channelName); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, + AmazonWSParserUtils.RESOURCE_ID_RESOLVER_REF); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "task-executor"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "destination-resolver"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-timeout"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "payload-type"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "delete-message-on-exception"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "max-number-of-messages"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "visibility-timeout"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "wait-time-out"); + } + +} diff --git a/src/main/java/org/springframework/integration/aws/config/xml/SqsOutboundChannelAdapterParser.java b/src/main/java/org/springframework/integration/aws/config/xml/SqsOutboundChannelAdapterParser.java new file mode 100644 index 0000000..010da41 --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/config/xml/SqsOutboundChannelAdapterParser.java @@ -0,0 +1,52 @@ +/* + * 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 org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.aws.outbound.SqsMessageHandler; +import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.util.StringUtils; + +/** + * The parser for the {@code } + * + * @author Artem Bilan + */ +public class SqsOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser { + + @Override + protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(SqsMessageHandler.class); + builder.addConstructorArgReference(element.getAttribute(AmazonWSParserUtils.SQS_REF)); + if (StringUtils.hasText(element.getAttribute(AmazonWSParserUtils.RESOURCE_ID_RESOLVER_REF))) { + builder.addConstructorArgReference(element.getAttribute(AmazonWSParserUtils.RESOURCE_ID_RESOLVER_REF)); + } + BeanDefinition queue = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("queue", + "queue-expression", parserContext, element, false); + if (queue != null) { + builder.addPropertyValue("queueExpression", queue); + } + return builder.getBeanDefinition(); + } + +} diff --git a/src/main/java/org/springframework/integration/aws/config/xml/package-info.java b/src/main/java/org/springframework/integration/aws/config/xml/package-info.java new file mode 100644 index 0000000..28e56de --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/config/xml/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides the parser classes for Integration AWS Namespace. + */ +package org.springframework.integration.aws.config.xml; diff --git a/src/main/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapter.java b/src/main/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapter.java new file mode 100644 index 0000000..6859990 --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapter.java @@ -0,0 +1,150 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.cloud.aws.core.env.ResourceIdResolver; +import org.springframework.cloud.aws.messaging.config.SimpleMessageListenerContainerFactory; +import org.springframework.cloud.aws.messaging.listener.QueueMessageHandler; +import org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer; +import org.springframework.core.task.TaskExecutor; +import org.springframework.integration.aws.support.AwsHeaders; +import org.springframework.integration.endpoint.MessageProducerSupport; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.core.DestinationResolver; +import org.springframework.messaging.handler.HandlerMethod; +import org.springframework.util.Assert; + +import com.amazonaws.services.sqs.AmazonSQSAsync; + +/** + * The {@link MessageProducerSupport} implementation for the Amazon SQS {@code receiveMessage}. + * Works in 'listener' manner and delegates hard to the {@link SimpleMessageListenerContainer}. + * + * @author Artem Bilan + * * + * @see SimpleMessageListenerContainerFactory + * @see SimpleMessageListenerContainer + * @see QueueMessageHandler + */ +public class SqsMessageDrivenChannelAdapter extends MessageProducerSupport + implements DisposableBean { + + private final SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory = + new SimpleMessageListenerContainerFactory(); + + private final String[] queues; + + private SimpleMessageListenerContainer listenerContainer; + + public SqsMessageDrivenChannelAdapter(AmazonSQSAsync amazonSqs, String... queues) { + Assert.noNullElements(queues, "'queues' must not be empty"); + this.simpleMessageListenerContainerFactory.setAmazonSqs(amazonSqs); + this.queues = Arrays.copyOf(queues, queues.length); + } + + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.simpleMessageListenerContainerFactory.setTaskExecutor(taskExecutor); + } + + public void setMaxNumberOfMessages(Integer maxNumberOfMessages) { + this.simpleMessageListenerContainerFactory.setMaxNumberOfMessages(maxNumberOfMessages); + } + + public void setVisibilityTimeout(Integer visibilityTimeout) { + this.simpleMessageListenerContainerFactory.setVisibilityTimeout(visibilityTimeout); + } + + public void setWaitTimeOut(Integer waitTimeOut) { + this.simpleMessageListenerContainerFactory.setWaitTimeOut(waitTimeOut); + } + + public void setResourceIdResolver(ResourceIdResolver resourceIdResolver) { + this.simpleMessageListenerContainerFactory.setResourceIdResolver(resourceIdResolver); + } + + public void setDestinationResolver(DestinationResolver destinationResolver) { + this.simpleMessageListenerContainerFactory.setDestinationResolver(destinationResolver); + } + + public void setDeleteMessageOnException(Boolean deleteMessageOnException) { + this.simpleMessageListenerContainerFactory.setDeleteMessageOnException(deleteMessageOnException); + } + + @Override + protected void onInit() { + super.onInit(); + this.listenerContainer = this.simpleMessageListenerContainerFactory.createSimpleMessageListenerContainer(); + this.listenerContainer.setMessageHandler(new IntegrationQueueMessageHandler()); + try { + this.listenerContainer.afterPropertiesSet(); + } + catch (Exception e) { + throw new BeanCreationException("Cannot instantiate 'SimpleMessageListenerContainer'", e); + } + } + + @Override + protected void doStart() { + this.listenerContainer.start(); + } + + @Override + protected void doStop() { + this.listenerContainer.stop(); + } + + @Override + public void destroy() throws Exception { + this.listenerContainer.destroy(); + } + + private class IntegrationQueueMessageHandler extends QueueMessageHandler { + + @Override + public Map getHandlerMethods() { + Set queues = new HashSet<>(Arrays.asList(SqsMessageDrivenChannelAdapter.this.queues)); + return Collections.singletonMap(new MappingInformation(queues), null); + } + + @Override + protected void handleMessageInternal(Message message, String lookupDestination) { + MessageHeaders headers = message.getHeaders(); + Message messageToSend = getMessageBuilderFactory() + .fromMessage(message) + .removeHeaders(QueueMessageHandler.Headers.LOGICAL_RESOURCE_ID_MESSAGE_HEADER_KEY, + "MessageId", + "ReceiptHandle") + .setHeader(AwsHeaders.MESSAGE_ID, headers.get("MessageId")) + .setHeader(AwsHeaders.RECEIPT_HANDLE, headers.get("ReceiptHandle")) + .setHeader(AwsHeaders.QUEUE, + headers.get(QueueMessageHandler.Headers.LOGICAL_RESOURCE_ID_MESSAGE_HEADER_KEY)) + .build(); + sendMessage(messageToSend); + } + + } + +} diff --git a/src/main/java/org/springframework/integration/aws/inbound/package-info.java b/src/main/java/org/springframework/integration/aws/inbound/package-info.java new file mode 100644 index 0000000..f48c7d9 --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/inbound/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes which represent inbound AWS components. + */ +package org.springframework.integration.aws.inbound; diff --git a/src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java b/src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java new file mode 100644 index 0000000..5712bf5 --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/outbound/SqsMessageHandler.java @@ -0,0 +1,89 @@ +/* + * 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.outbound; + +import org.springframework.cloud.aws.core.env.ResourceIdResolver; +import org.springframework.cloud.aws.messaging.core.QueueMessagingTemplate; +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.expression.IntegrationEvaluationContextAware; +import org.springframework.integration.handler.AbstractMessageHandler; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.amazonaws.services.sqs.AmazonSQS; + +/** + * The {@link AbstractMessageHandler} implementation for the Amazon SQS {@code sendMessage}. + * + * @author Artem Bilan + * + * @see QueueMessagingTemplate + * @see org.springframework.cloud.aws.messaging.core.QueueMessageChannel + */ +public class SqsMessageHandler extends AbstractMessageHandler implements IntegrationEvaluationContextAware { + + private final QueueMessagingTemplate template; + + private Expression queueExpression; + + private EvaluationContext evaluationContext; + + public SqsMessageHandler(AmazonSQS amazonSqs) { + this(amazonSqs, null); + } + + public SqsMessageHandler(AmazonSQS amazonSqs, ResourceIdResolver resourceIdResolver) { + this.template = new QueueMessagingTemplate(amazonSqs, resourceIdResolver); + } + + public void setQueue(String queue) { + Assert.hasText(queue, "'queue' must not be empty"); + this.queueExpression = new LiteralExpression(queue); + } + + public void setQueueExpression(Expression queueExpression) { + Assert.notNull(queueExpression, "'queueExpression' must not be null"); + this.queueExpression = queueExpression; + } + + @Override + public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) { + this.evaluationContext = evaluationContext; + } + + @Override + protected void onInit() throws Exception { + Assert.notNull(this.evaluationContext); + } + + @Override + protected void handleMessageInternal(Message message) throws Exception { + String queue = message.getHeaders().get(AwsHeaders.QUEUE, String.class); + if (!StringUtils.hasText(queue) && this.queueExpression != null) { + queue = this.queueExpression.getValue(this.evaluationContext, message, String.class); + } + Assert.state(queue != null, "'queue' must not be null for sending an SQS message. " + + "Consider configuring this handler with a 'queue'( or 'queueExpression') or supply an " + + "'aws_queue' message header"); + this.template.send(queue, message); + } + +} diff --git a/src/main/java/org/springframework/integration/aws/outbound/package-info.java b/src/main/java/org/springframework/integration/aws/outbound/package-info.java new file mode 100644 index 0000000..843e9d1 --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/outbound/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes which represent outbound AWS components. + */ +package org.springframework.integration.aws.outbound; diff --git a/src/main/java/org/springframework/integration/aws/support/AwsHeaders.java b/src/main/java/org/springframework/integration/aws/support/AwsHeaders.java new file mode 100644 index 0000000..7ee6b0e --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/support/AwsHeaders.java @@ -0,0 +1,32 @@ +/* + * 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.support; + +/** + * @author Artem Bilan + */ +public abstract class AwsHeaders { + + private static final String PREFIX = "aws_"; + + public static final String QUEUE = PREFIX + "queue"; + + public static final String MESSAGE_ID = PREFIX + "messageId"; + + public static final String RECEIPT_HANDLE = PREFIX + "receiptHandle"; + +} diff --git a/src/main/java/org/springframework/integration/aws/support/package-info.java b/src/main/java/org/springframework/integration/aws/support/package-info.java new file mode 100644 index 0000000..d774de6 --- /dev/null +++ b/src/main/java/org/springframework/integration/aws/support/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides support classes for AWS components. + */ +package org.springframework.integration.aws.support; 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 e57b115..b3bee51 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 @@ -1,17 +1,17 @@ + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:beans="http://www.springframework.org/schema/beans" + xmlns:tool="http://www.springframework.org/schema/tool" + xmlns:integration="http://www.springframework.org/schema/integration" + targetNamespace="http://www.springframework.org/schema/integration/aws" + elementFormDefault="qualified" + attributeFormDefault="unqualified"> + schemaLocation="http://www.springframework.org/schema/integration/spring-integration.xsd"/> @@ -20,46 +20,46 @@ ]]> - - - - - Specify the access key to be used to connect to the AWS service. - - - - - - - Specify the secret key corresponding to the access key to be used to - authenticate the user and connect to the AWS service. - - - - - - - Specifies the properties file used that will be used to hold the access key - and the secret key of the AWS. This file expects two properties accessKey and - secretKey for Access key and Secret Key respectively. This attribute is mutually - exclusive to the accessKey, secretKey and credentials-ref attribute. - - - - - - - - - - - - If none of the default mechanisms work, you may provide a custom implementation - of org.springframework.integration.aws.core.AWSCredentials. Various use cases are - when you might want to read the credentials from a database or any other secure store - - - + + + + + Specify the access key to be used to connect to the AWS service. + + + + + + + Specify the secret key corresponding to the access key to be used to + authenticate the user and connect to the AWS service. + + + + + + + Specifies the properties file used that will be used to hold the access key + and the secret key of the AWS. This file expects two properties accessKey and + secretKey for Access key and Secret Key respectively. This attribute is mutually + exclusive to the accessKey, secretKey and credentials-ref attribute. + + + + + + + + + + + + If none of the default mechanisms work, you may provide a custom implementation + of org.springframework.integration.aws.core.AWSCredentials. Various use cases are + when you might want to read the credentials from a database or any other secure store + + + @@ -166,7 +166,8 @@ - + @@ -201,7 +202,8 @@ - Reference to the bean with an implementation of org.springframework.integration.aws.s3.core.AmazonS3Operations + Reference to the bean with an implementation of + org.springframework.integration.aws.s3.core.AmazonS3Operations that would be used to perform the operations on the S3 bucket. If not provided, the default implementation used is org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations which is the @@ -266,7 +268,8 @@ - Reference to the bean with an implementation of org.springframework.integration.aws.s3.core.AmazonS3Operations + Reference to the bean with an implementation of + org.springframework.integration.aws.s3.core.AmazonS3Operations that would be used to perform the operations on the S3 bucket. If not provided, the default implementation used is org.springframework.integration.aws.s3.core.DefaultAmazonS3Operations which uses AWS SDK. @@ -314,7 +317,8 @@ The maximum number of objects returned in the listOperation performed on the S3 bucket The default value used internally is 100. That is not more than 100 objects would be returned - in one call to list the objects, subsequent calls will be made to the Web service to retrieve the next + in one call to list the objects, subsequent calls will be made to the Web service to retrieve + the next batch of objects. @@ -348,4 +352,197 @@ + + + + + Defines an outbound SQS Channel Adapter for sending messages to queues. + + + + + + + + + + + + The Amazon queue name or URL. + Mutually exclusive with 'queue-expression'. + This attribute isn't mandatory and the queue can be specified in message headers + with the 'AwsHeaders.QUEUE' header name. + + + + + + + A SpEL expression that resolves to an Amazon queue or its URL. + The 'requestMessage' is the root object for evaluation context. + Mutually exclusive with 'queue'. + This attribute isn't mandatory and the queue can be specified in message headers with + the 'AwsHeaders.QUEUE' header name. + + + + + + + + + + + + Configures an endpoint ('SqsMessageDrivenChannelAdapter') that will receive + Amazon SQS message from the provided 'queues'. + + + + + + + + + Message Channel to which error Messages should be sent. + + + + + + + + + + + + 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. + + + + + + + Comma-separated SQS queue names or their URLs. + + + + + + + The 'org.springframework.core.task.TaskExecutor' to run the underlying listener task + from the + 'org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer'. + + + + + + + + + + + + Configure the maximum number of messages that should be retrieved during one + poll to the Amazon SQS system. This number must be a positive, non-zero number that + has a maximum number of 10. Values higher then 10 are currently + not supported by the queueing system. Defaults to 1. + + + + + + + Configures the duration (in seconds) that the received messages are hidden from + subsequent poll requests after being retrieved from the system. + + + + + + + + + + + + + + A reference to a bean that implements the + 'org.springframework.messaging.core.DestinationResolver' interface. + E.g. + 'org.springframework.cloud.aws.messaging.support.destination.DynamicQueueUrlDestinationResolver' + + + + + + + + + + + + Defines if a message must be deleted or not if the message handling throws an exception + By default this value is set to 'true' which means that the message is deleted to + avoid poison messages. If the 'error-channel' is specified and the direct error flow + doesn't rethrow a 'MessagingException' this attribute doesn't have an effect. If this is + to 'false' it is a responsibility of the error flow to remove the message from + AmazonSQS. + + + + + + + + + + + + Base type for the 'sqs-message-driven-channel-adapter' and 'sqs-outbound-channel-adapter' elements. + + + + + + The 'com.amazonaws.services.sqs.AmazonSQS' bean reference. + Must be 'AmazonSQSAsync' for the 'sqs-message-driven-channel-adapter'. + + + + + + + + + + + + The 'org.springframework.cloud.aws.core.env.ResourceIdResolver' bean reference. + + + + + + + + + + + + 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 new file mode 100644 index 0000000..6761df8 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests-context.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests.java b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests.java new file mode 100644 index 0000000..9fa7286 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageDrivenChannelAdapterParserTests.java @@ -0,0 +1,101 @@ +/* + * 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.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.cloud.aws.core.env.ResourceIdResolver; +import org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer; +import org.springframework.core.task.TaskExecutor; +import org.springframework.integration.aws.inbound.SqsMessageDrivenChannelAdapter; +import org.springframework.integration.channel.NullChannel; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.core.DestinationResolver; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.amazonaws.services.sqs.AmazonSQS; + +/** + * @author Artem Bilan + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class SqsMessageDrivenChannelAdapterParserTests { + + @Autowired + private AmazonSQS amazonSqs; + + @Autowired + private ResourceIdResolver resourceIdResolver; + + @Autowired + private DestinationResolver destinationResolver; + + @Autowired + private TaskExecutor taskExecutor; + + @Autowired + private MessageChannel errorChannel; + + @Autowired + private NullChannel nullChannel; + + @Autowired + private SqsMessageDrivenChannelAdapter sqsMessageDrivenChannelAdapter; + + + @Test + public void testSqsMessageDrivenChannelAdapterParser() { + SimpleMessageListenerContainer listenerContainer = + TestUtils.getPropertyValue(this.sqsMessageDrivenChannelAdapter, "listenerContainer", + SimpleMessageListenerContainer.class); + assertSame(this.amazonSqs, TestUtils.getPropertyValue(listenerContainer, "amazonSqs")); + assertSame(this.resourceIdResolver, TestUtils.getPropertyValue(listenerContainer, "resourceIdResolver")); + assertSame(this.taskExecutor, TestUtils.getPropertyValue(listenerContainer, "taskExecutor")); + assertSame(this.destinationResolver, TestUtils.getPropertyValue(listenerContainer, "destinationResolver")); + assertFalse(listenerContainer.isRunning()); + assertEquals(5, TestUtils.getPropertyValue(listenerContainer, "maxNumberOfMessages")); + assertEquals(200, TestUtils.getPropertyValue(listenerContainer, "visibilityTimeout")); + assertEquals(40, TestUtils.getPropertyValue(listenerContainer, "waitTimeOut")); + assertFalse(TestUtils.getPropertyValue(listenerContainer, "deleteMessageOnException", Boolean.class)); + + @SuppressWarnings("rawtypes") + Set queues = TestUtils.getPropertyValue(listenerContainer, "queues", Set.class); + assertTrue(queues.contains("foo")); + assertTrue(queues.contains("bar")); + + assertEquals(100, this.sqsMessageDrivenChannelAdapter.getPhase()); + assertFalse(this.sqsMessageDrivenChannelAdapter.isAutoStartup()); + assertFalse(this.sqsMessageDrivenChannelAdapter.isRunning()); + assertSame(this.errorChannel, TestUtils.getPropertyValue(this.sqsMessageDrivenChannelAdapter, "outputChannel")); + assertSame(this.nullChannel, TestUtils.getPropertyValue(this.sqsMessageDrivenChannelAdapter, "errorChannel")); + assertEquals(2000L, TestUtils.getPropertyValue(this.sqsMessageDrivenChannelAdapter, + "messagingTemplate.sendTimeout")); + } + +} 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 new file mode 100644 index 0000000..cc69062 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests-context.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests.java b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests.java new file mode 100644 index 0000000..889a5d3 --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/config/xml/SqsMessageHandlerParserTests.java @@ -0,0 +1,79 @@ +/* + * 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.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +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.core.env.ResourceIdResolver; +import org.springframework.integration.channel.NullChannel; +import org.springframework.integration.endpoint.EventDrivenConsumer; +import org.springframework.integration.endpoint.PollingConsumer; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.amazonaws.services.sqs.AmazonSQS; + +/** + * @author Artem Bilan + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class SqsMessageHandlerParserTests { + + @Autowired + private AmazonSQS amazonSqs; + + @Autowired + private ResourceIdResolver resourceIdResolver; + + @Autowired + private MessageChannel errorChannel; + + @Autowired + private EventDrivenConsumer sqsOutboundChannelAdapter; + + @Autowired + @Qualifier("sqsOutboundChannelAdapter.handler") + private MessageHandler sqsOutboundChannelAdapterHandler; + + @Test + public void testSqsMessageHandlerParser() { + assertSame(this.amazonSqs, + TestUtils.getPropertyValue(this.sqsOutboundChannelAdapterHandler, "template.amazonSqs")); + assertSame(this.resourceIdResolver, TestUtils.getPropertyValue(this.sqsOutboundChannelAdapterHandler, + "template.destinationResolver.targetDestinationResolver.resourceIdResolver")); + assertEquals("foo", TestUtils.getPropertyValue(this.sqsOutboundChannelAdapterHandler, + "queueExpression.literalValue")); + assertEquals(100, this.sqsOutboundChannelAdapter.getPhase()); + assertFalse(this.sqsOutboundChannelAdapter.isAutoStartup()); + assertFalse(this.sqsOutboundChannelAdapter.isRunning()); + assertSame(this.errorChannel, TestUtils.getPropertyValue(this.sqsOutboundChannelAdapter, "inputChannel")); + assertSame(this.sqsOutboundChannelAdapterHandler, + TestUtils.getPropertyValue(this.sqsOutboundChannelAdapter, "handler")); + } + +} diff --git a/src/test/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapterTests.java b/src/test/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapterTests.java new file mode 100644 index 0000000..cf77d5d --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapterTests.java @@ -0,0 +1,110 @@ +/* + * 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.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.aws.support.AwsHeaders; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.core.MessageProducer; +import org.springframework.messaging.PollableChannel; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.amazonaws.services.sqs.AmazonSQSAsync; +import com.amazonaws.services.sqs.model.GetQueueAttributesRequest; +import com.amazonaws.services.sqs.model.GetQueueAttributesResult; +import com.amazonaws.services.sqs.model.GetQueueUrlRequest; +import com.amazonaws.services.sqs.model.GetQueueUrlResult; +import com.amazonaws.services.sqs.model.Message; +import com.amazonaws.services.sqs.model.ReceiveMessageRequest; +import com.amazonaws.services.sqs.model.ReceiveMessageResult; + +/** + * @author Artem Bilan + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class SqsMessageDrivenChannelAdapterTests { + + @Autowired + private PollableChannel inputChannel; + + @Test + public void testSqsMessageDrivenChannelAdapter() { + org.springframework.messaging.Message receive = this.inputChannel.receive(1000); + assertNotNull(receive); + assertThat((String) receive.getPayload(), Matchers.isOneOf("messageContent", "messageContent2")); + assertEquals("testQueue", receive.getHeaders().get(AwsHeaders.QUEUE)); + receive = this.inputChannel.receive(1000); + assertNotNull(receive); + assertThat((String) receive.getPayload(), Matchers.isOneOf("messageContent", "messageContent2")); + assertEquals("testQueue", receive.getHeaders().get(AwsHeaders.QUEUE)); + } + + @Configuration + @EnableIntegration + public static class ContextConfiguration { + + @Bean + public AmazonSQSAsync amazonSqs() { + AmazonSQSAsync sqs = mock(AmazonSQSAsync.class); + when(sqs.getQueueUrl(new GetQueueUrlRequest("testQueue"))).thenReturn(new GetQueueUrlResult(). + withQueueUrl("http://testQueue.amazonaws.com")); + + + when(sqs.receiveMessage(new ReceiveMessageRequest("http://testQueue.amazonaws.com") + .withAttributeNames("All") + .withMessageAttributeNames("All") + .withMaxNumberOfMessages(10))) + .thenReturn(new ReceiveMessageResult() + .withMessages(new Message().withBody("messageContent"), + new Message().withBody("messageContent2"))) + .thenReturn(new ReceiveMessageResult()); + when(sqs.getQueueAttributes(any(GetQueueAttributesRequest.class))) + .thenReturn(new GetQueueAttributesResult()); + return sqs; + } + + @Bean + public PollableChannel inputChannel() { + return new QueueChannel(); + } + + @Bean + public MessageProducer sqsMessageDrivenChannelAdapter() { + SqsMessageDrivenChannelAdapter adapter = new SqsMessageDrivenChannelAdapter(amazonSqs(), "testQueue"); + adapter.setOutputChannel(inputChannel()); + return adapter; + } + + } + +} diff --git a/src/test/java/org/springframework/integration/aws/outbound/SqsMessageHandlerTests.java b/src/test/java/org/springframework/integration/aws/outbound/SqsMessageHandlerTests.java new file mode 100644 index 0000000..db3044d --- /dev/null +++ b/src/test/java/org/springframework/integration/aws/outbound/SqsMessageHandlerTests.java @@ -0,0 +1,134 @@ +/* + * 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.outbound; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.aws.support.AwsHeaders; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.GetQueueUrlRequest; +import com.amazonaws.services.sqs.model.GetQueueUrlResult; +import com.amazonaws.services.sqs.model.SendMessageRequest; + +/** + * @author Artem Bilan + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class SqsMessageHandlerTests { + + @Autowired + private AmazonSQS amazonSqs; + + @Autowired + private MessageChannel sqsSendChannel; + + @Autowired + private SqsMessageHandler sqsMessageHandler; + + @Test + public void testSqsMessageHandler() { + Message message = MessageBuilder.withPayload("message").build(); + try { + this.sqsSendChannel.send(message); + } + catch (Exception e) { + assertThat(e, instanceOf(MessageHandlingException.class)); + assertThat(e.getCause(), instanceOf(IllegalStateException.class)); + } + + this.sqsMessageHandler.setQueue("foo"); + this.sqsSendChannel.send(message); + ArgumentCaptor sendMessageRequestArgumentCaptor = + ArgumentCaptor.forClass(SendMessageRequest.class); + verify(this.amazonSqs).sendMessage(sendMessageRequestArgumentCaptor.capture()); + assertEquals("http://queue-url.com/foo", sendMessageRequestArgumentCaptor.getValue().getQueueUrl()); + + message = MessageBuilder.withPayload("message").setHeader(AwsHeaders.QUEUE, "bar").build(); + this.sqsSendChannel.send(message); + verify(this.amazonSqs, times(2)).sendMessage(sendMessageRequestArgumentCaptor.capture()); + assertEquals("http://queue-url.com/bar", sendMessageRequestArgumentCaptor.getValue().getQueueUrl()); + + SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); + Expression expression = spelExpressionParser.parseExpression("headers.foo"); + this.sqsMessageHandler.setQueueExpression(expression); + message = MessageBuilder.withPayload("message").setHeader("foo", "baz").build(); + this.sqsSendChannel.send(message); + verify(this.amazonSqs, times(3)).sendMessage(sendMessageRequestArgumentCaptor.capture()); + assertEquals("http://queue-url.com/baz", sendMessageRequestArgumentCaptor.getValue().getQueueUrl()); + } + + @Configuration + @EnableIntegration + public static class ContextConfiguration { + + @Bean + public AmazonSQS amazonSqs() { + AmazonSQS amazonSqs = mock(AmazonSQS.class); + + doAnswer(new Answer() { + + @Override + public GetQueueUrlResult answer(InvocationOnMock invocation) throws Throwable { + GetQueueUrlRequest getQueueUrlRequest = (GetQueueUrlRequest) invocation.getArguments()[0]; + GetQueueUrlResult queueUrl = new GetQueueUrlResult(); + queueUrl.setQueueUrl("http://queue-url.com/" + getQueueUrlRequest.getQueueName()); + return queueUrl; + } + + }).when(amazonSqs).getQueueUrl(any(GetQueueUrlRequest.class)); + + return amazonSqs; + } + + @Bean + @ServiceActivator(inputChannel = "sqsSendChannel") + public MessageHandler sqsMessageHandler() { + return new SqsMessageHandler(amazonSqs()); + } + + } + +} diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties index 8e9bc68..bf146e4 100644 --- a/src/test/resources/log4j.properties +++ b/src/test/resources/log4j.properties @@ -1,8 +1,8 @@ -log4j.rootCategory=INFO, stdout +log4j.rootCategory=WARN, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n log4j.category.org.springframework=WARN -log4j.category.org.springframework.integration.aws=DEBUG +log4j.category.org.springframework.integration.aws=WARN