├── .github ├── dco.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── pr-build.yml │ ├── ci-snapshot.yml │ ├── verify-staged-artifacts.yml │ └── release.yml └── PULL_REQUEST_TEMPLATE.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── src ├── main │ └── java │ │ └── org │ │ └── springframework │ │ └── integration │ │ └── aws │ │ ├── event │ │ ├── package-info.java │ │ ├── KinesisIntegrationEvent.java │ │ ├── AwsIntegrationEvent.java │ │ └── KinesisShardEndedEvent.java │ │ ├── lock │ │ └── package-info.java │ │ ├── metadata │ │ └── package-info.java │ │ ├── support │ │ ├── package-info.java │ │ ├── filters │ │ │ ├── package-info.java │ │ │ ├── S3SimplePatternFileListFilter.java │ │ │ ├── S3RegexPatternFileListFilter.java │ │ │ └── S3PersistentAcceptOnceFileListFilter.java │ │ ├── AwsRequestFailureException.java │ │ ├── KplBackpressureException.java │ │ ├── SnsHeaderMapper.java │ │ ├── SqsHeaderMapper.java │ │ ├── SnsAsyncTopicArnResolver.java │ │ ├── S3SessionFactory.java │ │ ├── UserRecordResponse.java │ │ ├── S3RemoteFileTemplate.java │ │ ├── SnsBodyBuilder.java │ │ ├── S3FileInfo.java │ │ ├── AwsHeaders.java │ │ └── AbstractMessageAttributesHeaderMapper.java │ │ ├── inbound │ │ ├── kinesis │ │ │ ├── package-info.java │ │ │ ├── ListenerMode.java │ │ │ ├── CheckpointMode.java │ │ │ ├── Checkpointer.java │ │ │ ├── KinesisMessageHeaderErrorMessageStrategy.java │ │ │ ├── ShardCheckpointer.java │ │ │ └── KinesisShardOffset.java │ │ ├── package-info.java │ │ ├── S3InboundFileSynchronizingMessageSource.java │ │ ├── S3StreamingMessageSource.java │ │ ├── S3InboundFileSynchronizer.java │ │ ├── SqsMessageDrivenChannelAdapter.java │ │ └── SnsInboundChannelAdapter.java │ │ └── outbound │ │ ├── package-info.java │ │ ├── ConvertingFromMessageConverter.java │ │ └── AbstractAwsMessageHandler.java ├── checkstyle │ ├── checkstyle-suppressions.xml │ ├── checkstyle-header.txt │ └── checkstyle.xml ├── test │ ├── resources │ │ └── log4j2-test.xml │ └── java │ │ └── org │ │ └── springframework │ │ └── integration │ │ └── aws │ │ ├── inbound │ │ ├── subscriptionConfirmation.json │ │ ├── unsubscribeConfirmation.json │ │ ├── notificationMessage.json │ │ ├── SqsMessageDrivenChannelAdapterTests.java │ │ ├── S3StreamingChannelAdapterTests.java │ │ ├── S3InboundChannelAdapterTests.java │ │ └── SnsInboundChannelAdapterTests.java │ │ ├── outbound │ │ ├── SnsBodyBuilderTests.java │ │ ├── SnsMessageHandlerTests.java │ │ ├── KinesisMessageHandlerTests.java │ │ ├── SqsMessageHandlerTests.java │ │ ├── KplMessageHandlerTests.java │ │ └── KinesisProducingMessageHandlerTests.java │ │ ├── LocalstackContainerTest.java │ │ ├── metadata │ │ └── DynamoDbMetadataStoreTests.java │ │ ├── kinesis │ │ ├── KclMessageDrivenChannelAdapterMultiStreamTests.java │ │ └── KplKclIntegrationTests.java │ │ └── leader │ │ └── DynamoDbLockRegistryLeaderInitiatorTests.java ├── api │ └── overview.html └── dist │ └── notice.txt ├── settings.gradle ├── publish-maven.gradle ├── CODE_OF_CONDUCT.adoc └── gradlew.bat /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=3.0.11-SNAPSHOT 2 | org.gradle.daemon=true 3 | org.gradle.caching=true 4 | org.gradle.parallel=true 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-projects/spring-integration-aws/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .settings 3 | .springBeans 4 | .classpath 5 | .project 6 | /.idea 7 | /.gradle 8 | /build 9 | /*.iml 10 | /*.ipr 11 | /*.iws 12 | bin/ 13 | out 14 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/event/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides events for AWS Channel Adapters. 3 | */ 4 | package org.springframework.integration.aws.event; 5 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/lock/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides classes supporting lock registry. 3 | */ 4 | package org.springframework.integration.aws.lock; 5 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/metadata/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides classes supporting metadata stores. 3 | */ 4 | package org.springframework.integration.aws.metadata; 5 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides support classes for AWS components. 3 | */ 4 | package org.springframework.integration.aws.support; 5 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/kinesis/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides components for AWS Kinesis. 3 | */ 4 | package org.springframework.integration.aws.inbound.kinesis; 5 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides classes which represent inbound AWS components. 3 | */ 4 | package org.springframework.integration.aws.inbound; 5 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/outbound/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides classes which represent outbound AWS components. 3 | */ 4 | package org.springframework.integration.aws.outbound; 5 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/filters/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides classes supporting FTP file filtering. 3 | */ 4 | package org.springframework.integration.aws.support.filters; 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | plugins { 9 | id 'io.spring.develocity.conventions' version '0.0.23' 10 | } 11 | 12 | rootProject.name = 'spring-integration-aws' 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://stackoverflow.com/questions/tagged/spring-integration 5 | about: Please ask and answer questions on StackOverflow with the tag spring-integration 6 | -------------------------------------------------------------------------------- /.github/workflows/pr-build.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-pull-request: 10 | uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@main 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /src/checkstyle/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/ci-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: CI SNAPSHOT 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - '*.x' 9 | 10 | jobs: 11 | build-snapshot: 12 | uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@main 13 | with: 14 | gradleTasks: dist 15 | secrets: 16 | DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 17 | ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} 18 | ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'status: waiting-for-triage, type: enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Expected Behavior** 11 | 12 | 13 | 14 | **Current Behavior** 15 | 16 | 17 | 18 | **Context** 19 | 20 | 26 | -------------------------------------------------------------------------------- /src/test/resources/log4j2-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/checkstyle/checkstyle-header.txt: -------------------------------------------------------------------------------- 1 | ^\Q/*\E$ 2 | ^\Q * Copyright \E20\d\d\Q-present the original author or authors.\E$ 3 | ^\Q *\E$ 4 | ^\Q * Licensed under the Apache License, Version 2.0 (the "License");\E$ 5 | ^\Q * you may not use this file except in compliance with the License.\E$ 6 | ^\Q * You may obtain a copy of the License at\E$ 7 | ^\Q *\E$ 8 | ^\Q * https://www.apache.org/licenses/LICENSE-2.0\E$ 9 | ^\Q *\E$ 10 | ^\Q * Unless required by applicable law or agreed to in writing, software\E$ 11 | ^\Q * distributed under the License is distributed on an "AS IS" BASIS,\E$ 12 | ^\Q * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\E$ 13 | ^\Q * See the License for the specific language governing permissions and\E$ 14 | ^\Q * limitations under the License.\E$ 15 | ^\Q */\E$ 16 | ^$ 17 | ^.*$ 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'type: bug, status: waiting-for-triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **In what version(s) of Spring Integration AWS are you seeing this issue?** 11 | 12 | For example: 13 | 14 | 2.5.3 15 | 16 | Between 2.5.0 and 3.0.0 17 | 18 | **Describe the bug** 19 | 20 | A clear and concise description of what the bug is. 21 | 22 | **To Reproduce** 23 | 24 | Steps to reproduce the behavior. 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Sample** 31 | 32 | A link to a GitHub repository with a [minimal, reproducible sample](https://stackoverflow.com/help/minimal-reproducible-example). 33 | 34 | Reports that include a sample will take priority over reports that do not. 35 | At times, we may require a sample, so it is good to try and include a sample up front. 36 | -------------------------------------------------------------------------------- /src/api/overview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | This document is the API specification for Spring Integration AWS Support 4 |
5 |
6 |

7 | For further API reference and developer documentation, see the 8 | Spring 9 | Integration reference documentation. 10 | That documentation contains more detailed, developer-targeted 11 | descriptions, with conceptual overviews, definitions of terms, 12 | workarounds, and working code examples. 13 |

14 | 15 |

16 | Spring 17 | Integration main project page 18 |

19 | 20 |

21 | If you are interested in commercial training, consultancy, and 22 | support for Spring Integration, please visit 23 | https://spring.io 24 |

25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/verify-staged-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Verify Staged Artifacts 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseVersion: 7 | description: 'Release version like 3.0.0-M1, 3.1.0-RC1, 3.2.0 etc.' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | verify-staged-with-jfrog: 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - uses: jfrog/setup-jfrog-cli@v4 17 | env: 18 | JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} 19 | 20 | - name: Download Artifact from Staging Repo 21 | run: | 22 | fileToDownload=org/springframework/integration/spring-integration-aws/${{ inputs.releaseVersion }}/spring-integration-aws-${{ inputs.releaseVersion }}.jar 23 | jfrog rt download libs-staging-local/$fileToDownload 24 | if [ ! -f $fileToDownload ] 25 | then 26 | echo "::error title=No staged artifact::No spring-integration-aws-${{ inputs.releaseVersion }}.jar in staging repository" 27 | exit 1 28 | fi -------------------------------------------------------------------------------- /src/dist/notice.txt: -------------------------------------------------------------------------------- 1 | ======================================================================== 2 | == NOTICE file corresponding to section 4 d of the Apache License, == 3 | == Version 2.0, in this case for the Spring Integration distribution. == 4 | ======================================================================== 5 | 6 | This product includes software developed by 7 | the Apache Software Foundation (https://www.apache.org). 8 | 9 | The end-user documentation included with a redistribution, if any, 10 | must include the following acknowledgement: 11 | 12 | "This product includes software developed by the Spring Framework 13 | Project (https://www.springframework.org)." 14 | 15 | Alternatively, this acknowledgement may appear in the software itself, 16 | if and wherever such third-party acknowledgements normally appear. 17 | 18 | The names "Spring", "Spring Framework", and "Spring Integration" must 19 | not be used to endorse or promote products derived from this software 20 | without prior written permission. For written permission, please contact 21 | enquiries@springsource.com. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | run-name: Release current version for branch ${{ github.ref_name }} 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | actions: write 12 | contents: write 13 | issues: write 14 | 15 | uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main 16 | with: 17 | buildToolArgs: dist 18 | secrets: 19 | GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} 20 | DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} 21 | JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} 22 | ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} 23 | ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} 24 | CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }} 25 | CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} 26 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 27 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 28 | SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/inbound/subscriptionConfirmation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "SubscriptionConfirmation", 3 | "MessageId": "e267b24c-5532-472f-889d-c2cdd2143bbc", 4 | "Token": "111", 5 | "TopicArn": "arn:aws:sns:eu-west-1:111111111111:mySampleTopic", 6 | "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.", 7 | "SubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:eu-west-1:111111111111:mySampleTopic&Token=111", 8 | "Timestamp": "2014-06-28T10:22:18.086Z", 9 | "SignatureVersion": "1", 10 | "Signature": "JLdRUR+uhP4cyVW6bRuUSAkUosFMJyO7g7WCAwEUJoB4y8vQE1uDUWGpbQSEbruVTjPEM8hFsf4/95NftfM0W5IgND1uSnv4P/4AYyL+q0bLOJlquzXrw4w2NX3QShS3y+r/gXzo7p/UP4NOr35MGCEGPqHAEe1Coc5S0eaP3JvKU6xY1tcop6ze2RNHTwzhM43dda2bnjPYogAJzA5uHfmSjs3cMVvPCckj3zdLyvxISp+RgrogdvlNyu9ycND1SxagmbzjkBaqvF/4aiSYFxsEXX4e9zuNuHGmXGWgm1ppYUGLSPPJruCsPUa7Ii1mYvpX7SezuFZlAAXXBk0mHg==", 11 | "SigningCertURL": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-e372f8ca30337fdb084e8ac449342c77.pem" 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/inbound/unsubscribeConfirmation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "UnsubscribeConfirmation", 3 | "MessageId": "7b9a6321-45d5-461a-bc35-9c2d18ed9dbe", 4 | "Token": "233", 5 | "TopicArn": "arn:aws:sns:eu-west-1:111111111111:mySampleTopic", 6 | "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.", 7 | "SubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:eu-west-1:721324560415:mySampleTopic&Token=233", 8 | "Timestamp": "2014-06-28T19:33:00.497Z", 9 | "SignatureVersion": "1", 10 | "Signature": "EAb0k1XoD2h+u4j2GB42wEFCVUFKEaS/2W+p+3wK1GfZDZn4LeDLbkxJ1LYF/A1CYL+sCJ4ZmLFI1axm0V/p+fESkNbKQoQotMgma+PtA6KnmRrKEU8O6nUELqeVPWFAoQ9ZsW9FCAXVDXoPxiqHNTH+tC7mzAsvajyp4aTm/POqkRKBl+A/7dHUqfHGup/FJhLNgTAciBZSloa5EuBKxInJQfoZjy3DU8qKXXhmKKRdyVwOGEuReo/njy4c3Phtn0+logu2PZUKqkGTuJZVbapHmcTq+0MqIh05sevLDmTEfBlsmNThhWIyCza/t68RRlqm9cRLjINSWfrv1Xkrpw==", 11 | "SigningCertURL": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-e372f8ca30337fdb084e8ac449342c77.pem" 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/event/KinesisIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.event; 18 | 19 | /** 20 | * A common event type for AWS Kinesis Channel Adapters. 21 | * 22 | * @author Artem Bilan 23 | * 24 | * @since 2.3 25 | */ 26 | @SuppressWarnings("serial") 27 | public abstract class KinesisIntegrationEvent extends AwsIntegrationEvent { 28 | 29 | public KinesisIntegrationEvent(Object source) { 30 | super(source); 31 | } 32 | 33 | public KinesisIntegrationEvent(Object source, Throwable cause) { 34 | super(source, cause); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/kinesis/ListenerMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound.kinesis; 18 | 19 | import org.springframework.messaging.Message; 20 | 21 | /** 22 | * The listener mode, record or batch. 23 | * 24 | * @author Artem Bilan 25 | * 26 | * @since 1.1 27 | */ 28 | public enum ListenerMode { 29 | 30 | /** 31 | * Each {@link Message} will be converted from a single {@code Record}. 32 | */ 33 | record, 34 | 35 | /** 36 | * Each {@link Message} will contain {@code List} ( if not empty) of converted or raw 37 | * {@code Record}s. 38 | */ 39 | batch 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/event/AwsIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.event; 18 | 19 | import org.springframework.integration.events.IntegrationEvent; 20 | 21 | /** 22 | * A common event type for AWS Channel Adapters. 23 | * 24 | * @author Artem Bilan 25 | * 26 | * @since 2.3 27 | */ 28 | @SuppressWarnings("serial") 29 | public abstract class AwsIntegrationEvent extends IntegrationEvent { 30 | 31 | public AwsIntegrationEvent(Object source) { 32 | super(source); 33 | } 34 | 35 | public AwsIntegrationEvent(Object source, Throwable cause) { 36 | super(source, cause); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/event/KinesisShardEndedEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.event; 18 | 19 | /** 20 | * An event emitted when shard is closed for consumption. 21 | * 22 | * @author Artem Bilan 23 | * 24 | * @since 2.3 25 | */ 26 | @SuppressWarnings("serial") 27 | public class KinesisShardEndedEvent extends KinesisIntegrationEvent { 28 | 29 | private final String shardKey; 30 | 31 | public KinesisShardEndedEvent(Object source, String shardKey) { 32 | super(source); 33 | this.shardKey = shardKey; 34 | } 35 | 36 | public String getShardKey() { 37 | return this.shardKey; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/inbound/notificationMessage.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Notification", 3 | "MessageId": "f2c15fec-c617-5b08-b54d-13c4099fec60", 4 | "TopicArn": "arn:aws:sns:eu-west-1:111111111111:mySampleTopic", 5 | "Subject": "foo", 6 | "Message": "bar", 7 | "Timestamp": "2014-06-28T14:12:24.418Z", 8 | "SignatureVersion": "1", 9 | "Signature": "XDvKSAnhxECrAmyIrs0Dsfbp/tnKD1IvoOOYTU28FtbUoxr/CgziuW87yZwTuSNNbHJbdD3BEjHS0vKewm0xBeQ0PToDkgtoORXo5RWnmShDQ2nhkthFhZnNulKtmFtRogjBtCwbz8sPnbOCSk21ruyXNdV2RUbdDalndAW002CWEQmYMxFSN6OXUtMueuT610aX+tqeYP4Z6+8WTWLWjAuVyy7rOI6KHYBcVDhKtskvTOPZ4tiVohtQdQbO2Gjuh1vblRzzwMkfaoFTSWImd4pFXxEsv/fq9aGIlqq9xEryJ0w2huFwI5gxyhvGt0RnTd9YvmAEC+WzdJDOqaDNxg==", 10 | "SigningCertURL": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-e372f8ca30337fdb084e8ac449342c77.pem", 11 | "UnsubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:721324560415:mySampleTopic:9859a6c9-6083-4690-ab02-d1aead3442df", 12 | "MessageAttributes": { 13 | "AWS.SNS.MOBILE.MPNS.Type": { 14 | "Type": "String", 15 | "Value": "token" 16 | }, 17 | "AWS.SNS.MOBILE.WNS.Type": { 18 | "Type": "String", 19 | "Value": "wns/badge" 20 | }, 21 | "AWS.SNS.MOBILE.MPNS.NotificationClass": { 22 | "Type": "String", 23 | "Value": "realtime" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/kinesis/CheckpointMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound.kinesis; 18 | 19 | /** 20 | * The listener mode, record or batch. 21 | * 22 | * @author Artem Bilan 23 | * @author Hervé Fortin 24 | * 25 | * @since 1.1 26 | */ 27 | public enum CheckpointMode { 28 | 29 | /** 30 | * Checkpoint after each processed record. Makes sense only if 31 | * {@link ListenerMode#record} is used. 32 | */ 33 | record, 34 | 35 | /** 36 | * Checkpoint after each processed batch of records. 37 | */ 38 | batch, 39 | 40 | /** 41 | * Checkpoint on demand via provided to the message {@link Checkpointer} callback. 42 | */ 43 | manual, 44 | 45 | /** 46 | * Checkpoint at fixed time intervals. 47 | * @since 2.2.0 48 | */ 49 | periodic 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/kinesis/Checkpointer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound.kinesis; 18 | 19 | /** 20 | * A callback for target record process to perform checkpoint on the related shard. 21 | * 22 | * @author Artem Bilan 23 | * @since 1.1 24 | */ 25 | public interface Checkpointer { 26 | 27 | /** 28 | * Checkpoint the currently held sequence number if it is bigger than already stored. 29 | * @return true if checkpoint performed; false otherwise. 30 | */ 31 | boolean checkpoint(); 32 | 33 | /** 34 | * Checkpoint the provided sequence number, if it is bigger than already stored. 35 | * @param sequenceNumber the sequence number to checkpoint. 36 | * @return true if checkpoint performed; false otherwise. 37 | */ 38 | boolean checkpoint(String sequenceNumber); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/filters/S3SimplePatternFileListFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support.filters; 18 | 19 | import software.amazon.awssdk.services.s3.model.S3Object; 20 | 21 | import org.springframework.integration.file.filters.AbstractSimplePatternFileListFilter; 22 | 23 | /** 24 | * Implementation of {@link AbstractSimplePatternFileListFilter} for Amazon S3. 25 | * 26 | * @author Artem Bilan 27 | */ 28 | public class S3SimplePatternFileListFilter extends AbstractSimplePatternFileListFilter { 29 | 30 | public S3SimplePatternFileListFilter(String pattern) { 31 | super(pattern); 32 | } 33 | 34 | @Override 35 | protected String getFilename(S3Object file) { 36 | return (file != null) ? file.key() : null; 37 | } 38 | 39 | @Override 40 | protected boolean isDirectory(S3Object file) { 41 | return false; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/filters/S3RegexPatternFileListFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support.filters; 18 | 19 | import java.util.regex.Pattern; 20 | 21 | import software.amazon.awssdk.services.s3.model.S3Object; 22 | 23 | import org.springframework.integration.file.filters.AbstractRegexPatternFileListFilter; 24 | 25 | /** 26 | * Implementation of {@link AbstractRegexPatternFileListFilter} for Amazon S3. 27 | * 28 | * @author Artem Bilan 29 | */ 30 | public class S3RegexPatternFileListFilter extends AbstractRegexPatternFileListFilter { 31 | 32 | public S3RegexPatternFileListFilter(String pattern) { 33 | super(pattern); 34 | } 35 | 36 | public S3RegexPatternFileListFilter(Pattern pattern) { 37 | super(pattern); 38 | } 39 | 40 | @Override 41 | protected String getFilename(S3Object file) { 42 | return (file != null) ? file.key() : null; 43 | } 44 | 45 | @Override 46 | protected boolean isDirectory(S3Object file) { 47 | return false; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/AwsRequestFailureException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import software.amazon.awssdk.awscore.AwsRequest; 20 | 21 | import org.springframework.messaging.Message; 22 | import org.springframework.messaging.MessagingException; 23 | 24 | /** 25 | * An exception that is the payload of an {@code ErrorMessage} when a send fails. 26 | * 27 | * @author Jacob Severson 28 | * @author Artem Bilan 29 | * 30 | * @since 1.1 31 | */ 32 | public class AwsRequestFailureException extends MessagingException { 33 | 34 | private static final long serialVersionUID = 1L; 35 | 36 | private final transient AwsRequest request; 37 | 38 | public AwsRequestFailureException(Message message, AwsRequest request, Throwable cause) { 39 | super(message, cause); 40 | this.request = request; 41 | } 42 | 43 | public AwsRequest getRequest() { 44 | return this.request; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return super.toString() + " [request=" + this.request + "]"; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/KplBackpressureException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.io.Serial; 20 | 21 | import com.amazonaws.services.kinesis.producer.UserRecord; 22 | 23 | /** 24 | * An exception triggered from the {@link org.springframework.integration.aws.outbound.KplMessageHandler} 25 | * while sending records to Kinesis when maximum number of records in flight exceeds the backpressure threshold. 26 | * 27 | * @author Siddharth Jain 28 | * @author Artem Bilan 29 | * 30 | * @since 3.0.9 31 | */ 32 | public class KplBackpressureException extends RuntimeException { 33 | 34 | @Serial 35 | private static final long serialVersionUID = 1L; 36 | 37 | private final transient UserRecord userRecord; 38 | 39 | public KplBackpressureException(String message, UserRecord userRecord) { 40 | super(message); 41 | this.userRecord = userRecord; 42 | } 43 | 44 | /** 45 | * Get the {@link UserRecord} when this exception has been thrown. 46 | * @return the {@link UserRecord} when this exception has been thrown. 47 | */ 48 | public UserRecord getUserRecord() { 49 | return this.userRecord; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/outbound/ConvertingFromMessageConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import org.springframework.core.convert.converter.Converter; 20 | import org.springframework.messaging.Message; 21 | import org.springframework.messaging.MessageHeaders; 22 | import org.springframework.messaging.converter.MessageConverter; 23 | import org.springframework.util.Assert; 24 | 25 | /** 26 | * A simple {@link MessageConverter} that delegates to a {@link Converter}. 27 | * 28 | * @author Artem Bilan 29 | * 30 | * @since 2.3 31 | */ 32 | class ConvertingFromMessageConverter implements MessageConverter { 33 | 34 | private final Converter delegate; 35 | 36 | ConvertingFromMessageConverter(Converter delegate) { 37 | Assert.notNull(delegate, "'delegate' must not be null"); 38 | this.delegate = delegate; 39 | } 40 | 41 | @Override 42 | public Object fromMessage(Message message, Class targetClass) { 43 | return this.delegate.convert(message.getPayload()); 44 | } 45 | 46 | @Override 47 | public Message toMessage(Object payload, MessageHeaders headers) { 48 | throw new UnsupportedOperationException(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/S3InboundFileSynchronizingMessageSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.io.File; 20 | import java.util.Comparator; 21 | 22 | import software.amazon.awssdk.services.s3.model.S3Object; 23 | 24 | import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizer; 25 | import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizingMessageSource; 26 | 27 | /** 28 | * A {@link org.springframework.integration.core.MessageSource} implementation for the 29 | * Amazon S3. 30 | * 31 | * @author Artem Bilan 32 | */ 33 | public class S3InboundFileSynchronizingMessageSource 34 | extends AbstractInboundFileSynchronizingMessageSource { 35 | 36 | public S3InboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer synchronizer) { 37 | super(synchronizer); 38 | } 39 | 40 | public S3InboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer synchronizer, 41 | Comparator comparator) { 42 | 43 | super(synchronizer, comparator); 44 | } 45 | 46 | public String getComponentType() { 47 | return "aws:s3-inbound-channel-adapter"; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /publish-maven.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | 3 | publishing { 4 | publications { 5 | mavenJava(MavenPublication) { 6 | suppressAllPomMetadataWarnings() 7 | from components.java 8 | artifact docsZip 9 | artifact distZip 10 | pom { 11 | afterEvaluate { 12 | name = project.description 13 | description = project.description 14 | } 15 | url = linkScmUrl 16 | organization { 17 | name = 'Spring IO' 18 | url = 'https://spring.io/projects/spring-integration' 19 | } 20 | licenses { 21 | license { 22 | name = 'Apache License, Version 2.0' 23 | url = 'https://www.apache.org/licenses/LICENSE-2.0' 24 | distribution = 'repo' 25 | } 26 | } 27 | scm { 28 | url = linkScmUrl 29 | connection = linkScmConnection 30 | developerConnection = linkScmDevConnection 31 | } 32 | developers { 33 | developer { 34 | id = 'artembilan' 35 | name = 'Artem Bilan' 36 | email = 'artem.bilan@broadcom.com' 37 | roles = ['project lead'] 38 | } 39 | } 40 | issueManagement { 41 | system = 'GitHub' 42 | url = linkIssue 43 | } 44 | } 45 | versionMapping { 46 | usage('java-api') { 47 | fromResolutionResult() 48 | } 49 | usage('java-runtime') { 50 | fromResolutionResult() 51 | } 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/SnsHeaderMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | import software.amazon.awssdk.core.SdkBytes; 22 | import software.amazon.awssdk.services.sns.model.MessageAttributeValue; 23 | 24 | /** 25 | * The {@link AbstractMessageAttributesHeaderMapper} implementation for the mapping from 26 | * headers to SNS message attributes. 27 | *

28 | * On the Inbound side, the SNS message is fully mapped from the JSON to the message 29 | * payload. Only important HTTP headers are mapped to the message headers. 30 | * 31 | * @author Artem Bilan 32 | * 33 | * @since 2.0 34 | */ 35 | public class SnsHeaderMapper extends AbstractMessageAttributesHeaderMapper { 36 | 37 | @Override 38 | protected MessageAttributeValue buildMessageAttribute(String dataType, Object value) { 39 | MessageAttributeValue.Builder messageAttributeValue = 40 | MessageAttributeValue.builder() 41 | .dataType(dataType); 42 | if (value instanceof ByteBuffer byteBuffer) { 43 | messageAttributeValue.binaryValue(SdkBytes.fromByteBuffer(byteBuffer)); 44 | } 45 | else { 46 | messageAttributeValue.stringValue(value.toString()); 47 | } 48 | 49 | return messageAttributeValue.build(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/SqsHeaderMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | import software.amazon.awssdk.core.SdkBytes; 22 | import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; 23 | 24 | import org.springframework.messaging.MessageHeaders; 25 | 26 | /** 27 | * The {@link AbstractMessageAttributesHeaderMapper} implementation for the mapping from 28 | * headers to SQS message attributes. 29 | *

30 | * The 31 | * {@link io.awspring.cloud.sqs.listener.SqsMessageListenerContainer} 32 | * maps all the SQS message attributes to the {@link MessageHeaders}. 33 | * 34 | * @author Artem Bilan 35 | * 36 | * @since 2.0 37 | */ 38 | public class SqsHeaderMapper extends AbstractMessageAttributesHeaderMapper { 39 | 40 | @Override 41 | protected MessageAttributeValue buildMessageAttribute(String dataType, Object value) { 42 | MessageAttributeValue.Builder messageAttributeValue = 43 | MessageAttributeValue.builder() 44 | .dataType(dataType); 45 | if (value instanceof ByteBuffer byteBuffer) { 46 | messageAttributeValue.binaryValue(SdkBytes.fromByteBuffer(byteBuffer)); 47 | } 48 | else { 49 | messageAttributeValue.stringValue(value.toString()); 50 | } 51 | 52 | return messageAttributeValue.build(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/filters/S3PersistentAcceptOnceFileListFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support.filters; 18 | 19 | import software.amazon.awssdk.services.s3.model.S3Object; 20 | 21 | import org.springframework.integration.file.filters.AbstractPersistentAcceptOnceFileListFilter; 22 | import org.springframework.integration.metadata.ConcurrentMetadataStore; 23 | 24 | /** 25 | * Persistent file list filter using the server's file timestamp to detect if we've 26 | * already 'seen' this file. 27 | * 28 | * @author Artem Bilan 29 | */ 30 | public class S3PersistentAcceptOnceFileListFilter extends AbstractPersistentAcceptOnceFileListFilter { 31 | 32 | public S3PersistentAcceptOnceFileListFilter(ConcurrentMetadataStore store, String prefix) { 33 | super(store, prefix); 34 | } 35 | 36 | @Override 37 | protected long modified(S3Object file) { 38 | return (file != null) ? file.lastModified().getEpochSecond() : 0L; 39 | } 40 | 41 | @Override 42 | protected String fileName(S3Object file) { 43 | return (file != null) ? file.key() : null; 44 | } 45 | 46 | /** 47 | * Always return false since no directory notion in S3. 48 | * @param file the {@link S3Object} 49 | * @return always false: S3 does not have a notion of directory 50 | * @since 2.5 51 | */ 52 | @Override 53 | protected boolean isDirectory(S3Object file) { 54 | return false; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/outbound/SnsBodyBuilderTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import org.springframework.integration.aws.support.SnsBodyBuilder; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 25 | 26 | /** 27 | * @author Artem Bilan 28 | */ 29 | class SnsBodyBuilderTests { 30 | 31 | @Test 32 | void snsBodyBuilder() { 33 | assertThatIllegalArgumentException() 34 | .isThrownBy(() -> SnsBodyBuilder.withDefault("")) 35 | .withMessageContaining("defaultMessage must not be empty."); 36 | 37 | String message = SnsBodyBuilder.withDefault("foo").build(); 38 | assertThat(message).isEqualTo("{\"default\":\"foo\"}"); 39 | 40 | assertThatIllegalArgumentException() 41 | .isThrownBy(() -> SnsBodyBuilder.withDefault("foo").forProtocols("{\"foo\" : \"bar\"}").build()) 42 | .withMessageContaining("protocols must not be empty."); 43 | 44 | assertThatIllegalArgumentException() 45 | .isThrownBy(() -> SnsBodyBuilder.withDefault("foo").forProtocols("{\"foo\" : \"bar\"}", "").build()) 46 | .withMessageContaining("protocols must not contain empty elements."); 47 | 48 | message = SnsBodyBuilder.withDefault("foo").forProtocols("{\"foo\" : \"bar\"}", "sms").build(); 49 | 50 | assertThat(message).isEqualTo("{\"default\":\"foo\",\"sms\":\"{\\\"foo\\\" : \\\"bar\\\"}\"}"); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/kinesis/KinesisMessageHeaderErrorMessageStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound.kinesis; 18 | 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | import org.springframework.core.AttributeAccessor; 24 | import org.springframework.integration.aws.support.AwsHeaders; 25 | import org.springframework.integration.support.ErrorMessageStrategy; 26 | import org.springframework.integration.support.ErrorMessageUtils; 27 | import org.springframework.messaging.Message; 28 | import org.springframework.messaging.support.ErrorMessage; 29 | 30 | /** 31 | * The {@link ErrorMessageStrategy} implementation to build an {@link ErrorMessage} with 32 | * the {@link AwsHeaders#RAW_RECORD} header by the value from the the provided 33 | * {@link AttributeAccessor}. 34 | * 35 | * @author Artem Bilan 36 | * @since 2.0 37 | */ 38 | public class KinesisMessageHeaderErrorMessageStrategy implements ErrorMessageStrategy { 39 | 40 | @Override 41 | public ErrorMessage buildErrorMessage(Throwable throwable, AttributeAccessor context) { 42 | Object inputMessage = context == null ? null 43 | : context.getAttribute(ErrorMessageUtils.INPUT_MESSAGE_CONTEXT_KEY); 44 | 45 | Map headers = context == null ? new HashMap<>() 46 | : Collections.singletonMap(AwsHeaders.RAW_RECORD, context.getAttribute(AwsHeaders.RAW_RECORD)); 47 | 48 | return new ErrorMessage(throwable, headers, inputMessage instanceof Message ? (Message) inputMessage : null); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/SnsAsyncTopicArnResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import io.awspring.cloud.sns.core.TopicArnResolver; 20 | import software.amazon.awssdk.arns.Arn; 21 | import software.amazon.awssdk.services.sns.SnsAsyncClient; 22 | 23 | import org.springframework.util.Assert; 24 | 25 | /** 26 | * A {@link TopicArnResolver} implementation to determine topic ARN by name against an {@link SnsAsyncClient}. 27 | * 28 | * @author Artem Bilan 29 | * 30 | * @since 3.0 31 | */ 32 | public class SnsAsyncTopicArnResolver implements TopicArnResolver { 33 | private final SnsAsyncClient snsClient; 34 | 35 | public SnsAsyncTopicArnResolver(SnsAsyncClient snsClient) { 36 | Assert.notNull(snsClient, "snsClient is required"); 37 | this.snsClient = snsClient; 38 | } 39 | 40 | /** 41 | * Resolve topic ARN by topic name. If topicName is already an ARN, 42 | * it returns {@link Arn}. If topicName is just a 43 | * string with a topic name, it attempts to create a topic 44 | * or if topic already exists, just returns its ARN. 45 | */ 46 | @Override 47 | public Arn resolveTopicArn(String topicName) { 48 | Assert.notNull(topicName, "topicName must not be null"); 49 | if (topicName.toLowerCase().startsWith("arn:")) { 50 | return Arn.fromString(topicName); 51 | } 52 | else { 53 | // if topic exists, createTopic returns successful response with topic arn 54 | return Arn.fromString(this.snsClient.createTopic(request -> request.name(topicName)).join().topicArn()); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/S3SessionFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import software.amazon.awssdk.services.s3.S3Client; 20 | import software.amazon.awssdk.services.s3.model.S3Object; 21 | 22 | import org.springframework.integration.file.remote.session.SessionFactory; 23 | import org.springframework.integration.file.remote.session.SharedSessionCapable; 24 | import org.springframework.util.Assert; 25 | 26 | /** 27 | * An Amazon S3 specific {@link SessionFactory} implementation. Also, this class implements 28 | * {@link SharedSessionCapable} around the single instance, since the {@link S3Session} is 29 | * simple thread-safe wrapper for the {@link S3Client}. 30 | * 31 | * @author Artem Bilan 32 | * @author Xavier François 33 | */ 34 | public class S3SessionFactory implements SessionFactory, SharedSessionCapable { 35 | 36 | private final S3Session s3Session; 37 | 38 | public S3SessionFactory() { 39 | this(S3Client.create()); 40 | } 41 | 42 | public S3SessionFactory(S3Client amazonS3) { 43 | Assert.notNull(amazonS3, "'amazonS3' must not be null."); 44 | this.s3Session = new S3Session(amazonS3); 45 | } 46 | 47 | @Override 48 | public S3Session getSession() { 49 | return this.s3Session; 50 | } 51 | 52 | @Override 53 | public boolean isSharedSession() { 54 | return true; 55 | } 56 | 57 | @Override 58 | public void resetSharedSession() { 59 | // No-op. The S3Session is stateless and can be used concurrently. 60 | } 61 | 62 | public void setEndpoint(String endpoint) { 63 | this.s3Session.setEndpoint(endpoint); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/UserRecordResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.util.List; 20 | 21 | import com.amazonaws.services.kinesis.producer.Attempt; 22 | import com.amazonaws.services.kinesis.producer.UserRecordResult; 23 | import software.amazon.awssdk.awscore.AwsResponse; 24 | import software.amazon.awssdk.core.SdkField; 25 | import software.amazon.awssdk.services.kinesis.model.KinesisResponse; 26 | import software.amazon.awssdk.services.kinesis.model.PutRecordResponse; 27 | 28 | /** 29 | * The {@link KinesisResponse} adapter for the KPL {@link UserRecordResult} response. 30 | * 31 | * @author Artem Bilan 32 | * 33 | * @since 3.0.8 34 | */ 35 | public class UserRecordResponse extends KinesisResponse { 36 | 37 | private final String shardId; 38 | 39 | private final String sequenceNumber; 40 | 41 | private final List attempts; 42 | 43 | public UserRecordResponse(UserRecordResult userRecordResult) { 44 | super(PutRecordResponse.builder()); 45 | this.shardId = userRecordResult.getShardId(); 46 | this.sequenceNumber = userRecordResult.getSequenceNumber(); 47 | this.attempts = userRecordResult.getAttempts(); 48 | } 49 | 50 | public String shardId() { 51 | return this.shardId; 52 | } 53 | 54 | public String sequenceNumber() { 55 | return this.sequenceNumber; 56 | } 57 | 58 | public List attempts() { 59 | return this.attempts; 60 | } 61 | 62 | @Override 63 | public AwsResponse.Builder toBuilder() { 64 | throw new UnsupportedOperationException(); 65 | } 66 | 67 | @Override 68 | public List> sdkFields() { 69 | throw new UnsupportedOperationException(); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/S3RemoteFileTemplate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.io.IOException; 20 | import java.io.UncheckedIOException; 21 | 22 | import software.amazon.awssdk.services.s3.S3Client; 23 | import software.amazon.awssdk.services.s3.model.S3Object; 24 | 25 | import org.springframework.integration.file.remote.ClientCallback; 26 | import org.springframework.integration.file.remote.RemoteFileTemplate; 27 | import org.springframework.integration.file.remote.session.SessionFactory; 28 | 29 | /** 30 | * An Amazon S3 specific {@link RemoteFileTemplate} extension. 31 | * 32 | * @author Artem Bilan 33 | */ 34 | public class S3RemoteFileTemplate extends RemoteFileTemplate { 35 | 36 | public S3RemoteFileTemplate() { 37 | this(new S3SessionFactory()); 38 | } 39 | 40 | public S3RemoteFileTemplate(S3Client amazonS3) { 41 | this(new S3SessionFactory(amazonS3)); 42 | } 43 | 44 | /** 45 | * Construct a {@link RemoteFileTemplate} with the supplied session factory. 46 | * @param sessionFactory the session factory. 47 | */ 48 | public S3RemoteFileTemplate(SessionFactory sessionFactory) { 49 | super(sessionFactory); 50 | } 51 | 52 | @SuppressWarnings("unchecked") 53 | @Override 54 | public T executeWithClient(final ClientCallback callback) { 55 | return callback.doWithClient((C) this.sessionFactory.getSession().getClientInstance()); 56 | } 57 | 58 | @Override 59 | public boolean exists(final String path) { 60 | try { 61 | return this.sessionFactory.getSession().exists(path); 62 | } 63 | catch (IOException ex) { 64 | throw new UncheckedIOException("Failed to check the path " + path, ex); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/SnsBodyBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import org.springframework.util.Assert; 23 | 24 | /** 25 | * A utility class to simplify an SNS Message body building. Can be used from the 26 | * {@code SnsMessageHandler#bodyExpression} definition or directly in case of manual 27 | * {@link software.amazon.awssdk.services.sns.model.PublishRequest} building. 28 | * 29 | * @author Artem Bilan 30 | */ 31 | public final class SnsBodyBuilder { 32 | 33 | private final Map snsMessage = new HashMap<>(); 34 | 35 | private SnsBodyBuilder(String defaultMessage) { 36 | Assert.hasText(defaultMessage, "defaultMessage must not be empty."); 37 | this.snsMessage.put("default", defaultMessage); 38 | } 39 | 40 | public SnsBodyBuilder forProtocols(String message, String... protocols) { 41 | Assert.hasText(message, "message must not be empty."); 42 | Assert.notEmpty(protocols, "protocols must not be empty."); 43 | for (String protocol : protocols) { 44 | Assert.hasText(protocol, "protocols must not contain empty elements."); 45 | this.snsMessage.put(protocol, message); 46 | } 47 | return this; 48 | } 49 | 50 | public String build() { 51 | StringBuilder stringBuilder = new StringBuilder("{"); 52 | for (Map.Entry entry : this.snsMessage.entrySet()) { 53 | stringBuilder.append("\"").append(entry.getKey()).append("\":\"") 54 | .append(entry.getValue().replaceAll("\"", "\\\\\"")).append("\","); 55 | } 56 | return stringBuilder.substring(0, stringBuilder.length() - 1) + "}"; 57 | } 58 | 59 | public static SnsBodyBuilder withDefault(String defaultMessage) { 60 | return new SnsBodyBuilder(defaultMessage); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.adoc: -------------------------------------------------------------------------------- 1 | = Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open 4 | and welcoming community, we pledge to respect all people who contribute through reporting 5 | issues, posting feature requests, updating documentation, submitting pull requests or 6 | patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free experience for 9 | everyone, regardless of level of experience, gender, gender identity and expression, 10 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, 11 | religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 24 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 25 | Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors 26 | that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing this project. Project 30 | maintainers who do not follow or enforce the Code of Conduct may be permanently removed 31 | from the project team. 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will 38 | be reviewed and investigated and will result in a response that is deemed necessary and 39 | appropriate to the circumstances. Maintainers are obligated to maintain confidentiality 40 | with regard to the reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the 43 | https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at 44 | https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/] 45 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/S3FileInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.util.Date; 20 | 21 | import software.amazon.awssdk.services.s3.model.S3Object; 22 | 23 | import org.springframework.integration.file.remote.AbstractFileInfo; 24 | import org.springframework.util.Assert; 25 | 26 | /** 27 | * An Amazon S3 {@link org.springframework.integration.file.remote.FileInfo} 28 | * implementation. 29 | * 30 | * @author Christian Tzolov 31 | * @author Artem Bilan 32 | * 33 | * @since 1.1 34 | */ 35 | public class S3FileInfo extends AbstractFileInfo { 36 | 37 | private final S3Object s3Object; 38 | 39 | public S3FileInfo(S3Object s3Object) { 40 | Assert.notNull(s3Object, "s3Object must not be null"); 41 | this.s3Object = s3Object; 42 | } 43 | 44 | @Override 45 | public boolean isDirectory() { 46 | return false; 47 | } 48 | 49 | @Override 50 | public boolean isLink() { 51 | return false; 52 | } 53 | 54 | @Override 55 | public long getSize() { 56 | return this.s3Object.size(); 57 | } 58 | 59 | @Override 60 | public long getModified() { 61 | return this.s3Object.lastModified().getEpochSecond(); 62 | } 63 | 64 | @Override 65 | public String getFilename() { 66 | return this.s3Object.key(); 67 | } 68 | 69 | /** 70 | * A permissions representation string. Throws {@link UnsupportedOperationException} 71 | * to avoid extra {@link software.amazon.awssdk.services.s3.S3Client#getObjectAcl} REST call. 72 | * The target application amy choose to do that by its logic. 73 | * @return the permissions representation string. 74 | */ 75 | @Override 76 | public String getPermissions() { 77 | throw new UnsupportedOperationException("Use [AmazonS3.getObjectAcl()] to obtain permissions."); 78 | } 79 | 80 | @Override 81 | public S3Object getFileInfo() { 82 | return this.s3Object; 83 | } 84 | 85 | @Override 86 | public String toString() { 87 | return "FileInfo [isDirectory=" + isDirectory() + ", isLink=" + isLink() + ", Size=" + getSize() 88 | + ", ModifiedTime=" + new Date(getModified()) + ", Filename=" + getFilename() + ", RemoteDirectory=" 89 | + getRemoteDirectory() + "]"; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/S3StreamingMessageSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.util.Collection; 20 | import java.util.Comparator; 21 | import java.util.List; 22 | import java.util.stream.Collectors; 23 | 24 | import software.amazon.awssdk.services.s3.model.S3Object; 25 | 26 | import org.springframework.integration.aws.support.S3FileInfo; 27 | import org.springframework.integration.aws.support.S3Session; 28 | import org.springframework.integration.aws.support.filters.S3PersistentAcceptOnceFileListFilter; 29 | import org.springframework.integration.file.remote.AbstractFileInfo; 30 | import org.springframework.integration.file.remote.AbstractRemoteFileStreamingMessageSource; 31 | import org.springframework.integration.file.remote.RemoteFileTemplate; 32 | import org.springframework.integration.metadata.SimpleMetadataStore; 33 | 34 | /** 35 | * A {@link AbstractRemoteFileStreamingMessageSource} implementation for the Amazon S3. 36 | * 37 | * @author Christian Tzolov 38 | * @author Artem Bilan 39 | * 40 | * @since 1.1 41 | */ 42 | public class S3StreamingMessageSource extends AbstractRemoteFileStreamingMessageSource { 43 | 44 | public S3StreamingMessageSource(RemoteFileTemplate template) { 45 | super(template, null); 46 | } 47 | 48 | @SuppressWarnings("this-escape") 49 | public S3StreamingMessageSource(RemoteFileTemplate template, Comparator comparator) { 50 | super(template, comparator); 51 | doSetFilter(new S3PersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "s3StreamingMessageSource")); 52 | } 53 | 54 | @Override 55 | protected List> asFileInfoList(Collection collection) { 56 | return collection.stream().map(S3FileInfo::new).collect(Collectors.toList()); 57 | } 58 | 59 | @Override 60 | public String getComponentType() { 61 | return "aws:s3-inbound-streaming-channel-adapter"; 62 | } 63 | 64 | @Override 65 | protected AbstractFileInfo poll() { 66 | AbstractFileInfo file = super.poll(); 67 | if (file != null) { 68 | S3Session s3Session = (S3Session) getRemoteFileTemplate().getSession(); 69 | file.setRemoteDirectory(s3Session.normalizeBucketName(file.getRemoteDirectory())); 70 | } 71 | return file; 72 | } 73 | 74 | @Override 75 | protected boolean isDirectory(S3Object file) { 76 | return false; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/S3InboundFileSynchronizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | 22 | import software.amazon.awssdk.services.s3.S3Client; 23 | import software.amazon.awssdk.services.s3.model.S3Object; 24 | 25 | import org.springframework.expression.EvaluationContext; 26 | import org.springframework.expression.common.LiteralExpression; 27 | import org.springframework.integration.aws.support.S3Session; 28 | import org.springframework.integration.aws.support.S3SessionFactory; 29 | import org.springframework.integration.aws.support.filters.S3PersistentAcceptOnceFileListFilter; 30 | import org.springframework.integration.file.remote.session.Session; 31 | import org.springframework.integration.file.remote.session.SessionFactory; 32 | import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizer; 33 | import org.springframework.integration.metadata.SimpleMetadataStore; 34 | import org.springframework.lang.Nullable; 35 | 36 | /** 37 | * An implementation of {@link AbstractInboundFileSynchronizer} for Amazon S3. 38 | * 39 | * @author Artem Bilan 40 | */ 41 | public class S3InboundFileSynchronizer extends AbstractInboundFileSynchronizer { 42 | 43 | public S3InboundFileSynchronizer() { 44 | this(new S3SessionFactory()); 45 | } 46 | 47 | public S3InboundFileSynchronizer(S3Client amazonS3) { 48 | this(new S3SessionFactory(amazonS3)); 49 | } 50 | 51 | /** 52 | * Create a synchronizer with the {@link SessionFactory} used to acquire 53 | * {@link Session} instances. 54 | * @param sessionFactory The session factory. 55 | */ 56 | @SuppressWarnings("this-escape") 57 | public S3InboundFileSynchronizer(SessionFactory sessionFactory) { 58 | super(sessionFactory); 59 | doSetRemoteDirectoryExpression(new LiteralExpression(null)); 60 | doSetFilter(new S3PersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "s3MessageSource")); 61 | } 62 | 63 | @Override 64 | protected boolean isFile(S3Object file) { 65 | return true; 66 | } 67 | 68 | @Override 69 | protected String getFilename(S3Object file) { 70 | return (file != null ? file.key() : null); 71 | } 72 | 73 | @Override 74 | protected long getModified(S3Object file) { 75 | return file.lastModified().getEpochSecond(); 76 | } 77 | 78 | @Override 79 | protected boolean copyFileToLocalDirectory(String remoteDirectoryPath, 80 | @Nullable EvaluationContext localFileEvaluationContext, S3Object remoteFile, 81 | File localDirectory, Session session) throws IOException { 82 | 83 | return super.copyFileToLocalDirectory(((S3Session) session).normalizeBucketName(remoteDirectoryPath), 84 | localFileEvaluationContext, remoteFile, localDirectory, session); 85 | } 86 | 87 | @Override 88 | protected String protocol() { 89 | return "s3"; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/kinesis/ShardCheckpointer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound.kinesis; 18 | 19 | import java.math.BigInteger; 20 | 21 | import org.apache.commons.logging.Log; 22 | import org.apache.commons.logging.LogFactory; 23 | 24 | import org.springframework.integration.metadata.ConcurrentMetadataStore; 25 | import org.springframework.integration.metadata.MetadataStore; 26 | import org.springframework.lang.Nullable; 27 | 28 | /** 29 | * An internal {@link Checkpointer} implementation based on provided {@link MetadataStore} 30 | * and {@code key} for shard. 31 | *

32 | * The instances of this class is created by the 33 | * {@link KinesisMessageDrivenChannelAdapter} for each {@code ShardConsumer}. 34 | * 35 | * @author Artem Bilan 36 | * @since 1.1 37 | */ 38 | class ShardCheckpointer implements Checkpointer { 39 | 40 | private static final Log logger = LogFactory.getLog(ShardCheckpointer.class); 41 | 42 | private final ConcurrentMetadataStore checkpointStore; 43 | 44 | private final String key; 45 | 46 | private volatile String highestSequence; 47 | 48 | private volatile String lastCheckpointValue; 49 | 50 | private volatile boolean active = true; 51 | 52 | ShardCheckpointer(ConcurrentMetadataStore checkpointStore, String key) { 53 | this.checkpointStore = checkpointStore; 54 | this.key = key; 55 | } 56 | 57 | @Override 58 | public boolean checkpoint() { 59 | return checkpoint(this.highestSequence); 60 | } 61 | 62 | @Override 63 | public boolean checkpoint(String sequenceNumber) { 64 | if (this.active) { 65 | String existingSequence = getCheckpoint(); 66 | if (existingSequence == null 67 | || new BigInteger(existingSequence).compareTo(new BigInteger(sequenceNumber)) < 0) { 68 | if (existingSequence != null) { 69 | return this.checkpointStore.replace(this.key, existingSequence, sequenceNumber); 70 | } 71 | else { 72 | boolean stored = this.checkpointStore.putIfAbsent(this.key, sequenceNumber) == null; 73 | if (stored) { 74 | this.lastCheckpointValue = sequenceNumber; 75 | } 76 | return stored; 77 | } 78 | } 79 | } 80 | else { 81 | if (logger.isInfoEnabled()) { 82 | logger.info("The [" + this + "] has been closed. Checkpoints aren't accepted anymore."); 83 | } 84 | } 85 | 86 | return false; 87 | } 88 | 89 | void setHighestSequence(String highestSequence) { 90 | this.highestSequence = highestSequence; 91 | } 92 | 93 | @Nullable 94 | String getHighestSequence() { 95 | return this.highestSequence; 96 | } 97 | 98 | @Nullable 99 | String getCheckpoint() { 100 | this.lastCheckpointValue = this.checkpointStore.get(this.key); 101 | return this.lastCheckpointValue; 102 | } 103 | 104 | @Nullable 105 | String getLastCheckpointValue() { 106 | return this.lastCheckpointValue; 107 | } 108 | 109 | void remove() { 110 | this.checkpointStore.remove(this.key); 111 | } 112 | 113 | void close() { 114 | this.active = false; 115 | } 116 | 117 | @Override 118 | public String toString() { 119 | return "ShardCheckpointer{" + "key='" + this.key + '\'' + ", lastCheckpointValue='" + this.lastCheckpointValue 120 | + '\'' + '}'; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/AwsHeaders.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | /** 20 | * The AWS specific message headers constants. 21 | * 22 | * @author Artem Bilan 23 | */ 24 | public abstract class AwsHeaders { 25 | 26 | private static final String PREFIX = "aws_"; 27 | 28 | /** 29 | * The {@value QUEUE} header for sending data to SQS. 30 | */ 31 | public static final String QUEUE = PREFIX + "queue"; 32 | 33 | /** 34 | * The {@value TOPIC} header for sending/receiving data over SNS. 35 | */ 36 | public static final String TOPIC = PREFIX + "topic"; 37 | 38 | /** 39 | * The {@value MESSAGE_ID} header for SQS/SNS message ids. 40 | */ 41 | public static final String MESSAGE_ID = PREFIX + "messageId"; 42 | 43 | /** 44 | * The {@value NOTIFICATION_STATUS} header for SNS notification status. 45 | */ 46 | public static final String NOTIFICATION_STATUS = PREFIX + "notificationStatus"; 47 | 48 | /** 49 | * The {@value SNS_MESSAGE_TYPE} header for SNS message type. 50 | */ 51 | public static final String SNS_MESSAGE_TYPE = PREFIX + "snsMessageType"; 52 | 53 | /** 54 | * The {@value SHARD} header to represent Kinesis shardId. 55 | */ 56 | public static final String SHARD = PREFIX + "shard"; 57 | 58 | /** 59 | * The {@value RECEIVED_STREAM} header for receiving data from Kinesis. 60 | */ 61 | public static final String RECEIVED_STREAM = PREFIX + "receivedStream"; 62 | 63 | /** 64 | * The {@value RECEIVED_PARTITION_KEY} header for receiving data from Kinesis. 65 | */ 66 | public static final String RECEIVED_PARTITION_KEY = PREFIX + "receivedPartitionKey"; 67 | 68 | /** 69 | * The {@value RECEIVED_SEQUENCE_NUMBER} header for receiving data from Kinesis. 70 | */ 71 | public static final String RECEIVED_SEQUENCE_NUMBER = PREFIX + "receivedSequenceNumber"; 72 | 73 | /** 74 | * The {@value STREAM} header for sending data to Kinesis. 75 | */ 76 | public static final String STREAM = PREFIX + "stream"; 77 | 78 | /** 79 | * The {@value PARTITION_KEY} header for sending data to Kinesis. 80 | */ 81 | public static final String PARTITION_KEY = PREFIX + "partitionKey"; 82 | 83 | /** 84 | * The {@value SEQUENCE_NUMBER} header for sending data to SQS/Kinesis. 85 | */ 86 | public static final String SEQUENCE_NUMBER = PREFIX + "sequenceNumber"; 87 | 88 | /** 89 | * The {@value CHECKPOINTER} header for checkpoint the shard sequenceNumber. 90 | */ 91 | public static final String CHECKPOINTER = PREFIX + "checkpointer"; 92 | 93 | /** 94 | * The {@value SERVICE_RESULT} header represents a 95 | * {@link com.amazonaws.AmazonWebServiceResult}. 96 | */ 97 | public static final String SERVICE_RESULT = PREFIX + "serviceResult"; 98 | 99 | /** 100 | * The {@value RAW_RECORD} header represents received Kinesis record(s). 101 | */ 102 | public static final String RAW_RECORD = PREFIX + "rawRecord"; 103 | 104 | /** 105 | * The {@value TRANSFER_LISTENER} header for 106 | * {@link software.amazon.awssdk.transfer.s3.progress.TransferListener} 107 | * callback used in the {@link org.springframework.integration.aws.outbound.S3MessageHandler} 108 | * for file uploads. 109 | */ 110 | public static final String TRANSFER_LISTENER = PREFIX + "transferListener"; 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.util.Arrays; 20 | import java.util.Collection; 21 | 22 | import io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory; 23 | import io.awspring.cloud.sqs.listener.MessageListener; 24 | import io.awspring.cloud.sqs.listener.SqsContainerOptions; 25 | import io.awspring.cloud.sqs.listener.SqsHeaders; 26 | import io.awspring.cloud.sqs.listener.SqsMessageListenerContainer; 27 | import software.amazon.awssdk.services.sqs.SqsAsyncClient; 28 | 29 | import org.springframework.integration.endpoint.MessageProducerSupport; 30 | import org.springframework.integration.support.management.IntegrationManagedResource; 31 | import org.springframework.jmx.export.annotation.ManagedAttribute; 32 | import org.springframework.jmx.export.annotation.ManagedResource; 33 | import org.springframework.messaging.Message; 34 | import org.springframework.messaging.support.GenericMessage; 35 | import org.springframework.util.Assert; 36 | 37 | /** 38 | * The {@link MessageProducerSupport} implementation for the Amazon SQS 39 | * {@code receiveMessage}. Works in 'listener' manner and delegates hard to the 40 | * {@link SqsMessageListenerContainer}. 41 | * 42 | * @author Artem Bilan 43 | * @author Patrick Fitzsimons 44 | * 45 | * @see SqsMessageListenerContainerFactory 46 | * @see SqsMessageListenerContainerFactory 47 | * @see MessageListener 48 | * @see SqsHeaders 49 | */ 50 | @ManagedResource 51 | @IntegrationManagedResource 52 | public class SqsMessageDrivenChannelAdapter extends MessageProducerSupport { 53 | 54 | private final SqsMessageListenerContainerFactory.Builder sqsMessageListenerContainerFactory = 55 | SqsMessageListenerContainerFactory.builder(); 56 | 57 | private final String[] queues; 58 | 59 | private SqsContainerOptions sqsContainerOptions; 60 | 61 | private SqsMessageListenerContainer listenerContainer; 62 | 63 | public SqsMessageDrivenChannelAdapter(SqsAsyncClient amazonSqs, String... queues) { 64 | Assert.noNullElements(queues, "'queues' must not be empty"); 65 | this.sqsMessageListenerContainerFactory.sqsAsyncClient(amazonSqs); 66 | this.queues = Arrays.copyOf(queues, queues.length); 67 | } 68 | 69 | public void setSqsContainerOptions(SqsContainerOptions sqsContainerOptions) { 70 | this.sqsContainerOptions = sqsContainerOptions; 71 | } 72 | 73 | @Override 74 | protected void onInit() { 75 | super.onInit(); 76 | if (this.sqsContainerOptions != null) { 77 | this.sqsMessageListenerContainerFactory.configure(sqsContainerOptionsBuilder -> 78 | sqsContainerOptionsBuilder.fromBuilder(this.sqsContainerOptions.toBuilder())); 79 | } 80 | this.sqsMessageListenerContainerFactory.messageListener(new IntegrationMessageListener()); 81 | this.listenerContainer = this.sqsMessageListenerContainerFactory.build().createContainer(this.queues); 82 | } 83 | 84 | @Override 85 | public String getComponentType() { 86 | return "aws:sqs-message-driven-channel-adapter"; 87 | } 88 | 89 | @Override 90 | protected void doStart() { 91 | this.listenerContainer.start(); 92 | } 93 | 94 | @Override 95 | protected void doStop() { 96 | this.listenerContainer.stop(); 97 | } 98 | 99 | @ManagedAttribute 100 | public String[] getQueues() { 101 | return Arrays.copyOf(this.queues, this.queues.length); 102 | } 103 | 104 | private class IntegrationMessageListener implements MessageListener { 105 | 106 | IntegrationMessageListener() { 107 | } 108 | 109 | @Override 110 | public void onMessage(Message message) { 111 | sendMessage(message); 112 | } 113 | 114 | @Override 115 | public void onMessage(Collection> messages) { 116 | onMessage(new GenericMessage<>(messages)); 117 | } 118 | 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/LocalstackContainerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws; 18 | 19 | import org.junit.jupiter.api.BeforeAll; 20 | import org.testcontainers.containers.localstack.LocalStackContainer; 21 | import org.testcontainers.junit.jupiter.Testcontainers; 22 | import org.testcontainers.utility.DockerImageName; 23 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 24 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 25 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 26 | import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; 27 | import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; 28 | import software.amazon.awssdk.regions.Region; 29 | import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; 30 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 31 | import software.amazon.awssdk.services.kinesis.KinesisAsyncClient; 32 | import software.amazon.awssdk.services.s3.S3AsyncClient; 33 | import software.amazon.awssdk.services.s3.S3Client; 34 | import software.amazon.awssdk.services.sqs.SqsAsyncClient; 35 | 36 | /** 37 | * The base contract for JUnit tests based on the container for Localstack. 38 | * The Testcontainers 'reuse' option must be disabled,so, Ryuk container is started 39 | * and will clean all the containers up from this test suite after JVM exit. 40 | * Since the Localstack container instance is shared via static property, it is going to be 41 | * started only once per JVM, therefore the target Docker container is reused automatically. 42 | * 43 | * @author Artem Bilan 44 | * 45 | * @since 3.0 46 | */ 47 | @Testcontainers(disabledWithoutDocker = true) 48 | public interface LocalstackContainerTest { 49 | 50 | LocalStackContainer LOCAL_STACK_CONTAINER = 51 | new LocalStackContainer(DockerImageName.parse("localstack/localstack:2.3.2")); 52 | 53 | @BeforeAll 54 | static void startContainer() { 55 | LOCAL_STACK_CONTAINER.start(); 56 | System.setProperty("software.amazon.awssdk.http.async.service.impl", 57 | "software.amazon.awssdk.http.crt.AwsCrtSdkHttpService"); 58 | System.setProperty("software.amazon.awssdk.http.service.impl", 59 | "software.amazon.awssdk.http.apache.ApacheSdkHttpService"); 60 | } 61 | 62 | static DynamoDbAsyncClient dynamoDbClient() { 63 | return applyAwsClientOptions(DynamoDbAsyncClient.builder()); 64 | } 65 | 66 | static KinesisAsyncClient kinesisClient() { 67 | return applyAwsClientOptions(KinesisAsyncClient.builder().httpClientBuilder(NettyNioAsyncHttpClient.builder())); 68 | } 69 | 70 | static CloudWatchAsyncClient cloudWatchClient() { 71 | return applyAwsClientOptions(CloudWatchAsyncClient.builder()); 72 | } 73 | 74 | static S3AsyncClient s3AsyncClient() { 75 | return S3AsyncClient.crtBuilder() 76 | .region(Region.of(LOCAL_STACK_CONTAINER.getRegion())) 77 | .credentialsProvider(credentialsProvider()) 78 | .endpointOverride(LOCAL_STACK_CONTAINER.getEndpoint()) 79 | .build(); 80 | } 81 | 82 | static S3Client s3Client() { 83 | return applyAwsClientOptions(S3Client.builder()); 84 | } 85 | 86 | static SqsAsyncClient sqsClient() { 87 | return applyAwsClientOptions(SqsAsyncClient.builder()); 88 | } 89 | 90 | static AwsCredentialsProvider credentialsProvider() { 91 | return StaticCredentialsProvider.create( 92 | AwsBasicCredentials.create(LOCAL_STACK_CONTAINER.getAccessKey(), LOCAL_STACK_CONTAINER.getSecretKey())); 93 | } 94 | 95 | private static , T> T applyAwsClientOptions(B clientBuilder) { 96 | return clientBuilder 97 | .region(Region.of(LOCAL_STACK_CONTAINER.getRegion())) 98 | .credentialsProvider(credentialsProvider()) 99 | .endpointOverride(LOCAL_STACK_CONTAINER.getEndpoint()) 100 | .build(); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/inbound/SqsMessageDrivenChannelAdapterTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.util.Map; 20 | 21 | import io.awspring.cloud.sqs.listener.SqsHeaders; 22 | import org.junit.jupiter.api.BeforeAll; 23 | import org.junit.jupiter.api.Test; 24 | import software.amazon.awssdk.services.sqs.SqsAsyncClient; 25 | import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; 26 | import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; 27 | 28 | import org.springframework.beans.factory.annotation.Autowired; 29 | import org.springframework.context.annotation.Bean; 30 | import org.springframework.context.annotation.Configuration; 31 | import org.springframework.integration.aws.LocalstackContainerTest; 32 | import org.springframework.integration.channel.QueueChannel; 33 | import org.springframework.integration.config.EnableIntegration; 34 | import org.springframework.integration.core.MessageProducer; 35 | import org.springframework.messaging.PollableChannel; 36 | import org.springframework.test.annotation.DirtiesContext; 37 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 38 | 39 | import static org.assertj.core.api.Assertions.assertThat; 40 | 41 | /** 42 | * @author Artem Bilan 43 | */ 44 | @SpringJUnitConfig 45 | @DirtiesContext 46 | class SqsMessageDrivenChannelAdapterTests implements LocalstackContainerTest { 47 | 48 | private static SqsAsyncClient AMAZON_SQS; 49 | 50 | private static String testQueueUrl; 51 | 52 | @Autowired 53 | private PollableChannel inputChannel; 54 | 55 | @BeforeAll 56 | static void setup() { 57 | AMAZON_SQS = LocalstackContainerTest.sqsClient(); 58 | testQueueUrl = AMAZON_SQS.createQueue(request -> request.queueName("testQueue")).join().queueUrl(); 59 | } 60 | 61 | @Test 62 | void sqsMessageDrivenChannelAdapter() { 63 | Map attributes = 64 | Map.of("someAttribute", 65 | MessageAttributeValue.builder() 66 | .stringValue("someValue") 67 | .dataType("String") 68 | .build()); 69 | 70 | AMAZON_SQS.sendMessageBatch(request -> 71 | request.queueUrl(testQueueUrl) 72 | .entries(SendMessageBatchRequestEntry.builder() 73 | .messageBody("messageContent") 74 | .id("messageContent_id") 75 | .messageAttributes(attributes) 76 | .build(), 77 | SendMessageBatchRequestEntry.builder() 78 | .messageBody("messageContent2") 79 | .id("messageContent2_id") 80 | .messageAttributes(attributes) 81 | .build())); 82 | 83 | org.springframework.messaging.Message receive = this.inputChannel.receive(10000); 84 | assertThat(receive).isNotNull(); 85 | assertThat((String) receive.getPayload()).isIn("messageContent", "messageContent2"); 86 | assertThat(receive.getHeaders().get(SqsHeaders.SQS_QUEUE_NAME_HEADER)).isEqualTo("testQueue"); 87 | assertThat(receive.getHeaders().get("someAttribute")).isEqualTo("someValue"); 88 | 89 | receive = this.inputChannel.receive(10000); 90 | assertThat(receive).isNotNull(); 91 | assertThat((String) receive.getPayload()).isIn("messageContent", "messageContent2"); 92 | assertThat(receive.getHeaders().get(SqsHeaders.SQS_QUEUE_NAME_HEADER)).isEqualTo("testQueue"); 93 | assertThat(receive.getHeaders().get("someAttribute")).isEqualTo("someValue"); 94 | } 95 | 96 | @Configuration 97 | @EnableIntegration 98 | public static class ContextConfiguration { 99 | 100 | @Bean 101 | public PollableChannel inputChannel() { 102 | return new QueueChannel(); 103 | } 104 | 105 | @Bean 106 | public MessageProducer sqsMessageDrivenChannelAdapter() { 107 | SqsMessageDrivenChannelAdapter adapter = new SqsMessageDrivenChannelAdapter(AMAZON_SQS, "testQueue"); 108 | adapter.setOutputChannel(inputChannel()); 109 | return adapter; 110 | } 111 | 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/metadata/DynamoDbMetadataStoreTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.metadata; 18 | 19 | import java.time.Duration; 20 | import java.util.Map; 21 | import java.util.concurrent.CountDownLatch; 22 | 23 | import org.junit.jupiter.api.BeforeAll; 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy; 27 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 28 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 29 | 30 | import org.springframework.integration.aws.LocalstackContainerTest; 31 | import org.springframework.integration.aws.lock.DynamoDbLockRepository; 32 | import org.springframework.integration.test.util.TestUtils; 33 | 34 | import static org.assertj.core.api.Assertions.assertThat; 35 | 36 | /** 37 | * @author Artem Bilan 38 | * 39 | * @since 1.1 40 | */ 41 | class DynamoDbMetadataStoreTests implements LocalstackContainerTest { 42 | 43 | private static final String TEST_TABLE = "testMetadataStore"; 44 | 45 | private static DynamoDbAsyncClient DYNAMO_DB; 46 | 47 | private static DynamoDbMetadataStore store; 48 | 49 | private final String file1 = "/remotepath/filesTodownload/file-1.txt"; 50 | 51 | private final String file1Id = "12345"; 52 | 53 | @BeforeAll 54 | static void setup() { 55 | DYNAMO_DB = LocalstackContainerTest.dynamoDbClient(); 56 | try { 57 | DYNAMO_DB.deleteTable(request -> request.tableName(TEST_TABLE)) 58 | .thenCompose(result -> 59 | DYNAMO_DB.waiter() 60 | .waitUntilTableNotExists(request -> request 61 | .tableName(DynamoDbLockRepository.DEFAULT_TABLE_NAME), 62 | waiter -> waiter 63 | .maxAttempts(25) 64 | .backoffStrategy( 65 | FixedDelayBackoffStrategy.create(Duration.ofSeconds(1))))) 66 | .join(); 67 | } 68 | catch (Exception e) { 69 | // Ignore if table does not exist 70 | } 71 | 72 | store = new DynamoDbMetadataStore(DYNAMO_DB, TEST_TABLE); 73 | store.setTimeToLive(10); 74 | store.afterPropertiesSet(); 75 | } 76 | 77 | @BeforeEach 78 | void clear() throws InterruptedException { 79 | CountDownLatch createTableLatch = TestUtils.getPropertyValue(store, "createTableLatch", CountDownLatch.class); 80 | 81 | createTableLatch.await(); 82 | 83 | DYNAMO_DB.deleteItem(request -> request 84 | .tableName(TEST_TABLE) 85 | .key(Map.of(DynamoDbMetadataStore.KEY, AttributeValue.fromS((this.file1))))) 86 | .join(); 87 | } 88 | 89 | @Test 90 | void getFromStore() { 91 | String fileID = store.get(this.file1); 92 | assertThat(fileID).isNull(); 93 | 94 | store.put(this.file1, this.file1Id); 95 | 96 | fileID = store.get(this.file1); 97 | assertThat(fileID).isNotNull(); 98 | assertThat(fileID).isEqualTo(this.file1Id); 99 | } 100 | 101 | @Test 102 | void putIfAbsent() { 103 | String fileID = store.get(this.file1); 104 | assertThat(fileID).describedAs("Get First time, Value must not exist").isNull(); 105 | 106 | fileID = store.putIfAbsent(this.file1, this.file1Id); 107 | assertThat(fileID).describedAs("Insert First time, Value must return null").isNull(); 108 | 109 | fileID = store.putIfAbsent(this.file1, "56789"); 110 | assertThat(fileID).describedAs("Key Already Exists - Insertion Failed, ol value must be returned").isNotNull(); 111 | assertThat(fileID).describedAs("The Old Value must be equal to returned").isEqualTo(this.file1Id); 112 | 113 | assertThat(store.get(this.file1)).describedAs("The Old Value must return").isEqualTo(this.file1Id); 114 | } 115 | 116 | @Test 117 | void remove() { 118 | String fileID = store.remove(this.file1); 119 | assertThat(fileID).isNull(); 120 | 121 | fileID = store.putIfAbsent(this.file1, this.file1Id); 122 | assertThat(fileID).isNull(); 123 | 124 | fileID = store.remove(this.file1); 125 | assertThat(fileID).isNotNull(); 126 | assertThat(fileID).isEqualTo(this.file1Id); 127 | 128 | fileID = store.get(this.file1); 129 | assertThat(fileID).isNull(); 130 | } 131 | 132 | @Test 133 | void replace() { 134 | boolean removedValue = store.replace(this.file1, this.file1Id, "4567"); 135 | assertThat(removedValue).isFalse(); 136 | 137 | String fileID = store.get(this.file1); 138 | assertThat(fileID).isNull(); 139 | 140 | fileID = store.putIfAbsent(this.file1, this.file1Id); 141 | assertThat(fileID).isNull(); 142 | 143 | removedValue = store.replace(this.file1, this.file1Id, "4567"); 144 | assertThat(removedValue).isTrue(); 145 | 146 | fileID = store.get(this.file1); 147 | assertThat(fileID).isNotNull(); 148 | assertThat(fileID).isEqualTo("4567"); 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/inbound/S3StreamingChannelAdapterTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.nio.charset.Charset; 22 | import java.util.Comparator; 23 | 24 | import org.apache.commons.io.IOUtils; 25 | import org.junit.jupiter.api.BeforeAll; 26 | import org.junit.jupiter.api.Test; 27 | import software.amazon.awssdk.core.sync.RequestBody; 28 | import software.amazon.awssdk.services.s3.S3Client; 29 | import software.amazon.awssdk.services.s3.model.S3Object; 30 | 31 | import org.springframework.beans.factory.annotation.Autowired; 32 | import org.springframework.context.annotation.Bean; 33 | import org.springframework.context.annotation.Configuration; 34 | import org.springframework.integration.annotation.InboundChannelAdapter; 35 | import org.springframework.integration.annotation.Poller; 36 | import org.springframework.integration.aws.LocalstackContainerTest; 37 | import org.springframework.integration.aws.support.S3RemoteFileTemplate; 38 | import org.springframework.integration.aws.support.S3SessionFactory; 39 | import org.springframework.integration.aws.support.filters.S3PersistentAcceptOnceFileListFilter; 40 | import org.springframework.integration.channel.QueueChannel; 41 | import org.springframework.integration.config.EnableIntegration; 42 | import org.springframework.integration.file.FileHeaders; 43 | import org.springframework.integration.metadata.SimpleMetadataStore; 44 | import org.springframework.messaging.Message; 45 | import org.springframework.messaging.PollableChannel; 46 | import org.springframework.test.annotation.DirtiesContext; 47 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 48 | 49 | import static org.assertj.core.api.Assertions.assertThat; 50 | 51 | /** 52 | * @author Christian Tzolov 53 | * @author Artem Bilan 54 | * 55 | * @since 1.1 56 | */ 57 | @SpringJUnitConfig 58 | @DirtiesContext 59 | class S3StreamingChannelAdapterTests implements LocalstackContainerTest { 60 | 61 | private static final String S3_BUCKET = "s3-bucket"; 62 | 63 | private static S3Client S3; 64 | 65 | @Autowired 66 | private PollableChannel s3FilesChannel; 67 | 68 | @BeforeAll 69 | static void setup() { 70 | S3 = LocalstackContainerTest.s3Client(); 71 | S3.createBucket(request -> request.bucket(S3_BUCKET)); 72 | S3.putObject(request -> request.bucket(S3_BUCKET).key("subdir/a.test"), RequestBody.fromString("Hello")); 73 | S3.putObject(request -> request.bucket(S3_BUCKET).key("subdir/b.test"), RequestBody.fromString("Bye")); 74 | } 75 | 76 | @Test 77 | void s3InboundStreamingChannelAdapter() throws IOException { 78 | Message message = this.s3FilesChannel.receive(10000); 79 | assertThat(message).isNotNull(); 80 | assertThat(message.getPayload()).isInstanceOf(InputStream.class); 81 | assertThat(message.getHeaders().get(FileHeaders.REMOTE_FILE)).isEqualTo("subdir/a.test"); 82 | 83 | InputStream inputStreamA = (InputStream) message.getPayload(); 84 | assertThat(inputStreamA).isNotNull(); 85 | assertThat(IOUtils.toString(inputStreamA, Charset.defaultCharset())).isEqualTo("Hello"); 86 | inputStreamA.close(); 87 | 88 | message = this.s3FilesChannel.receive(10000); 89 | assertThat(message).isNotNull(); 90 | assertThat(message.getPayload()).isInstanceOf(InputStream.class); 91 | assertThat(message.getHeaders().get(FileHeaders.REMOTE_FILE)).isEqualTo("subdir/b.test"); 92 | assertThat(message.getHeaders()) 93 | .containsKeys(FileHeaders.REMOTE_DIRECTORY, FileHeaders.REMOTE_HOST_PORT, FileHeaders.REMOTE_FILE); 94 | InputStream inputStreamB = (InputStream) message.getPayload(); 95 | assertThat(IOUtils.toString(inputStreamB, Charset.defaultCharset())).isEqualTo("Bye"); 96 | 97 | assertThat(this.s3FilesChannel.receive(10)).isNull(); 98 | 99 | inputStreamB.close(); 100 | } 101 | 102 | @Configuration 103 | @EnableIntegration 104 | public static class Config { 105 | 106 | @Bean 107 | @InboundChannelAdapter(value = "s3FilesChannel", poller = @Poller(fixedDelay = "100")) 108 | public S3StreamingMessageSource s3InboundStreamingMessageSource() { 109 | S3SessionFactory s3SessionFactory = new S3SessionFactory(S3); 110 | S3RemoteFileTemplate s3FileTemplate = new S3RemoteFileTemplate(s3SessionFactory); 111 | S3StreamingMessageSource s3MessageSource = 112 | new S3StreamingMessageSource(s3FileTemplate, Comparator.comparing(S3Object::key)); 113 | s3MessageSource.setRemoteDirectory("/" + S3_BUCKET + "/subdir"); 114 | s3MessageSource.setFilter(new S3PersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "streaming")); 115 | 116 | return s3MessageSource; 117 | } 118 | 119 | @Bean 120 | public PollableChannel s3FilesChannel() { 121 | return new QueueChannel(); 122 | } 123 | 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/support/AbstractMessageAttributesHeaderMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.support; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.util.Arrays; 21 | import java.util.Map; 22 | import java.util.UUID; 23 | 24 | import org.apache.commons.logging.Log; 25 | import org.apache.commons.logging.LogFactory; 26 | 27 | import org.springframework.integration.mapping.HeaderMapper; 28 | import org.springframework.integration.support.utils.PatternMatchUtils; 29 | import org.springframework.messaging.MessageHeaders; 30 | import org.springframework.messaging.support.NativeMessageHeaderAccessor; 31 | import org.springframework.util.Assert; 32 | import org.springframework.util.MimeType; 33 | import org.springframework.util.NumberUtils; 34 | 35 | /** 36 | * Base {@link HeaderMapper} implementation for common logic in SQS and SNS around message 37 | * attributes mapping. 38 | *

39 | * The {@link #toHeaders(Map)} is not supported. 40 | * 41 | * @param the target message attribute type. 42 | * 43 | * @author Artem Bilan 44 | * @author Christopher Smith 45 | * 46 | * @since 2.0 47 | */ 48 | public abstract class AbstractMessageAttributesHeaderMapper implements HeaderMapper> { 49 | 50 | protected final Log logger = LogFactory.getLog(getClass()); 51 | 52 | private volatile String[] outboundHeaderNames = { 53 | "!" + MessageHeaders.ID, 54 | "!" + MessageHeaders.TIMESTAMP, 55 | "!" + NativeMessageHeaderAccessor.NATIVE_HEADERS, 56 | "!" + AwsHeaders.MESSAGE_ID, 57 | "!" + AwsHeaders.QUEUE, 58 | "!" + AwsHeaders.TOPIC, 59 | "*", 60 | }; 61 | 62 | /** 63 | * Provide the header names that should be mapped to a AWS request object attributes 64 | * (for outbound adapters) from a Spring Integration Message's headers. The values can 65 | * also contain simple wildcard patterns (e.g. "foo*" or "*foo") to be matched. Also 66 | * supports negated ('!') patterns. First match wins (positive or negative). To match 67 | * the names starting with {@code !} symbol, you have to escape it prepending with the 68 | * {@code \} symbol in the pattern definition. Defaults to map all ({@code *}) if the 69 | * type is supported by SQS. The {@link MessageHeaders#ID}, 70 | * {@link MessageHeaders#TIMESTAMP}, 71 | * {@link NativeMessageHeaderAccessor#NATIVE_HEADERS}, 72 | * {@link AwsHeaders#MESSAGE_ID}, {@link AwsHeaders#QUEUE}, and 73 | * {@link AwsHeaders#TOPIC} are ignored by default. 74 | * @param outboundHeaderNames The inbound header names. 75 | */ 76 | public void setOutboundHeaderNames(String... outboundHeaderNames) { 77 | Assert.notNull(outboundHeaderNames, "'outboundHeaderNames' must not be null."); 78 | Assert.noNullElements(outboundHeaderNames, "'outboundHeaderNames' must not contains null elements."); 79 | Arrays.sort(outboundHeaderNames); 80 | this.outboundHeaderNames = outboundHeaderNames; 81 | } 82 | 83 | @Override 84 | public void fromHeaders(MessageHeaders headers, Map target) { 85 | for (Map.Entry messageHeader : headers.entrySet()) { 86 | String messageHeaderName = messageHeader.getKey(); 87 | Object messageHeaderValue = messageHeader.getValue(); 88 | 89 | if (Boolean.TRUE.equals(PatternMatchUtils.smartMatch(messageHeaderName, this.outboundHeaderNames))) { 90 | 91 | if (messageHeaderValue instanceof UUID || messageHeaderValue instanceof MimeType 92 | || messageHeaderValue instanceof Boolean || messageHeaderValue instanceof String) { 93 | 94 | target.put(messageHeaderName, getStringMessageAttribute(messageHeaderValue.toString())); 95 | } 96 | else if (messageHeaderValue instanceof Number) { 97 | target.put(messageHeaderName, getNumberMessageAttribute(messageHeaderValue)); 98 | } 99 | else if (messageHeaderValue instanceof ByteBuffer) { 100 | target.put(messageHeaderName, getBinaryMessageAttribute((ByteBuffer) messageHeaderValue)); 101 | } 102 | else if (messageHeaderValue instanceof byte[]) { 103 | target.put(messageHeaderName, 104 | getBinaryMessageAttribute(ByteBuffer.wrap((byte[]) messageHeaderValue))); 105 | } 106 | else { 107 | if (this.logger.isWarnEnabled()) { 108 | this.logger.warn(String.format( 109 | "Message header with name '%s' and type '%s' cannot be sent as" 110 | + " message attribute because it is not supported by the current AWS service.", 111 | messageHeaderName, messageHeaderValue.getClass().getName())); 112 | } 113 | } 114 | 115 | } 116 | } 117 | } 118 | 119 | private A getBinaryMessageAttribute(ByteBuffer messageHeaderValue) { 120 | return buildMessageAttribute("Binary", messageHeaderValue); 121 | } 122 | 123 | private A getStringMessageAttribute(String messageHeaderValue) { 124 | return buildMessageAttribute("String", messageHeaderValue); 125 | } 126 | 127 | private A getNumberMessageAttribute(Object messageHeaderValue) { 128 | Assert.isTrue(NumberUtils.STANDARD_NUMBER_TYPES.contains(messageHeaderValue.getClass()), 129 | "Only standard number types are accepted as message header."); 130 | 131 | return buildMessageAttribute("Number." + messageHeaderValue.getClass().getName(), 132 | messageHeaderValue); 133 | } 134 | 135 | protected abstract A buildMessageAttribute(String dataType, Object value); 136 | 137 | @Override 138 | public Map toHeaders(Map source) { 139 | throw new UnsupportedOperationException("The mapping from AWS Response Message is not supported"); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/inbound/S3InboundChannelAdapterTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.io.File; 20 | import java.io.FileReader; 21 | import java.io.IOException; 22 | import java.nio.file.Path; 23 | 24 | import org.junit.jupiter.api.BeforeAll; 25 | import org.junit.jupiter.api.Test; 26 | import org.junit.jupiter.api.io.TempDir; 27 | import software.amazon.awssdk.core.sync.RequestBody; 28 | import software.amazon.awssdk.services.s3.S3Client; 29 | 30 | import org.springframework.beans.factory.annotation.Autowired; 31 | import org.springframework.context.annotation.Bean; 32 | import org.springframework.context.annotation.Configuration; 33 | import org.springframework.expression.Expression; 34 | import org.springframework.expression.ExpressionParser; 35 | import org.springframework.expression.spel.standard.SpelExpressionParser; 36 | import org.springframework.integration.annotation.InboundChannelAdapter; 37 | import org.springframework.integration.annotation.Poller; 38 | import org.springframework.integration.aws.LocalstackContainerTest; 39 | import org.springframework.integration.aws.support.S3SessionFactory; 40 | import org.springframework.integration.aws.support.filters.S3RegexPatternFileListFilter; 41 | import org.springframework.integration.channel.QueueChannel; 42 | import org.springframework.integration.config.EnableIntegration; 43 | import org.springframework.integration.file.FileHeaders; 44 | import org.springframework.integration.file.filters.AcceptOnceFileListFilter; 45 | import org.springframework.messaging.Message; 46 | import org.springframework.messaging.PollableChannel; 47 | import org.springframework.test.annotation.DirtiesContext; 48 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 49 | import org.springframework.util.FileCopyUtils; 50 | 51 | import static org.assertj.core.api.Assertions.assertThat; 52 | 53 | /** 54 | * @author Artem Bilan 55 | * @author Jim Krygowski 56 | * @author Xavier François 57 | */ 58 | @SpringJUnitConfig 59 | @DirtiesContext 60 | class S3InboundChannelAdapterTests implements LocalstackContainerTest { 61 | 62 | private static final ExpressionParser PARSER = new SpelExpressionParser(); 63 | 64 | private static final String S3_BUCKET = "s3-bucket"; 65 | 66 | private static S3Client S3; 67 | 68 | @TempDir 69 | static Path TEMPORARY_FOLDER; 70 | 71 | private static File LOCAL_FOLDER; 72 | 73 | @Autowired 74 | private PollableChannel s3FilesChannel; 75 | 76 | @BeforeAll 77 | static void setup() { 78 | S3 = LocalstackContainerTest.s3Client(); 79 | S3.createBucket(request -> request.bucket(S3_BUCKET)); 80 | S3.putObject(request -> request.bucket(S3_BUCKET).key("subdir/a.test"), RequestBody.fromString("Hello")); 81 | S3.putObject(request -> request.bucket(S3_BUCKET).key("subdir/b.test"), RequestBody.fromString("Bye")); 82 | 83 | LOCAL_FOLDER = TEMPORARY_FOLDER.resolve("local").toFile(); 84 | } 85 | 86 | @Test 87 | void s3InboundChannelAdapter() throws IOException { 88 | Message message = this.s3FilesChannel.receive(10000); 89 | assertThat(message).isNotNull(); 90 | assertThat(message.getPayload()).isInstanceOf(File.class); 91 | File localFile = (File) message.getPayload(); 92 | assertThat(localFile).hasName("A.TEST.a"); 93 | 94 | String content = FileCopyUtils.copyToString(new FileReader(localFile)); 95 | assertThat(content).isEqualTo("Hello"); 96 | 97 | message = this.s3FilesChannel.receive(10000); 98 | assertThat(message).isNotNull(); 99 | assertThat(message.getPayload()).isInstanceOf(File.class); 100 | localFile = (File) message.getPayload(); 101 | assertThat(localFile).hasName("B.TEST.a"); 102 | 103 | content = FileCopyUtils.copyToString(new FileReader(localFile)); 104 | assertThat(content).isEqualTo("Bye"); 105 | 106 | assertThat(message.getHeaders()) 107 | .containsKeys(FileHeaders.REMOTE_DIRECTORY, FileHeaders.REMOTE_HOST_PORT, FileHeaders.REMOTE_FILE); 108 | } 109 | 110 | @Configuration 111 | @EnableIntegration 112 | public static class Config { 113 | 114 | @Bean 115 | public S3SessionFactory s3SessionFactory() { 116 | S3SessionFactory s3SessionFactory = new S3SessionFactory(S3); 117 | s3SessionFactory.setEndpoint("s3-url.com:8000"); 118 | return s3SessionFactory; 119 | } 120 | 121 | @Bean 122 | public S3InboundFileSynchronizer s3InboundFileSynchronizer() { 123 | S3InboundFileSynchronizer synchronizer = new S3InboundFileSynchronizer(s3SessionFactory()); 124 | synchronizer.setDeleteRemoteFiles(true); 125 | synchronizer.setPreserveTimestamp(true); 126 | synchronizer.setRemoteDirectory(S3_BUCKET); 127 | synchronizer.setFilter(new S3RegexPatternFileListFilter(".*\\.test$")); 128 | Expression expression = PARSER.parseExpression( 129 | "(#this.contains('/') ? #this.substring(#this.lastIndexOf('/') + 1) : #this).toUpperCase() + '.a'"); 130 | synchronizer.setLocalFilenameGeneratorExpression(expression); 131 | return synchronizer; 132 | } 133 | 134 | @Bean 135 | @InboundChannelAdapter(value = "s3FilesChannel", poller = @Poller(fixedDelay = "100")) 136 | public S3InboundFileSynchronizingMessageSource s3InboundFileSynchronizingMessageSource() { 137 | S3InboundFileSynchronizingMessageSource messageSource = new S3InboundFileSynchronizingMessageSource( 138 | s3InboundFileSynchronizer()); 139 | messageSource.setAutoCreateLocalDirectory(true); 140 | messageSource.setLocalDirectory(LOCAL_FOLDER); 141 | messageSource.setLocalFilter(new AcceptOnceFileListFilter<>()); 142 | return messageSource; 143 | } 144 | 145 | @Bean 146 | public PollableChannel s3FilesChannel() { 147 | return new QueueChannel(); 148 | } 149 | 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/outbound/AbstractAwsMessageHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import java.util.Map; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.concurrent.ExecutionException; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.concurrent.TimeoutException; 24 | 25 | import com.amazonaws.handlers.AsyncHandler; 26 | import software.amazon.awssdk.awscore.AwsRequest; 27 | import software.amazon.awssdk.awscore.AwsResponse; 28 | 29 | import org.springframework.expression.EvaluationContext; 30 | import org.springframework.expression.Expression; 31 | import org.springframework.integration.MessageTimeoutException; 32 | import org.springframework.integration.aws.support.AwsHeaders; 33 | import org.springframework.integration.aws.support.AwsRequestFailureException; 34 | import org.springframework.integration.expression.ExpressionUtils; 35 | import org.springframework.integration.expression.ValueExpression; 36 | import org.springframework.integration.handler.AbstractMessageProducingHandler; 37 | import org.springframework.integration.mapping.HeaderMapper; 38 | import org.springframework.integration.support.ErrorMessageStrategy; 39 | import org.springframework.lang.Nullable; 40 | import org.springframework.messaging.Message; 41 | import org.springframework.util.Assert; 42 | 43 | /** 44 | * The base {@link AbstractMessageProducingHandler} for AWS services. Utilizes common 45 | * logic ({@link AsyncHandler}, {@link ErrorMessageStrategy}, {@code failureChannel} etc.) 46 | * and message pre- and post-processing, 47 | * 48 | * @param the headers container type. 49 | * 50 | * @author Artem Bilan 51 | * 52 | * @since 2.0 53 | */ 54 | public abstract class AbstractAwsMessageHandler extends AbstractMessageProducingHandler { 55 | 56 | protected static final long DEFAULT_SEND_TIMEOUT = 10000; 57 | 58 | private EvaluationContext evaluationContext; 59 | 60 | private Expression sendTimeoutExpression = new ValueExpression<>(DEFAULT_SEND_TIMEOUT); 61 | 62 | private HeaderMapper headerMapper; 63 | 64 | private boolean headerMapperSet; 65 | 66 | public void setSendTimeout(long sendTimeout) { 67 | setSendTimeoutExpression(new ValueExpression<>(sendTimeout)); 68 | } 69 | 70 | public void setSendTimeoutExpressionString(String sendTimeoutExpression) { 71 | setSendTimeoutExpression(EXPRESSION_PARSER.parseExpression(sendTimeoutExpression)); 72 | } 73 | 74 | public void setSendTimeoutExpression(Expression sendTimeoutExpression) { 75 | Assert.notNull(sendTimeoutExpression, "'sendTimeoutExpression' must not be null"); 76 | this.sendTimeoutExpression = sendTimeoutExpression; 77 | } 78 | 79 | protected Expression getSendTimeoutExpression() { 80 | return this.sendTimeoutExpression; 81 | } 82 | 83 | /** 84 | * Specify a {@link HeaderMapper} to map outbound headers. 85 | * @param headerMapper the {@link HeaderMapper} to map outbound headers. 86 | */ 87 | public void setHeaderMapper(HeaderMapper headerMapper) { 88 | this.headerMapper = headerMapper; 89 | this.headerMapperSet = true; 90 | } 91 | 92 | protected boolean isHeaderMapperSet() { 93 | return this.headerMapperSet; 94 | } 95 | 96 | /** 97 | * Set a {@link HeaderMapper} to use. 98 | * @param headerMapper the header mapper to set 99 | * @deprecated in favor of {@link #setHeaderMapper(HeaderMapper)} to be called from {@link #onInit()}. 100 | */ 101 | @Deprecated(forRemoval = true, since = "3.0.8") 102 | protected void doSetHeaderMapper(HeaderMapper headerMapper) { 103 | this.headerMapper = headerMapper; 104 | } 105 | 106 | protected HeaderMapper getHeaderMapper() { 107 | return this.headerMapper; 108 | } 109 | 110 | protected EvaluationContext getEvaluationContext() { 111 | return this.evaluationContext; 112 | } 113 | 114 | @Override 115 | protected void onInit() { 116 | super.onInit(); 117 | this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); 118 | } 119 | 120 | @Override 121 | protected boolean shouldCopyRequestHeaders() { 122 | return false; 123 | } 124 | 125 | @Override 126 | protected void handleMessageInternal(Message message) { 127 | AwsRequest request = messageToAwsRequest(message); 128 | CompletableFuture resultFuture = 129 | handleMessageToAws(message, request) 130 | .handle((response, ex) -> handleResponse(message, request, response, ex)); 131 | 132 | if (isAsync()) { 133 | sendOutputs(resultFuture, message); 134 | return; 135 | } 136 | 137 | Long sendTimeout = this.sendTimeoutExpression.getValue(this.evaluationContext, message, Long.class); 138 | if (sendTimeout == null || sendTimeout < 0) { 139 | try { 140 | resultFuture.get(); 141 | } 142 | catch (InterruptedException | ExecutionException ex) { 143 | throw new IllegalStateException(ex); 144 | } 145 | } 146 | else { 147 | try { 148 | resultFuture.get(sendTimeout, TimeUnit.MILLISECONDS); 149 | } 150 | catch (TimeoutException te) { 151 | throw new MessageTimeoutException(message, "Timeout waiting for response from AmazonKinesis", te); 152 | } 153 | catch (InterruptedException | ExecutionException ex) { 154 | throw new IllegalStateException(ex); 155 | } 156 | } 157 | } 158 | 159 | protected Message handleResponse(Message message, AwsRequest request, AwsResponse response, Throwable cause) { 160 | if (cause != null) { 161 | throw new AwsRequestFailureException(message, request, cause); 162 | } 163 | return getMessageBuilderFactory() 164 | .fromMessage(message) 165 | .copyHeadersIfAbsent(additionalOnSuccessHeaders(request, response)) 166 | .setHeaderIfAbsent(AwsHeaders.SERVICE_RESULT, response) 167 | .build(); 168 | } 169 | 170 | protected abstract AwsRequest messageToAwsRequest(Message message); 171 | 172 | protected abstract CompletableFuture handleMessageToAws(Message message, 173 | AwsRequest request); 174 | 175 | @Nullable 176 | protected abstract Map additionalOnSuccessHeaders(AwsRequest request, AwsResponse response); 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/outbound/SnsMessageHandlerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import java.util.Map; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.function.Consumer; 22 | 23 | import org.junit.jupiter.api.Test; 24 | import org.mockito.ArgumentCaptor; 25 | import software.amazon.awssdk.services.sns.SnsAsyncClient; 26 | import software.amazon.awssdk.services.sns.model.CreateTopicResponse; 27 | import software.amazon.awssdk.services.sns.model.MessageAttributeValue; 28 | import software.amazon.awssdk.services.sns.model.PublishRequest; 29 | import software.amazon.awssdk.services.sns.model.PublishResponse; 30 | 31 | import org.springframework.beans.factory.annotation.Autowired; 32 | import org.springframework.context.annotation.Bean; 33 | import org.springframework.context.annotation.Configuration; 34 | import org.springframework.expression.spel.standard.SpelExpressionParser; 35 | import org.springframework.integration.annotation.ServiceActivator; 36 | import org.springframework.integration.aws.support.AwsHeaders; 37 | import org.springframework.integration.aws.support.SnsBodyBuilder; 38 | import org.springframework.integration.aws.support.SnsHeaderMapper; 39 | import org.springframework.integration.channel.QueueChannel; 40 | import org.springframework.integration.config.EnableIntegration; 41 | import org.springframework.integration.support.MessageBuilder; 42 | import org.springframework.messaging.Message; 43 | import org.springframework.messaging.MessageChannel; 44 | import org.springframework.messaging.MessageHandler; 45 | import org.springframework.messaging.MessageHeaders; 46 | import org.springframework.messaging.PollableChannel; 47 | import org.springframework.test.annotation.DirtiesContext; 48 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 49 | 50 | import static org.assertj.core.api.Assertions.assertThat; 51 | import static org.mockito.ArgumentMatchers.any; 52 | import static org.mockito.BDDMockito.willAnswer; 53 | import static org.mockito.Mockito.mock; 54 | import static org.mockito.Mockito.verify; 55 | 56 | /** 57 | * @author Artem Bilan 58 | * @author Christopher Smith 59 | */ 60 | @SpringJUnitConfig 61 | @DirtiesContext 62 | class SnsMessageHandlerTests { 63 | 64 | private static final SpelExpressionParser PARSER = new SpelExpressionParser(); 65 | 66 | @Autowired 67 | private MessageChannel sendToSnsChannel; 68 | 69 | @Autowired 70 | private SnsAsyncClient amazonSNS; 71 | 72 | @Autowired 73 | private PollableChannel resultChannel; 74 | 75 | @Test 76 | void snsMessageHandler() { 77 | SnsBodyBuilder payload = SnsBodyBuilder.withDefault("foo").forProtocols("{\"foo\" : \"bar\"}", "sms"); 78 | 79 | Message message = MessageBuilder.withPayload(payload).setHeader("topic", "topic") 80 | .setHeader("subject", "subject").setHeader("foo", "bar").build(); 81 | 82 | this.sendToSnsChannel.send(message); 83 | 84 | Message reply = this.resultChannel.receive(10000); 85 | assertThat(reply).isNotNull(); 86 | 87 | ArgumentCaptor captor = ArgumentCaptor.forClass(PublishRequest.class); 88 | verify(this.amazonSNS).publish(captor.capture()); 89 | 90 | PublishRequest publishRequest = captor.getValue(); 91 | 92 | assertThat(publishRequest.messageStructure()).isEqualTo("json"); 93 | assertThat(publishRequest.topicArn()).isEqualTo("arn:aws:sns:eu-west-1:111111111111:topic.fifo"); 94 | assertThat(publishRequest.subject()).isEqualTo("subject"); 95 | assertThat(publishRequest.messageGroupId()).isEqualTo("SUBJECT"); 96 | assertThat(publishRequest.messageDeduplicationId()).isEqualTo("BAR"); 97 | assertThat(publishRequest.message()) 98 | .isEqualTo("{\"default\":\"foo\",\"sms\":\"{\\\"foo\\\" : \\\"bar\\\"}\"}"); 99 | 100 | Map messageAttributes = publishRequest.messageAttributes(); 101 | 102 | assertThat(messageAttributes) 103 | .doesNotContainKeys(MessageHeaders.ID, MessageHeaders.TIMESTAMP) 104 | .containsKey("foo"); 105 | assertThat(messageAttributes.get("foo").stringValue()).isEqualTo("bar"); 106 | 107 | assertThat(reply.getHeaders().get(AwsHeaders.MESSAGE_ID)).isEqualTo("111"); 108 | assertThat(reply.getHeaders().get(AwsHeaders.TOPIC)).isEqualTo("arn:aws:sns:eu-west-1:111111111111:topic.fifo"); 109 | assertThat(reply.getPayload()).isSameAs(payload); 110 | } 111 | 112 | @Configuration 113 | @EnableIntegration 114 | public static class ContextConfiguration { 115 | 116 | @Bean 117 | @SuppressWarnings("unchecked") 118 | public SnsAsyncClient amazonSNS() { 119 | SnsAsyncClient mock = mock(SnsAsyncClient.class); 120 | 121 | willAnswer(invocation -> 122 | CompletableFuture.completedFuture( 123 | CreateTopicResponse.builder() 124 | .topicArn("arn:aws:sns:eu-west-1:111111111111:topic.fifo") 125 | .build())) 126 | .given(mock) 127 | .createTopic(any(Consumer.class)); 128 | 129 | willAnswer(invocation -> 130 | CompletableFuture.completedFuture(PublishResponse.builder().messageId("111").build())) 131 | .given(mock) 132 | .publish(any(PublishRequest.class)); 133 | 134 | return mock; 135 | } 136 | 137 | @Bean 138 | public PollableChannel resultChannel() { 139 | return new QueueChannel(); 140 | } 141 | 142 | @Bean 143 | @ServiceActivator(inputChannel = "sendToSnsChannel") 144 | public MessageHandler snsMessageHandler() { 145 | SnsMessageHandler snsMessageHandler = new SnsMessageHandler(amazonSNS()); 146 | snsMessageHandler.setTopicArnExpression(PARSER.parseExpression("headers.topic")); 147 | snsMessageHandler.setMessageGroupIdExpression(PARSER.parseExpression("headers.subject.toUpperCase()")); 148 | snsMessageHandler.setMessageDeduplicationIdExpression(PARSER.parseExpression("headers.foo.toUpperCase()")); 149 | snsMessageHandler.setSubjectExpression(PARSER.parseExpression("headers.subject")); 150 | snsMessageHandler.setBodyExpression(PARSER.parseExpression("payload")); 151 | snsMessageHandler.setAsync(true); 152 | snsMessageHandler.setOutputChannel(resultChannel()); 153 | SnsHeaderMapper headerMapper = new SnsHeaderMapper(); 154 | headerMapper.setOutboundHeaderNames("foo"); 155 | snsMessageHandler.setHeaderMapper(headerMapper); 156 | return snsMessageHandler; 157 | } 158 | 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/kinesis/KinesisShardOffset.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound.kinesis; 18 | 19 | import java.time.Instant; 20 | import java.util.Objects; 21 | 22 | import software.amazon.awssdk.services.kinesis.model.GetShardIteratorRequest; 23 | import software.amazon.awssdk.services.kinesis.model.ShardIteratorType; 24 | 25 | import org.springframework.util.Assert; 26 | 27 | /** 28 | * A model to represent a sequence in the shard for particular {@link ShardIteratorType}. 29 | * 30 | * @author Artem Bilan 31 | * @since 1.1 32 | */ 33 | public class KinesisShardOffset { 34 | 35 | private ShardIteratorType iteratorType; 36 | 37 | private String sequenceNumber; 38 | 39 | private Instant timestamp; 40 | 41 | private String stream; 42 | 43 | private String shard; 44 | 45 | private boolean reset; 46 | 47 | public KinesisShardOffset(ShardIteratorType iteratorType) { 48 | Assert.notNull(iteratorType, "'iteratorType' must not be null."); 49 | this.iteratorType = iteratorType; 50 | } 51 | 52 | public KinesisShardOffset(KinesisShardOffset other) { 53 | this.iteratorType = other.getIteratorType(); 54 | this.stream = other.getStream(); 55 | this.shard = other.getShard(); 56 | this.sequenceNumber = other.getSequenceNumber(); 57 | this.timestamp = other.getTimestamp(); 58 | this.reset = other.isReset(); 59 | } 60 | 61 | public void setIteratorType(ShardIteratorType iteratorType) { 62 | this.iteratorType = iteratorType; 63 | } 64 | 65 | public ShardIteratorType getIteratorType() { 66 | return this.iteratorType; 67 | } 68 | 69 | public void setSequenceNumber(String sequenceNumber) { 70 | this.sequenceNumber = sequenceNumber; 71 | } 72 | 73 | public void setTimestamp(Instant timestamp) { 74 | this.timestamp = timestamp; 75 | } 76 | 77 | public void setStream(String stream) { 78 | this.stream = stream; 79 | } 80 | 81 | public void setShard(String shard) { 82 | this.shard = shard; 83 | } 84 | 85 | public void setReset(boolean reset) { 86 | this.reset = reset; 87 | } 88 | 89 | public String getSequenceNumber() { 90 | return this.sequenceNumber; 91 | } 92 | 93 | public Instant getTimestamp() { 94 | return this.timestamp; 95 | } 96 | 97 | public String getStream() { 98 | return this.stream; 99 | } 100 | 101 | public String getShard() { 102 | return this.shard; 103 | } 104 | 105 | public boolean isReset() { 106 | return this.reset; 107 | } 108 | 109 | public KinesisShardOffset reset() { 110 | this.reset = true; 111 | return this; 112 | } 113 | 114 | public GetShardIteratorRequest toShardIteratorRequest() { 115 | Assert.state(this.stream != null && this.shard != null, 116 | "'stream' and 'shard' must not be null for conversion to the GetShardIteratorRequest."); 117 | return GetShardIteratorRequest.builder() 118 | .streamName(this.stream) 119 | .shardId(this.shard) 120 | .shardIteratorType(this.iteratorType) 121 | .startingSequenceNumber(this.sequenceNumber) 122 | .timestamp(this.timestamp) 123 | .build(); 124 | } 125 | 126 | @Override 127 | public boolean equals(Object o) { 128 | if (this == o) { 129 | return true; 130 | } 131 | if (o == null || getClass() != o.getClass()) { 132 | return false; 133 | } 134 | KinesisShardOffset that = (KinesisShardOffset) o; 135 | return Objects.equals(this.stream, that.stream) && Objects.equals(this.shard, that.shard); 136 | } 137 | 138 | @Override 139 | public int hashCode() { 140 | return Objects.hash(this.stream, this.shard); 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | return "KinesisShardOffset{" + "iteratorType=" + this.iteratorType + ", sequenceNumber='" + this.sequenceNumber 146 | + '\'' + ", timestamp=" + this.timestamp + ", stream='" + this.stream + '\'' + ", shard='" + this.shard 147 | + '\'' + ", reset=" + this.reset + '}'; 148 | } 149 | 150 | public static KinesisShardOffset latest() { 151 | return latest(null, null); 152 | } 153 | 154 | public static KinesisShardOffset latest(String stream, String shard) { 155 | KinesisShardOffset kinesisShardOffset = new KinesisShardOffset(ShardIteratorType.LATEST); 156 | kinesisShardOffset.stream = stream; 157 | kinesisShardOffset.shard = shard; 158 | return kinesisShardOffset; 159 | } 160 | 161 | public static KinesisShardOffset trimHorizon() { 162 | return trimHorizon(null, null); 163 | } 164 | 165 | public static KinesisShardOffset trimHorizon(String stream, String shard) { 166 | KinesisShardOffset kinesisShardOffset = new KinesisShardOffset(ShardIteratorType.TRIM_HORIZON); 167 | kinesisShardOffset.stream = stream; 168 | kinesisShardOffset.shard = shard; 169 | return kinesisShardOffset; 170 | } 171 | 172 | public static KinesisShardOffset atSequenceNumber(String sequenceNumber) { 173 | return atSequenceNumber(null, null, sequenceNumber); 174 | } 175 | 176 | public static KinesisShardOffset atSequenceNumber(String stream, String shard, String sequenceNumber) { 177 | KinesisShardOffset kinesisShardOffset = new KinesisShardOffset(ShardIteratorType.AT_SEQUENCE_NUMBER); 178 | kinesisShardOffset.stream = stream; 179 | kinesisShardOffset.shard = shard; 180 | kinesisShardOffset.sequenceNumber = sequenceNumber; 181 | return kinesisShardOffset; 182 | } 183 | 184 | public static KinesisShardOffset afterSequenceNumber(String sequenceNumber) { 185 | return afterSequenceNumber(null, null, sequenceNumber); 186 | } 187 | 188 | public static KinesisShardOffset afterSequenceNumber(String stream, String shard, String sequenceNumber) { 189 | KinesisShardOffset kinesisShardOffset = new KinesisShardOffset(ShardIteratorType.AFTER_SEQUENCE_NUMBER); 190 | kinesisShardOffset.stream = stream; 191 | kinesisShardOffset.shard = shard; 192 | kinesisShardOffset.sequenceNumber = sequenceNumber; 193 | return kinesisShardOffset; 194 | } 195 | 196 | public static KinesisShardOffset atTimestamp(Instant timestamp) { 197 | return atTimestamp(null, null, timestamp); 198 | } 199 | 200 | public static KinesisShardOffset atTimestamp(String stream, String shard, Instant timestamp) { 201 | KinesisShardOffset kinesisShardOffset = new KinesisShardOffset(ShardIteratorType.AT_TIMESTAMP); 202 | kinesisShardOffset.stream = stream; 203 | kinesisShardOffset.shard = shard; 204 | kinesisShardOffset.timestamp = timestamp; 205 | return kinesisShardOffset; 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapterTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.util.Map; 20 | 21 | import io.awspring.cloud.sns.handlers.NotificationStatus; 22 | import org.junit.jupiter.api.BeforeEach; 23 | import org.junit.jupiter.api.Test; 24 | import software.amazon.awssdk.services.sns.SnsClient; 25 | import software.amazon.awssdk.services.sns.model.ConfirmSubscriptionRequest; 26 | 27 | import org.springframework.beans.factory.annotation.Autowired; 28 | import org.springframework.beans.factory.annotation.Value; 29 | import org.springframework.context.annotation.Bean; 30 | import org.springframework.context.annotation.Configuration; 31 | import org.springframework.core.io.Resource; 32 | import org.springframework.http.MediaType; 33 | import org.springframework.integration.aws.support.AwsHeaders; 34 | import org.springframework.integration.channel.QueueChannel; 35 | import org.springframework.integration.config.EnableIntegration; 36 | import org.springframework.messaging.Message; 37 | import org.springframework.messaging.PollableChannel; 38 | import org.springframework.test.annotation.DirtiesContext; 39 | import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; 40 | import org.springframework.test.web.servlet.MockMvc; 41 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 42 | import org.springframework.util.StreamUtils; 43 | import org.springframework.web.HttpRequestHandler; 44 | import org.springframework.web.context.WebApplicationContext; 45 | 46 | import static org.assertj.core.api.Assertions.assertThat; 47 | import static org.mockito.BDDMockito.verify; 48 | import static org.mockito.Mockito.mock; 49 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 50 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 51 | 52 | /** 53 | * @author Artem Bilan 54 | * @author Kamil Przerwa 55 | */ 56 | @SpringJUnitWebConfig 57 | @DirtiesContext 58 | class SnsInboundChannelAdapterTests { 59 | 60 | @Autowired 61 | private WebApplicationContext context; 62 | 63 | @Autowired 64 | private SnsClient amazonSns; 65 | 66 | @Autowired 67 | private PollableChannel inputChannel; 68 | 69 | @Value("classpath:org/springframework/integration/aws/inbound/subscriptionConfirmation.json") 70 | private Resource subscriptionConfirmation; 71 | 72 | @Value("classpath:org/springframework/integration/aws/inbound/notificationMessage.json") 73 | private Resource notificationMessage; 74 | 75 | @Value("classpath:org/springframework/integration/aws/inbound/unsubscribeConfirmation.json") 76 | private Resource unsubscribeConfirmation; 77 | 78 | private MockMvc mockMvc; 79 | 80 | @BeforeEach 81 | void setUp() { 82 | this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); 83 | } 84 | 85 | @Test 86 | void subscriptionConfirmation() throws Exception { 87 | this.mockMvc 88 | .perform(post("/mySampleTopic").header("x-amz-sns-message-type", "SubscriptionConfirmation") 89 | .contentType(MediaType.APPLICATION_JSON) 90 | .content(StreamUtils.copyToByteArray(this.subscriptionConfirmation.getInputStream()))) 91 | .andExpect(status().isNoContent()); 92 | 93 | Message receive = this.inputChannel.receive(10000); 94 | assertThat(receive).isNotNull(); 95 | assertThat(receive.getHeaders().containsKey(AwsHeaders.SNS_MESSAGE_TYPE)).isTrue(); 96 | assertThat(receive.getHeaders().get(AwsHeaders.SNS_MESSAGE_TYPE)).isEqualTo("SubscriptionConfirmation"); 97 | 98 | assertThat(receive.getHeaders().containsKey(AwsHeaders.NOTIFICATION_STATUS)).isTrue(); 99 | NotificationStatus notificationStatus = (NotificationStatus) receive.getHeaders() 100 | .get(AwsHeaders.NOTIFICATION_STATUS); 101 | 102 | notificationStatus.confirmSubscription(); 103 | 104 | verify(this.amazonSns).confirmSubscription( 105 | ConfirmSubscriptionRequest.builder() 106 | .topicArn("arn:aws:sns:eu-west-1:111111111111:mySampleTopic") 107 | .token("111") 108 | .build()); 109 | } 110 | 111 | @Test 112 | @SuppressWarnings("unchecked") 113 | void notification() throws Exception { 114 | this.mockMvc 115 | .perform(post("/mySampleTopic").header("x-amz-sns-message-type", "Notification") 116 | .contentType(MediaType.TEXT_PLAIN) 117 | .content(StreamUtils.copyToByteArray(this.notificationMessage.getInputStream()))) 118 | .andExpect(status().isNoContent()); 119 | 120 | Message receive = this.inputChannel.receive(10000); 121 | assertThat(receive).isNotNull(); 122 | Map payload = (Map) receive.getPayload(); 123 | 124 | assertThat(payload) 125 | .containsEntry("Subject", "foo") 126 | .containsEntry("Message", "bar"); 127 | } 128 | 129 | @Test 130 | void unsubscribe() throws Exception { 131 | this.mockMvc 132 | .perform(post("/mySampleTopic").header("x-amz-sns-message-type", "UnsubscribeConfirmation") 133 | .contentType(MediaType.TEXT_PLAIN) 134 | .content(StreamUtils.copyToByteArray(this.unsubscribeConfirmation.getInputStream()))) 135 | .andExpect(status().isNoContent()); 136 | 137 | Message receive = this.inputChannel.receive(10000); 138 | assertThat(receive).isNotNull(); 139 | assertThat(receive.getHeaders().containsKey(AwsHeaders.SNS_MESSAGE_TYPE)).isTrue(); 140 | assertThat(receive.getHeaders().get(AwsHeaders.SNS_MESSAGE_TYPE)).isEqualTo("UnsubscribeConfirmation"); 141 | 142 | assertThat(receive.getHeaders().containsKey(AwsHeaders.NOTIFICATION_STATUS)).isTrue(); 143 | NotificationStatus notificationStatus = (NotificationStatus) receive.getHeaders() 144 | .get(AwsHeaders.NOTIFICATION_STATUS); 145 | 146 | notificationStatus.confirmSubscription(); 147 | 148 | verify(this.amazonSns).confirmSubscription( 149 | ConfirmSubscriptionRequest.builder() 150 | .topicArn("arn:aws:sns:eu-west-1:111111111111:mySampleTopic") 151 | .token("233") 152 | .build()); 153 | } 154 | 155 | @Configuration 156 | @EnableIntegration 157 | public static class ContextConfiguration { 158 | 159 | @Bean 160 | public SnsClient amazonSns() { 161 | return mock(SnsClient.class); 162 | } 163 | 164 | @Bean 165 | public PollableChannel inputChannel() { 166 | return new QueueChannel(); 167 | } 168 | 169 | @Bean 170 | public HttpRequestHandler snsInboundChannelAdapter() { 171 | SnsInboundChannelAdapter adapter = new SnsInboundChannelAdapter(amazonSns(), "/mySampleTopic"); 172 | adapter.setRequestChannel(inputChannel()); 173 | adapter.setHandleNotificationStatus(true); 174 | return adapter; 175 | } 176 | 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/kinesis/KclMessageDrivenChannelAdapterMultiStreamTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.kinesis; 18 | 19 | import java.util.List; 20 | import java.util.concurrent.CompletableFuture; 21 | 22 | import org.junit.jupiter.api.AfterAll; 23 | import org.junit.jupiter.api.BeforeAll; 24 | import org.junit.jupiter.api.Test; 25 | import software.amazon.awssdk.core.SdkBytes; 26 | import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; 27 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 28 | import software.amazon.awssdk.services.kinesis.KinesisAsyncClient; 29 | import software.amazon.awssdk.services.kinesis.model.Consumer; 30 | import software.amazon.kinesis.common.InitialPositionInStream; 31 | import software.amazon.kinesis.common.InitialPositionInStreamExtended; 32 | 33 | import org.springframework.beans.factory.annotation.Autowired; 34 | import org.springframework.context.annotation.Bean; 35 | import org.springframework.context.annotation.Configuration; 36 | import org.springframework.integration.aws.LocalstackContainerTest; 37 | import org.springframework.integration.aws.inbound.kinesis.KclMessageDrivenChannelAdapter; 38 | import org.springframework.integration.aws.support.AwsHeaders; 39 | import org.springframework.integration.channel.QueueChannel; 40 | import org.springframework.integration.config.EnableIntegration; 41 | import org.springframework.messaging.Message; 42 | import org.springframework.messaging.PollableChannel; 43 | import org.springframework.test.annotation.DirtiesContext; 44 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 45 | 46 | import static org.assertj.core.api.Assertions.assertThat; 47 | 48 | /** 49 | * @author Siddharth Jain 50 | * @author Artem Bilan 51 | * 52 | * @since 3.0 53 | */ 54 | @SpringJUnitConfig 55 | @DirtiesContext 56 | class KclMessageDrivenChannelAdapterMultiStreamTests implements LocalstackContainerTest { 57 | 58 | private static final String TEST_STREAM1 = "MultiStreamKcl1"; 59 | 60 | private static final String TEST_STREAM2 = "MultiStreamKcl2"; 61 | 62 | private static KinesisAsyncClient AMAZON_KINESIS; 63 | 64 | private static DynamoDbAsyncClient DYNAMO_DB; 65 | 66 | private static CloudWatchAsyncClient CLOUD_WATCH; 67 | 68 | @Autowired 69 | private PollableChannel kinesisReceiveChannel; 70 | 71 | @BeforeAll 72 | static void setup() { 73 | AMAZON_KINESIS = LocalstackContainerTest.kinesisClient(); 74 | DYNAMO_DB = LocalstackContainerTest.dynamoDbClient(); 75 | CLOUD_WATCH = LocalstackContainerTest.cloudWatchClient(); 76 | 77 | CompletableFuture completableFuture1 = 78 | AMAZON_KINESIS.createStream(request -> request.streamName(TEST_STREAM1).shardCount(1)) 79 | .thenCompose(result -> AMAZON_KINESIS.waiter() 80 | .waitUntilStreamExists(request -> request.streamName(TEST_STREAM1))); 81 | 82 | CompletableFuture completableFuture2 = 83 | AMAZON_KINESIS.createStream(request -> request.streamName(TEST_STREAM2).shardCount(1)) 84 | .thenCompose(result -> AMAZON_KINESIS.waiter() 85 | .waitUntilStreamExists(request -> request.streamName(TEST_STREAM2))); 86 | 87 | CompletableFuture.allOf(completableFuture1, completableFuture2).join(); 88 | } 89 | 90 | @AfterAll 91 | static void tearDown() { 92 | CompletableFuture completableFuture1 = 93 | AMAZON_KINESIS.deleteStream(request -> request.streamName(TEST_STREAM1).enforceConsumerDeletion(true)) 94 | .thenCompose(result -> AMAZON_KINESIS.waiter() 95 | .waitUntilStreamNotExists(request -> request.streamName(TEST_STREAM1))); 96 | 97 | CompletableFuture completableFuture2 = 98 | AMAZON_KINESIS.deleteStream(request -> request.streamName(TEST_STREAM2).enforceConsumerDeletion(true)) 99 | .thenCompose(result -> AMAZON_KINESIS.waiter() 100 | .waitUntilStreamNotExists(request -> request.streamName(TEST_STREAM2))); 101 | 102 | CompletableFuture.allOf(completableFuture1, completableFuture2).join(); 103 | } 104 | 105 | @Test 106 | void kclChannelAdapterMultiStream() { 107 | String testData = "test data"; 108 | AMAZON_KINESIS.putRecord(request -> request 109 | .streamName(TEST_STREAM1) 110 | .data(SdkBytes.fromUtf8String(testData)) 111 | .partitionKey("test")); 112 | 113 | String testData2 = "test data 2"; 114 | AMAZON_KINESIS.putRecord(request -> request 115 | .streamName(TEST_STREAM2) 116 | .data(SdkBytes.fromUtf8String(testData2)) 117 | .partitionKey("test")); 118 | 119 | // The below statement works but with a higher timeout. For 2 streams, this takes too long. 120 | Message receive = this.kinesisReceiveChannel.receive(300_000); 121 | assertThat(receive).isNotNull(); 122 | assertThat(receive.getPayload()).isIn(testData, testData2); 123 | assertThat(receive.getHeaders().get(AwsHeaders.RECEIVED_SEQUENCE_NUMBER, String.class)).isNotEmpty(); 124 | 125 | receive = this.kinesisReceiveChannel.receive(10_000); 126 | assertThat(receive).isNotNull(); 127 | assertThat(receive.getPayload()).isIn(testData, testData2); 128 | 129 | List stream1Consumers = 130 | AMAZON_KINESIS.describeStream(request -> request.streamName(TEST_STREAM1)) 131 | .thenCompose(describeStreamResponse -> 132 | AMAZON_KINESIS.listStreamConsumers(request -> 133 | request.streamARN(describeStreamResponse.streamDescription().streamARN()))) 134 | .join() 135 | .consumers(); 136 | 137 | List stream2Consumers = AMAZON_KINESIS 138 | .describeStream(request -> request.streamName(TEST_STREAM2)) 139 | .thenCompose(describeStreamResponse -> 140 | AMAZON_KINESIS.listStreamConsumers(request -> 141 | request.streamARN(describeStreamResponse.streamDescription().streamARN()))) 142 | .join() 143 | .consumers(); 144 | 145 | assertThat(stream1Consumers).hasSize(1); 146 | assertThat(stream2Consumers).hasSize(1); 147 | } 148 | 149 | @Configuration 150 | @EnableIntegration 151 | public static class TestConfiguration { 152 | 153 | @Bean 154 | public KclMessageDrivenChannelAdapter kclMessageDrivenChannelAdapter() { 155 | KclMessageDrivenChannelAdapter adapter = new KclMessageDrivenChannelAdapter( 156 | AMAZON_KINESIS, CLOUD_WATCH, DYNAMO_DB, TEST_STREAM1, TEST_STREAM2); 157 | adapter.setOutputChannel(kinesisReceiveChannel()); 158 | adapter.setStreamInitialSequence( 159 | InitialPositionInStreamExtended.newInitialPosition(InitialPositionInStream.TRIM_HORIZON)); 160 | adapter.setConverter(String::new); 161 | adapter.setConsumerGroup("multi_stream_group"); 162 | return adapter; 163 | } 164 | 165 | @Bean 166 | public PollableChannel kinesisReceiveChannel() { 167 | return new QueueChannel(); 168 | } 169 | 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/outbound/KinesisMessageHandlerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | import org.junit.jupiter.api.Test; 22 | import org.mockito.ArgumentCaptor; 23 | import software.amazon.awssdk.core.SdkBytes; 24 | import software.amazon.awssdk.services.kinesis.KinesisAsyncClient; 25 | import software.amazon.awssdk.services.kinesis.model.PutRecordRequest; 26 | import software.amazon.awssdk.services.kinesis.model.PutRecordsRequest; 27 | import software.amazon.awssdk.services.kinesis.model.PutRecordsRequestEntry; 28 | 29 | import org.springframework.beans.factory.annotation.Autowired; 30 | import org.springframework.context.annotation.Bean; 31 | import org.springframework.context.annotation.Configuration; 32 | import org.springframework.core.serializer.support.SerializingConverter; 33 | import org.springframework.integration.annotation.ServiceActivator; 34 | import org.springframework.integration.aws.support.AwsHeaders; 35 | import org.springframework.integration.config.EnableIntegration; 36 | import org.springframework.integration.support.json.EmbeddedJsonHeadersMessageMapper; 37 | import org.springframework.messaging.Message; 38 | import org.springframework.messaging.MessageChannel; 39 | import org.springframework.messaging.MessageHandler; 40 | import org.springframework.messaging.MessageHandlingException; 41 | import org.springframework.messaging.MessageHeaders; 42 | import org.springframework.messaging.converter.MessageConverter; 43 | import org.springframework.messaging.support.GenericMessage; 44 | import org.springframework.messaging.support.MessageBuilder; 45 | import org.springframework.test.annotation.DirtiesContext; 46 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 47 | 48 | import static org.assertj.core.api.Assertions.assertThat; 49 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 50 | import static org.assertj.core.api.Assertions.entry; 51 | import static org.mockito.ArgumentMatchers.any; 52 | import static org.mockito.BDDMockito.given; 53 | import static org.mockito.Mockito.mock; 54 | import static org.mockito.Mockito.verify; 55 | 56 | /** 57 | * @author Artem Bilan 58 | * 59 | * @since 1.1 60 | */ 61 | @SpringJUnitConfig 62 | @DirtiesContext 63 | class KinesisMessageHandlerTests { 64 | 65 | @Autowired 66 | protected KinesisAsyncClient amazonKinesis; 67 | 68 | @Autowired 69 | protected MessageChannel kinesisSendChannel; 70 | 71 | @Autowired 72 | protected KinesisMessageHandler kinesisMessageHandler; 73 | 74 | @Test 75 | @SuppressWarnings("unchecked") 76 | void kinesisMessageHandler() { 77 | final Message message = MessageBuilder.withPayload("message").build(); 78 | 79 | assertThatExceptionOfType(MessageHandlingException.class) 80 | .isThrownBy(() -> this.kinesisSendChannel.send(message)) 81 | .withCauseInstanceOf(IllegalStateException.class) 82 | .withStackTraceContaining("'stream' must not be null for sending a Kinesis record"); 83 | 84 | this.kinesisMessageHandler.setStream("foo"); 85 | 86 | assertThatExceptionOfType(MessageHandlingException.class) 87 | .isThrownBy(() -> this.kinesisSendChannel.send(message)) 88 | .withCauseInstanceOf(IllegalStateException.class) 89 | .withStackTraceContaining("'partitionKey' must not be null for sending a Kinesis record"); 90 | 91 | Message message2 = MessageBuilder.fromMessage(message).setHeader(AwsHeaders.PARTITION_KEY, "fooKey") 92 | .setHeader(AwsHeaders.SEQUENCE_NUMBER, "10").setHeader("foo", "bar").build(); 93 | 94 | this.kinesisSendChannel.send(message2); 95 | 96 | ArgumentCaptor putRecordRequestArgumentCaptor = ArgumentCaptor 97 | .forClass(PutRecordRequest.class); 98 | 99 | verify(this.amazonKinesis).putRecord(putRecordRequestArgumentCaptor.capture()); 100 | 101 | PutRecordRequest putRecordRequest = putRecordRequestArgumentCaptor.getValue(); 102 | 103 | assertThat(putRecordRequest.streamName()).isEqualTo("foo"); 104 | assertThat(putRecordRequest.partitionKey()).isEqualTo("fooKey"); 105 | assertThat(putRecordRequest.sequenceNumberForOrdering()).isEqualTo("10"); 106 | assertThat(putRecordRequest.explicitHashKey()).isNull(); 107 | 108 | Message messageToCheck = new EmbeddedJsonHeadersMessageMapper() 109 | .toMessage(putRecordRequest.data().asByteArray()); 110 | 111 | assertThat(messageToCheck.getHeaders()).contains(entry("foo", "bar")); 112 | assertThat(messageToCheck.getPayload()).isEqualTo("message".getBytes()); 113 | 114 | message2 = new GenericMessage<>(PutRecordsRequest.builder() 115 | .streamName("myStream").records(request -> 116 | request.data(SdkBytes.fromByteArray("test".getBytes())) 117 | .partitionKey("testKey")) 118 | .build()); 119 | 120 | this.kinesisSendChannel.send(message2); 121 | 122 | ArgumentCaptor putRecordsRequestArgumentCaptor = ArgumentCaptor 123 | .forClass(PutRecordsRequest.class); 124 | verify(this.amazonKinesis).putRecords(putRecordsRequestArgumentCaptor.capture()); 125 | 126 | PutRecordsRequest putRecordsRequest = putRecordsRequestArgumentCaptor.getValue(); 127 | 128 | assertThat(putRecordsRequest.streamName()).isEqualTo("myStream"); 129 | assertThat(putRecordsRequest.records()) 130 | .containsExactlyInAnyOrder( 131 | PutRecordsRequestEntry.builder() 132 | .data(SdkBytes.fromByteArray("test".getBytes())) 133 | .partitionKey("testKey") 134 | .build()); 135 | } 136 | 137 | @Configuration 138 | @EnableIntegration 139 | public static class ContextConfiguration { 140 | 141 | @Bean 142 | @SuppressWarnings("unchecked") 143 | public KinesisAsyncClient amazonKinesis() { 144 | KinesisAsyncClient mock = mock(KinesisAsyncClient.class); 145 | 146 | given(mock.putRecord(any(PutRecordRequest.class))) 147 | .willReturn(mock(CompletableFuture.class)); 148 | 149 | given(mock.putRecords(any(PutRecordsRequest.class))) 150 | .willReturn(mock(CompletableFuture.class)); 151 | 152 | return mock; 153 | } 154 | 155 | @Bean 156 | @ServiceActivator(inputChannel = "kinesisSendChannel") 157 | public MessageHandler kinesisMessageHandler() { 158 | KinesisMessageHandler kinesisMessageHandler = new KinesisMessageHandler(amazonKinesis()); 159 | kinesisMessageHandler.setAsync(true); 160 | kinesisMessageHandler.setMessageConverter(new MessageConverter() { 161 | 162 | private SerializingConverter serializingConverter = new SerializingConverter(); 163 | 164 | @Override 165 | public Object fromMessage(Message message, Class targetClass) { 166 | Object source = message.getPayload(); 167 | if (source instanceof String) { 168 | return ((String) source).getBytes(); 169 | } 170 | else { 171 | return this.serializingConverter.convert(source); 172 | } 173 | } 174 | 175 | @Override 176 | public Message toMessage(Object payload, MessageHeaders headers) { 177 | return null; 178 | } 179 | 180 | }); 181 | kinesisMessageHandler.setEmbeddedHeadersMapper(new EmbeddedJsonHeadersMessageMapper("foo")); 182 | return kinesisMessageHandler; 183 | } 184 | 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/outbound/SqsMessageHandlerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import java.util.Map; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.concurrent.atomic.AtomicReference; 22 | 23 | import io.awspring.cloud.sqs.listener.QueueNotFoundStrategy; 24 | import org.junit.jupiter.api.BeforeAll; 25 | import org.junit.jupiter.api.Test; 26 | import software.amazon.awssdk.services.sqs.SqsAsyncClient; 27 | import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; 28 | import software.amazon.awssdk.services.sqs.model.QueueAttributeName; 29 | import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse; 30 | 31 | import org.springframework.beans.factory.annotation.Autowired; 32 | import org.springframework.context.annotation.Bean; 33 | import org.springframework.context.annotation.Configuration; 34 | import org.springframework.expression.Expression; 35 | import org.springframework.expression.spel.standard.SpelExpressionParser; 36 | import org.springframework.integration.annotation.ServiceActivator; 37 | import org.springframework.integration.aws.LocalstackContainerTest; 38 | import org.springframework.integration.aws.support.AwsHeaders; 39 | import org.springframework.integration.config.EnableIntegration; 40 | import org.springframework.messaging.Message; 41 | import org.springframework.messaging.MessageChannel; 42 | import org.springframework.messaging.MessageHandler; 43 | import org.springframework.messaging.MessageHandlingException; 44 | import org.springframework.messaging.MessageHeaders; 45 | import org.springframework.messaging.support.MessageBuilder; 46 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 47 | 48 | import static org.assertj.core.api.Assertions.assertThat; 49 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 50 | 51 | /** 52 | * Instantiating SqsMessageHandler using amazonSqs. 53 | * 54 | * @author Artem Bilan 55 | * @author Rahul Pilani 56 | * @author Seth Kelly 57 | */ 58 | @SpringJUnitConfig 59 | class SqsMessageHandlerTests implements LocalstackContainerTest { 60 | 61 | private static final AtomicReference fooUrl = new AtomicReference<>(); 62 | 63 | private static final AtomicReference barUrl = new AtomicReference<>(); 64 | 65 | private static final AtomicReference bazUrl = new AtomicReference<>(); 66 | 67 | private static SqsAsyncClient AMAZON_SQS; 68 | 69 | @Autowired 70 | protected MessageChannel sqsSendChannel; 71 | 72 | @Autowired 73 | protected MessageChannel sqsSendChannelWithAutoCreate; 74 | 75 | @Autowired 76 | protected SqsMessageHandler sqsMessageHandler; 77 | 78 | @BeforeAll 79 | static void setup() { 80 | AMAZON_SQS = LocalstackContainerTest.sqsClient(); 81 | CompletableFuture foo = 82 | AMAZON_SQS.createQueue(request -> request.queueName("foo")) 83 | .thenAccept(response -> fooUrl.set(response.queueUrl())); 84 | CompletableFuture bar = 85 | AMAZON_SQS.createQueue(request -> request.queueName("bar")) 86 | .thenAccept(response -> barUrl.set(response.queueUrl())); 87 | CompletableFuture baz = 88 | AMAZON_SQS.createQueue(request -> request.queueName("baz")) 89 | .thenAccept(response -> bazUrl.set(response.queueUrl())); 90 | 91 | CompletableFuture.allOf(foo, bar, baz).join(); 92 | } 93 | 94 | @Test 95 | void sqsMessageHandler() { 96 | final Message message = MessageBuilder.withPayload("message").build(); 97 | 98 | assertThatExceptionOfType(MessageHandlingException.class) 99 | .isThrownBy(() -> this.sqsSendChannel.send(message)) 100 | .withCauseInstanceOf(IllegalStateException.class); 101 | 102 | this.sqsMessageHandler.setQueue("foo"); 103 | this.sqsSendChannel.send(message); 104 | 105 | ReceiveMessageResponse receiveMessageResponse = 106 | AMAZON_SQS.receiveMessage(request -> request.queueUrl(fooUrl.get()).waitTimeSeconds(10)) 107 | .join(); 108 | 109 | assertThat(receiveMessageResponse.hasMessages()).isTrue(); 110 | assertThat(receiveMessageResponse.messages().get(0).body()).isEqualTo("message"); 111 | 112 | Message message2 = MessageBuilder.withPayload("message").setHeader(AwsHeaders.QUEUE, "bar").build(); 113 | this.sqsSendChannel.send(message2); 114 | 115 | receiveMessageResponse = 116 | AMAZON_SQS.receiveMessage(request -> request.queueUrl(barUrl.get()).waitTimeSeconds(10)) 117 | .join(); 118 | 119 | assertThat(receiveMessageResponse.hasMessages()).isTrue(); 120 | assertThat(receiveMessageResponse.messages().get(0).body()).isEqualTo("message"); 121 | 122 | 123 | SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); 124 | Expression expression = spelExpressionParser.parseExpression("headers.foo"); 125 | this.sqsMessageHandler.setQueueExpression(expression); 126 | message2 = MessageBuilder.withPayload("message").setHeader("foo", "baz").build(); 127 | this.sqsSendChannel.send(message2); 128 | 129 | receiveMessageResponse = 130 | AMAZON_SQS.receiveMessage(request -> 131 | request.queueUrl(bazUrl.get()) 132 | .messageAttributeNames(QueueAttributeName.ALL.toString()) 133 | .waitTimeSeconds(10)) 134 | .join(); 135 | 136 | assertThat(receiveMessageResponse.hasMessages()).isTrue(); 137 | software.amazon.awssdk.services.sqs.model.Message message1 = receiveMessageResponse.messages().get(0); 138 | assertThat(message1.body()).isEqualTo("message"); 139 | 140 | Map messageAttributes = message1.messageAttributes(); 141 | 142 | assertThat(messageAttributes) 143 | .doesNotContainKeys(MessageHeaders.ID, MessageHeaders.TIMESTAMP) 144 | .containsKey("foo"); 145 | assertThat(messageAttributes.get("foo").stringValue()).isEqualTo("baz"); 146 | } 147 | 148 | @Test 149 | void sqsMessageHandlerWithAutoQueueCreate() { 150 | Message message = MessageBuilder.withPayload("message").build(); 151 | 152 | this.sqsSendChannelWithAutoCreate.send(message); 153 | 154 | ReceiveMessageResponse autoCreateQueueResponse = 155 | AMAZON_SQS.getQueueUrl(request -> request.queueName("autoCreateQueue")) 156 | .thenCompose(response -> 157 | AMAZON_SQS.receiveMessage(request -> 158 | request.queueUrl(response.queueUrl()).waitTimeSeconds(10))) 159 | .join(); 160 | 161 | assertThat(autoCreateQueueResponse.hasMessages()).isTrue(); 162 | assertThat(autoCreateQueueResponse.messages().get(0).body()).isEqualTo("message"); 163 | } 164 | 165 | @Configuration 166 | @EnableIntegration 167 | public static class ContextConfiguration { 168 | 169 | @Bean 170 | @ServiceActivator(inputChannel = "sqsSendChannel") 171 | public MessageHandler sqsMessageHandler() { 172 | return new SqsMessageHandler(AMAZON_SQS); 173 | } 174 | 175 | @Bean 176 | @ServiceActivator(inputChannel = "sqsSendChannelWithAutoCreate") 177 | public MessageHandler sqsMessageHandlerWithQueueAutoCreate() { 178 | SqsMessageHandler sqsMessageHandler = new SqsMessageHandler(AMAZON_SQS); 179 | sqsMessageHandler.setQueueNotFoundStrategy(QueueNotFoundStrategy.CREATE); 180 | sqsMessageHandler.setQueue("autoCreateQueue"); 181 | return sqsMessageHandler; 182 | } 183 | 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 121 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 195 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/outbound/KplMessageHandlerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import com.amazonaws.services.kinesis.producer.KinesisProducer; 20 | import com.amazonaws.services.kinesis.producer.UserRecord; 21 | import com.amazonaws.services.schemaregistry.common.Schema; 22 | import org.junit.jupiter.api.AfterEach; 23 | import org.junit.jupiter.api.Test; 24 | import org.mockito.ArgumentCaptor; 25 | import org.mockito.Mockito; 26 | 27 | import org.springframework.beans.factory.annotation.Autowired; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Configuration; 30 | import org.springframework.integration.annotation.ServiceActivator; 31 | import org.springframework.integration.aws.support.AwsHeaders; 32 | import org.springframework.integration.aws.support.KplBackpressureException; 33 | import org.springframework.integration.config.EnableIntegration; 34 | import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice; 35 | import org.springframework.messaging.Message; 36 | import org.springframework.messaging.MessageChannel; 37 | import org.springframework.messaging.MessageHandler; 38 | import org.springframework.messaging.MessageHandlingException; 39 | import org.springframework.messaging.support.MessageBuilder; 40 | import org.springframework.retry.support.RetryTemplate; 41 | import org.springframework.test.annotation.DirtiesContext; 42 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 43 | 44 | import static org.assertj.core.api.Assertions.assertThat; 45 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 46 | import static org.mockito.ArgumentMatchers.any; 47 | import static org.mockito.BDDMockito.given; 48 | import static org.mockito.Mockito.clearInvocations; 49 | import static org.mockito.Mockito.mock; 50 | import static org.mockito.Mockito.verify; 51 | 52 | /** The class contains test cases for KplMessageHandler. 53 | * 54 | * @author Siddharth Jain 55 | * 56 | * @since 3.0.9 57 | */ 58 | @SpringJUnitConfig 59 | @DirtiesContext 60 | public class KplMessageHandlerTests { 61 | 62 | @Autowired 63 | protected Schema schema; 64 | 65 | @Autowired 66 | protected KinesisProducer kinesisProducer; 67 | 68 | @Autowired 69 | protected MessageChannel kinesisSendChannel; 70 | 71 | @Autowired 72 | protected KplMessageHandler kplMessageHandler; 73 | 74 | @Test 75 | @SuppressWarnings("unchecked") 76 | void kplMessageHandlerWithRawPayloadBackpressureDisabledSuccess() { 77 | given(this.kinesisProducer.addUserRecord(any(UserRecord.class))) 78 | .willReturn(mock()); 79 | final Message message = MessageBuilder 80 | .withPayload("someMessage") 81 | .setHeader(AwsHeaders.PARTITION_KEY, "somePartitionKey") 82 | .setHeader(AwsHeaders.SEQUENCE_NUMBER, "10") 83 | .setHeader("someHeaderKey", "someHeaderValue") 84 | .build(); 85 | 86 | ArgumentCaptor userRecordRequestArgumentCaptor = ArgumentCaptor 87 | .forClass(UserRecord.class); 88 | this.kplMessageHandler.setBackPressureThreshold(0); 89 | this.kinesisSendChannel.send(message); 90 | verify(this.kinesisProducer).addUserRecord(userRecordRequestArgumentCaptor.capture()); 91 | verify(this.kinesisProducer, Mockito.never()).getOutstandingRecordsCount(); 92 | UserRecord userRecord = userRecordRequestArgumentCaptor.getValue(); 93 | assertThat(userRecord.getStreamName()).isEqualTo("someStream"); 94 | assertThat(userRecord.getPartitionKey()).isEqualTo("somePartitionKey"); 95 | assertThat(userRecord.getExplicitHashKey()).isNull(); 96 | assertThat(userRecord.getSchema()).isSameAs(this.schema); 97 | } 98 | 99 | @Test 100 | @SuppressWarnings("unchecked") 101 | void kplMessageHandlerWithRawPayloadBackpressureEnabledCapacityAvailable() { 102 | given(this.kinesisProducer.addUserRecord(any(UserRecord.class))) 103 | .willReturn(mock()); 104 | this.kplMessageHandler.setBackPressureThreshold(2); 105 | given(this.kinesisProducer.getOutstandingRecordsCount()) 106 | .willReturn(1); 107 | final Message message = MessageBuilder 108 | .withPayload("someMessage") 109 | .setHeader(AwsHeaders.PARTITION_KEY, "somePartitionKey") 110 | .setHeader(AwsHeaders.SEQUENCE_NUMBER, "10") 111 | .setHeader("someHeaderKey", "someHeaderValue") 112 | .build(); 113 | 114 | ArgumentCaptor userRecordRequestArgumentCaptor = ArgumentCaptor 115 | .forClass(UserRecord.class); 116 | 117 | this.kinesisSendChannel.send(message); 118 | verify(this.kinesisProducer).addUserRecord(userRecordRequestArgumentCaptor.capture()); 119 | verify(this.kinesisProducer).getOutstandingRecordsCount(); 120 | UserRecord userRecord = userRecordRequestArgumentCaptor.getValue(); 121 | assertThat(userRecord.getStreamName()).isEqualTo("someStream"); 122 | assertThat(userRecord.getPartitionKey()).isEqualTo("somePartitionKey"); 123 | assertThat(userRecord.getExplicitHashKey()).isNull(); 124 | assertThat(userRecord.getSchema()).isSameAs(this.schema); 125 | } 126 | 127 | @Test 128 | @SuppressWarnings("unchecked") 129 | void kplMessageHandlerWithRawPayloadBackpressureEnabledCapacityInsufficient() { 130 | given(this.kinesisProducer.addUserRecord(any(UserRecord.class))) 131 | .willReturn(mock()); 132 | this.kplMessageHandler.setBackPressureThreshold(2); 133 | given(this.kinesisProducer.getOutstandingRecordsCount()) 134 | .willReturn(5); 135 | final Message message = MessageBuilder 136 | .withPayload("someMessage") 137 | .setHeader(AwsHeaders.PARTITION_KEY, "somePartitionKey") 138 | .setHeader(AwsHeaders.SEQUENCE_NUMBER, "10") 139 | .setHeader("someHeaderKey", "someHeaderValue") 140 | .build(); 141 | 142 | assertThatExceptionOfType(RuntimeException.class) 143 | .isThrownBy(() -> this.kinesisSendChannel.send(message)) 144 | .withCauseInstanceOf(MessageHandlingException.class) 145 | .withRootCauseExactlyInstanceOf(KplBackpressureException.class) 146 | .withStackTraceContaining("Cannot send record to Kinesis since buffer is at max capacity."); 147 | 148 | verify(this.kinesisProducer, Mockito.never()).addUserRecord(any(UserRecord.class)); 149 | verify(this.kinesisProducer).getOutstandingRecordsCount(); 150 | } 151 | 152 | @AfterEach 153 | public void tearDown() { 154 | clearInvocations(this.kinesisProducer); 155 | } 156 | 157 | @Configuration 158 | @EnableIntegration 159 | public static class ContextConfiguration { 160 | 161 | @Bean 162 | public KinesisProducer kinesisProducer() { 163 | return mock(); 164 | } 165 | 166 | @Bean 167 | public RequestHandlerRetryAdvice retryAdvice() { 168 | RequestHandlerRetryAdvice requestHandlerRetryAdvice = new RequestHandlerRetryAdvice(); 169 | requestHandlerRetryAdvice.setRetryTemplate(RetryTemplate.builder() 170 | .retryOn(KplBackpressureException.class) 171 | .exponentialBackoff(100, 2.0, 1000) 172 | .maxAttempts(3) 173 | .build()); 174 | return requestHandlerRetryAdvice; 175 | } 176 | 177 | @Bean 178 | @ServiceActivator(inputChannel = "kinesisSendChannel", adviceChain = "retryAdvice") 179 | public MessageHandler kplMessageHandler(KinesisProducer kinesisProducer) { 180 | KplMessageHandler kplMessageHandler = new KplMessageHandler(kinesisProducer); 181 | kplMessageHandler.setAsync(true); 182 | kplMessageHandler.setStream("someStream"); 183 | kplMessageHandler.setGlueSchema(schema()); 184 | return kplMessageHandler; 185 | } 186 | 187 | @Bean 188 | public Schema schema() { 189 | return new Schema("syntax=\"proto2\";", "PROTOBUF", "testschema"); 190 | } 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/kinesis/KplKclIntegrationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.kinesis; 18 | 19 | import java.net.URI; 20 | import java.util.Date; 21 | 22 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 23 | import com.amazonaws.auth.BasicAWSCredentials; 24 | import com.amazonaws.services.kinesis.producer.KinesisProducer; 25 | import com.amazonaws.services.kinesis.producer.KinesisProducerConfiguration; 26 | import org.junit.jupiter.api.AfterAll; 27 | import org.junit.jupiter.api.BeforeAll; 28 | import org.junit.jupiter.api.Disabled; 29 | import org.junit.jupiter.api.Test; 30 | import org.testcontainers.containers.localstack.LocalStackContainer; 31 | import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; 32 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 33 | import software.amazon.awssdk.services.kinesis.KinesisAsyncClient; 34 | import software.amazon.kinesis.common.InitialPositionInStream; 35 | import software.amazon.kinesis.common.InitialPositionInStreamExtended; 36 | 37 | import org.springframework.beans.factory.annotation.Autowired; 38 | import org.springframework.context.annotation.Bean; 39 | import org.springframework.context.annotation.Configuration; 40 | import org.springframework.integration.IntegrationMessageHeaderAccessor; 41 | import org.springframework.integration.annotation.ServiceActivator; 42 | import org.springframework.integration.aws.LocalstackContainerTest; 43 | import org.springframework.integration.aws.inbound.kinesis.KclMessageDrivenChannelAdapter; 44 | import org.springframework.integration.aws.inbound.kinesis.KinesisMessageHeaderErrorMessageStrategy; 45 | import org.springframework.integration.aws.outbound.KplMessageHandler; 46 | import org.springframework.integration.aws.support.AwsHeaders; 47 | import org.springframework.integration.channel.QueueChannel; 48 | import org.springframework.integration.config.EnableIntegration; 49 | import org.springframework.integration.support.MessageBuilder; 50 | import org.springframework.integration.support.json.EmbeddedJsonHeadersMessageMapper; 51 | import org.springframework.messaging.Message; 52 | import org.springframework.messaging.MessageChannel; 53 | import org.springframework.messaging.MessageHandler; 54 | import org.springframework.messaging.PollableChannel; 55 | import org.springframework.messaging.support.ChannelInterceptor; 56 | import org.springframework.messaging.support.ErrorMessage; 57 | import org.springframework.test.annotation.DirtiesContext; 58 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 59 | 60 | import static org.assertj.core.api.Assertions.assertThat; 61 | import static org.assertj.core.api.Assertions.entry; 62 | 63 | /** 64 | * @author Artem Bilan 65 | * 66 | * @since 1.1 67 | */ 68 | @Disabled("Depends on real call to http://169.254.169.254 through native library") 69 | @SpringJUnitConfig 70 | @DirtiesContext 71 | class KplKclIntegrationTests implements LocalstackContainerTest { 72 | 73 | private static final String TEST_STREAM = "TestStreamKplKcl"; 74 | 75 | private static KinesisAsyncClient AMAZON_KINESIS; 76 | 77 | private static DynamoDbAsyncClient DYNAMO_DB; 78 | 79 | private static CloudWatchAsyncClient CLOUD_WATCH; 80 | 81 | @Autowired 82 | private MessageChannel kinesisSendChannel; 83 | 84 | @Autowired 85 | private PollableChannel kinesisReceiveChannel; 86 | 87 | @Autowired 88 | private PollableChannel errorChannel; 89 | 90 | @BeforeAll 91 | static void setup() { 92 | AMAZON_KINESIS = LocalstackContainerTest.kinesisClient(); 93 | DYNAMO_DB = LocalstackContainerTest.dynamoDbClient(); 94 | CLOUD_WATCH = LocalstackContainerTest.cloudWatchClient(); 95 | 96 | AMAZON_KINESIS.createStream(request -> request.streamName(TEST_STREAM).shardCount(1)) 97 | .thenCompose(result -> 98 | AMAZON_KINESIS.waiter().waitUntilStreamExists(request -> request.streamName(TEST_STREAM))) 99 | .join(); 100 | } 101 | 102 | @AfterAll 103 | static void tearDown() { 104 | AMAZON_KINESIS.deleteStream(request -> request.streamName(TEST_STREAM)); 105 | } 106 | 107 | @Test 108 | void kinesisInboundOutbound() { 109 | this.kinesisSendChannel 110 | .send(MessageBuilder.withPayload("foo").setHeader(AwsHeaders.STREAM, TEST_STREAM).build()); 111 | 112 | Date now = new Date(); 113 | this.kinesisSendChannel.send(MessageBuilder.withPayload(now).setHeader(AwsHeaders.STREAM, TEST_STREAM) 114 | .setHeader("foo", "BAR").build()); 115 | 116 | Message receive = this.kinesisReceiveChannel.receive(30_000); 117 | assertThat(receive).isNotNull(); 118 | assertThat(receive.getPayload()).isEqualTo(now); 119 | assertThat(receive.getHeaders()).contains(entry("foo", "BAR")); 120 | assertThat(receive.getHeaders()).containsKey(IntegrationMessageHeaderAccessor.SOURCE_DATA); 121 | 122 | Message errorMessage = this.errorChannel.receive(30_000); 123 | assertThat(errorMessage).isNotNull(); 124 | assertThat(errorMessage.getHeaders().get(AwsHeaders.RAW_RECORD)).isNotNull(); 125 | assertThat(((Exception) errorMessage.getPayload()).getMessage()) 126 | .contains("Channel 'kinesisReceiveChannel' expected one of the following data types " 127 | + "[class java.util.Date], but received [class java.lang.String]"); 128 | 129 | this.kinesisSendChannel 130 | .send(MessageBuilder.withPayload(new Date()).setHeader(AwsHeaders.STREAM, TEST_STREAM).build()); 131 | 132 | receive = this.kinesisReceiveChannel.receive(30_000); 133 | assertThat(receive).isNotNull(); 134 | assertThat(receive.getHeaders().get(AwsHeaders.RECEIVED_SEQUENCE_NUMBER, String.class)).isNotEmpty(); 135 | 136 | receive = this.kinesisReceiveChannel.receive(10); 137 | assertThat(receive).isNull(); 138 | } 139 | 140 | @Configuration 141 | @EnableIntegration 142 | public static class TestConfiguration { 143 | 144 | @Bean 145 | public KinesisProducerConfiguration kinesisProducerConfiguration() { 146 | URI kinesisUri = 147 | LocalstackContainerTest.LOCAL_STACK_CONTAINER.getEndpointOverride(LocalStackContainer.Service.KINESIS); 148 | URI cloudWatchUri = 149 | LocalstackContainerTest.LOCAL_STACK_CONTAINER.getEndpointOverride(LocalStackContainer.Service.CLOUDWATCH); 150 | 151 | return new KinesisProducerConfiguration() 152 | .setCredentialsProvider(new AWSStaticCredentialsProvider( 153 | new BasicAWSCredentials(LOCAL_STACK_CONTAINER.getAccessKey(), 154 | LOCAL_STACK_CONTAINER.getSecretKey()))) 155 | .setRegion(LocalstackContainerTest.LOCAL_STACK_CONTAINER.getRegion()) 156 | .setKinesisEndpoint(kinesisUri.getHost()) 157 | .setKinesisPort(kinesisUri.getPort()) 158 | .setCloudwatchEndpoint(cloudWatchUri.getHost()) 159 | .setCloudwatchPort(cloudWatchUri.getPort()) 160 | .setVerifyCertificate(false); 161 | } 162 | 163 | @Bean 164 | @ServiceActivator(inputChannel = "kinesisSendChannel") 165 | public MessageHandler kplMessageHandler(KinesisProducerConfiguration kinesisProducerConfiguration) { 166 | KplMessageHandler kinesisMessageHandler = 167 | new KplMessageHandler(new KinesisProducer(kinesisProducerConfiguration)); 168 | kinesisMessageHandler.setPartitionKey("1"); 169 | kinesisMessageHandler.setEmbeddedHeadersMapper(new EmbeddedJsonHeadersMessageMapper("foo")); 170 | return kinesisMessageHandler; 171 | } 172 | 173 | @Bean 174 | public KclMessageDrivenChannelAdapter kclMessageDrivenChannelAdapter() { 175 | KclMessageDrivenChannelAdapter adapter = 176 | new KclMessageDrivenChannelAdapter(AMAZON_KINESIS, CLOUD_WATCH, DYNAMO_DB, TEST_STREAM); 177 | adapter.setOutputChannel(kinesisReceiveChannel()); 178 | adapter.setErrorChannel(errorChannel()); 179 | adapter.setErrorMessageStrategy(new KinesisMessageHeaderErrorMessageStrategy()); 180 | adapter.setEmbeddedHeadersMapper(new EmbeddedJsonHeadersMessageMapper("foo")); 181 | adapter.setStreamInitialSequence( 182 | InitialPositionInStreamExtended.newInitialPosition(InitialPositionInStream.TRIM_HORIZON)); 183 | adapter.setBindSourceRecord(true); 184 | return adapter; 185 | } 186 | 187 | @Bean 188 | public PollableChannel kinesisReceiveChannel() { 189 | QueueChannel queueChannel = new QueueChannel(); 190 | queueChannel.setDatatypes(Date.class); 191 | return queueChannel; 192 | } 193 | 194 | @Bean 195 | public PollableChannel errorChannel() { 196 | QueueChannel queueChannel = new QueueChannel(); 197 | queueChannel.addInterceptor(new ChannelInterceptor() { 198 | 199 | @Override 200 | public void postSend(Message message, MessageChannel channel, boolean sent) { 201 | if (message instanceof ErrorMessage) { 202 | throw (RuntimeException) ((ErrorMessage) message).getPayload(); 203 | } 204 | } 205 | 206 | }); 207 | return queueChannel; 208 | } 209 | 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/integration/aws/inbound/SnsInboundChannelAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.inbound; 18 | 19 | import java.util.Arrays; 20 | import java.util.Collections; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | import com.fasterxml.jackson.databind.JsonNode; 26 | import io.awspring.cloud.sns.handlers.NotificationStatus; 27 | import io.awspring.cloud.sns.handlers.NotificationStatusHandlerMethodArgumentResolver; 28 | import software.amazon.awssdk.services.sns.SnsClient; 29 | 30 | import org.springframework.expression.EvaluationContext; 31 | import org.springframework.expression.Expression; 32 | import org.springframework.http.HttpHeaders; 33 | import org.springframework.http.HttpMethod; 34 | import org.springframework.http.HttpStatus; 35 | import org.springframework.http.MediaType; 36 | import org.springframework.http.converter.HttpMessageConverter; 37 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 38 | import org.springframework.integration.aws.support.AwsHeaders; 39 | import org.springframework.integration.expression.ValueExpression; 40 | import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway; 41 | import org.springframework.integration.http.inbound.RequestMapping; 42 | import org.springframework.integration.mapping.HeaderMapper; 43 | import org.springframework.integration.support.AbstractIntegrationMessageBuilder; 44 | import org.springframework.messaging.Message; 45 | import org.springframework.util.Assert; 46 | import org.springframework.web.multipart.MultipartResolver; 47 | 48 | /** 49 | * The {@link HttpRequestHandlingMessagingGateway} extension for the Amazon WS SNS HTTP(S) 50 | * endpoints. Accepts all {@code x-amz-sns-message-type}s, converts the received Topic 51 | * JSON message to the {@link Map} using {@link MappingJackson2HttpMessageConverter} and 52 | * send it to the provided {@link #getRequestChannel()} as {@link Message} 53 | * {@code payload}. 54 | *

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

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

66 | * By default, this endpoint just does {@link NotificationStatus#confirmSubscription()} for 67 | * the {@code SubscriptionConfirmation} message type. And does nothing for the 68 | * {@code UnsubscribeConfirmation}. 69 | *

70 | * For the convenience on the underlying message flow routing a 71 | * {@link AwsHeaders#SNS_MESSAGE_TYPE} header is present. 72 | * 73 | * @author Artem Bilan 74 | * @author Kamil Przerwa 75 | */ 76 | public class SnsInboundChannelAdapter extends HttpRequestHandlingMessagingGateway { 77 | 78 | private final NotificationStatusResolver notificationStatusResolver; 79 | 80 | private final MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = 81 | new MappingJackson2HttpMessageConverter(); 82 | 83 | private final String[] path; 84 | 85 | private volatile boolean handleNotificationStatus; 86 | 87 | private volatile Expression payloadExpression; 88 | 89 | private EvaluationContext evaluationContext; 90 | 91 | public SnsInboundChannelAdapter(SnsClient amazonSns, String... path) { 92 | super(false); 93 | Assert.notNull(amazonSns, "'amazonSns' must not be null."); 94 | Assert.notNull(path, "'path' must not be null."); 95 | Assert.noNullElements(path, "'path' must not contain null elements."); 96 | this.path = path; 97 | this.notificationStatusResolver = new NotificationStatusResolver(amazonSns); 98 | this.jackson2HttpMessageConverter 99 | .setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN)); 100 | } 101 | 102 | public void setHandleNotificationStatus(boolean handleNotificationStatus) { 103 | this.handleNotificationStatus = handleNotificationStatus; 104 | } 105 | 106 | @Override 107 | protected void onInit() { 108 | super.onInit(); 109 | RequestMapping requestMapping = new RequestMapping(); 110 | requestMapping.setMethods(HttpMethod.POST); 111 | requestMapping.setHeaders("x-amz-sns-message-type"); 112 | requestMapping.setPathPatterns(this.path); 113 | super.setStatusCodeExpression(new ValueExpression<>(HttpStatus.NO_CONTENT)); 114 | super.setMessageConverters(Collections.singletonList(this.jackson2HttpMessageConverter)); 115 | super.setRequestPayloadTypeClass(HashMap.class); 116 | super.setRequestMapping(requestMapping); 117 | if (this.payloadExpression != null) { 118 | this.evaluationContext = createEvaluationContext(); 119 | } 120 | } 121 | 122 | @Override 123 | public String getComponentType() { 124 | return "aws:sns-inbound-channel-adapter"; 125 | } 126 | 127 | @Override 128 | @SuppressWarnings("unchecked") 129 | protected void send(Object object) { 130 | Message message = (Message) object; 131 | Map payload = (HashMap) message.getPayload(); 132 | AbstractIntegrationMessageBuilder messageToSendBuilder; 133 | if (this.payloadExpression != null) { 134 | messageToSendBuilder = getMessageBuilderFactory() 135 | .withPayload(this.payloadExpression.getValue(this.evaluationContext, message)) 136 | .copyHeaders(message.getHeaders()); 137 | } 138 | else { 139 | messageToSendBuilder = getMessageBuilderFactory().fromMessage(message); 140 | } 141 | 142 | String type = payload.get("Type"); 143 | if ("SubscriptionConfirmation".equals(type) || "UnsubscribeConfirmation".equals(type)) { 144 | JsonNode content = this.jackson2HttpMessageConverter.getObjectMapper().valueToTree(payload); 145 | NotificationStatus notificationStatus = this.notificationStatusResolver.resolveNotificationStatus(content); 146 | if (this.handleNotificationStatus) { 147 | messageToSendBuilder.setHeader(AwsHeaders.NOTIFICATION_STATUS, notificationStatus); 148 | } 149 | else { 150 | if ("SubscriptionConfirmation".equals(type)) { 151 | notificationStatus.confirmSubscription(); 152 | } 153 | return; 154 | } 155 | } 156 | messageToSendBuilder.setHeader(AwsHeaders.SNS_MESSAGE_TYPE, type).setHeader(AwsHeaders.MESSAGE_ID, 157 | payload.get("MessageId")); 158 | 159 | super.send(messageToSendBuilder.build()); 160 | } 161 | 162 | @Override 163 | public void setPayloadExpression(Expression payloadExpression) { 164 | this.payloadExpression = payloadExpression; 165 | } 166 | 167 | @Override 168 | public void setHeaderExpressions(Map headerExpressions) { 169 | throw new UnsupportedOperationException(); 170 | } 171 | 172 | @Override 173 | public void setMessageConverters(List> messageConverters) { 174 | throw new UnsupportedOperationException(); 175 | } 176 | 177 | @Override 178 | public void setMergeWithDefaultConverters(boolean mergeWithDefaultConverters) { 179 | throw new UnsupportedOperationException(); 180 | } 181 | 182 | @Override 183 | public void setHeaderMapper(HeaderMapper headerMapper) { 184 | throw new UnsupportedOperationException(); 185 | } 186 | 187 | @Override 188 | public void setRequestMapping(RequestMapping requestMapping) { 189 | throw new UnsupportedOperationException(); 190 | } 191 | 192 | @Override 193 | public void setRequestPayloadTypeClass(Class requestPayloadType) { 194 | throw new UnsupportedOperationException(); 195 | } 196 | 197 | @Override 198 | public void setExtractReplyPayload(boolean extractReplyPayload) { 199 | throw new UnsupportedOperationException(); 200 | } 201 | 202 | @Override 203 | public void setMultipartResolver(MultipartResolver multipartResolver) { 204 | throw new UnsupportedOperationException(); 205 | } 206 | 207 | @Override 208 | public void setStatusCodeExpression(Expression statusCodeExpression) { 209 | throw new UnsupportedOperationException(); 210 | } 211 | 212 | private static class NotificationStatusResolver extends NotificationStatusHandlerMethodArgumentResolver { 213 | 214 | NotificationStatusResolver(SnsClient amazonSns) { 215 | super(amazonSns); 216 | } 217 | 218 | NotificationStatus resolveNotificationStatus(JsonNode content) { 219 | return (NotificationStatus) doResolveArgumentFromNotificationMessage(content, null, null); 220 | } 221 | 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/outbound/KinesisProducingMessageHandlerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.outbound; 18 | 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | import org.junit.jupiter.api.Test; 22 | import software.amazon.awssdk.core.SdkBytes; 23 | import software.amazon.awssdk.services.kinesis.KinesisAsyncClient; 24 | import software.amazon.awssdk.services.kinesis.model.PutRecordRequest; 25 | import software.amazon.awssdk.services.kinesis.model.PutRecordResponse; 26 | import software.amazon.awssdk.services.kinesis.model.PutRecordsRequest; 27 | import software.amazon.awssdk.services.kinesis.model.PutRecordsRequestEntry; 28 | import software.amazon.awssdk.services.kinesis.model.PutRecordsResponse; 29 | 30 | import org.springframework.beans.factory.annotation.Autowired; 31 | import org.springframework.context.annotation.Bean; 32 | import org.springframework.context.annotation.Configuration; 33 | import org.springframework.core.serializer.support.SerializingConverter; 34 | import org.springframework.integration.annotation.ServiceActivator; 35 | import org.springframework.integration.aws.support.AwsHeaders; 36 | import org.springframework.integration.aws.support.AwsRequestFailureException; 37 | import org.springframework.integration.channel.QueueChannel; 38 | import org.springframework.integration.config.EnableIntegration; 39 | import org.springframework.messaging.Message; 40 | import org.springframework.messaging.MessageChannel; 41 | import org.springframework.messaging.MessageHandler; 42 | import org.springframework.messaging.MessageHandlingException; 43 | import org.springframework.messaging.MessageHeaders; 44 | import org.springframework.messaging.PollableChannel; 45 | import org.springframework.messaging.converter.MessageConverter; 46 | import org.springframework.messaging.support.MessageBuilder; 47 | import org.springframework.test.annotation.DirtiesContext; 48 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 49 | 50 | import static org.assertj.core.api.Assertions.assertThat; 51 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 52 | import static org.mockito.ArgumentMatchers.any; 53 | import static org.mockito.BDDMockito.given; 54 | import static org.mockito.Mockito.mock; 55 | 56 | /** 57 | * @author Jacob Severson 58 | * @author Artem Bilan 59 | * 60 | * @since 1.1 61 | */ 62 | @SpringJUnitConfig 63 | @DirtiesContext 64 | class KinesisProducingMessageHandlerTests { 65 | 66 | @Autowired 67 | protected MessageChannel kinesisSendChannel; 68 | 69 | @Autowired 70 | protected KinesisMessageHandler kinesisMessageHandler; 71 | 72 | @Autowired 73 | protected PollableChannel errorChannel; 74 | 75 | @Autowired 76 | protected PollableChannel successChannel; 77 | 78 | @Test 79 | void kinesisMessageHandler() { 80 | final Message message = 81 | MessageBuilder.withPayload("message") 82 | .setErrorChannel(this.errorChannel) 83 | .build(); 84 | 85 | assertThatExceptionOfType(MessageHandlingException.class) 86 | .isThrownBy(() -> this.kinesisSendChannel.send(message)) 87 | .withCauseInstanceOf(IllegalStateException.class) 88 | .withStackTraceContaining("'stream' must not be null for sending a Kinesis record"); 89 | 90 | this.kinesisMessageHandler.setStream("foo"); 91 | 92 | assertThatExceptionOfType(MessageHandlingException.class) 93 | .isThrownBy(() -> this.kinesisSendChannel.send(message)) 94 | .withCauseInstanceOf(IllegalStateException.class) 95 | .withStackTraceContaining("'partitionKey' must not be null for sending a Kinesis record"); 96 | 97 | Message message2 = 98 | MessageBuilder.fromMessage(message) 99 | .setHeader(AwsHeaders.PARTITION_KEY, "fooKey") 100 | .setHeader(AwsHeaders.SEQUENCE_NUMBER, "10") 101 | .build(); 102 | 103 | this.kinesisSendChannel.send(message2); 104 | 105 | Message success = this.successChannel.receive(10000); 106 | assertThat(success.getHeaders().get(AwsHeaders.PARTITION_KEY)).isEqualTo("fooKey"); 107 | assertThat(success.getHeaders().get(AwsHeaders.SEQUENCE_NUMBER)).isEqualTo("10"); 108 | assertThat(success.getPayload()).isEqualTo("message"); 109 | 110 | message2 = 111 | MessageBuilder.fromMessage(message) 112 | .setHeader(AwsHeaders.PARTITION_KEY, "fooKey") 113 | .setHeader(AwsHeaders.SEQUENCE_NUMBER, "10") 114 | .build(); 115 | 116 | this.kinesisSendChannel.send(message2); 117 | 118 | Message failed = this.errorChannel.receive(10000); 119 | AwsRequestFailureException putRecordFailure = (AwsRequestFailureException) failed.getPayload(); 120 | assertThat(putRecordFailure.getCause().getMessage()).isEqualTo("putRecordRequestEx"); 121 | assertThat(((PutRecordRequest) putRecordFailure.getRequest()).streamName()).isEqualTo("foo"); 122 | assertThat(((PutRecordRequest) putRecordFailure.getRequest()).partitionKey()).isEqualTo("fooKey"); 123 | assertThat(((PutRecordRequest) putRecordFailure.getRequest()).sequenceNumberForOrdering()).isEqualTo("10"); 124 | assertThat(((PutRecordRequest) putRecordFailure.getRequest()).explicitHashKey()).isNull(); 125 | assertThat(((PutRecordRequest) putRecordFailure.getRequest()).data()) 126 | .isEqualTo(SdkBytes.fromUtf8String("message")); 127 | 128 | PutRecordsRequestEntry testRecordEntry = 129 | PutRecordsRequestEntry.builder() 130 | .data(SdkBytes.fromUtf8String("test")) 131 | .partitionKey("testKey") 132 | .build(); 133 | 134 | message2 = 135 | MessageBuilder.withPayload( 136 | PutRecordsRequest.builder() 137 | .streamName("myStream") 138 | .records(testRecordEntry) 139 | .build()) 140 | .setErrorChannel(this.errorChannel) 141 | .build(); 142 | 143 | this.kinesisSendChannel.send(message2); 144 | 145 | success = this.successChannel.receive(10000); 146 | assertThat(((PutRecordsRequest) success.getPayload()).records()) 147 | .containsExactlyInAnyOrder(testRecordEntry); 148 | 149 | this.kinesisSendChannel.send(message2); 150 | 151 | failed = this.errorChannel.receive(10000); 152 | AwsRequestFailureException putRecordsFailure = (AwsRequestFailureException) failed.getPayload(); 153 | assertThat(putRecordsFailure.getCause().getMessage()).isEqualTo("putRecordsRequestEx"); 154 | assertThat(((PutRecordsRequest) putRecordsFailure.getRequest()).streamName()).isEqualTo("myStream"); 155 | assertThat(((PutRecordsRequest) putRecordsFailure.getRequest()).records()) 156 | .containsExactlyInAnyOrder(testRecordEntry); 157 | } 158 | 159 | @Configuration 160 | @EnableIntegration 161 | public static class ContextConfiguration { 162 | 163 | @Bean 164 | public KinesisAsyncClient amazonKinesis() { 165 | KinesisAsyncClient mock = mock(KinesisAsyncClient.class); 166 | 167 | given(mock.putRecord(any(PutRecordRequest.class))) 168 | .willAnswer(invocation -> { 169 | PutRecordRequest request = invocation.getArgument(0); 170 | PutRecordResponse.Builder result = 171 | PutRecordResponse.builder() 172 | .sequenceNumber(request.sequenceNumberForOrdering()) 173 | .shardId("shardId-1"); 174 | return CompletableFuture.completedFuture(result.build()); 175 | }) 176 | .willAnswer(invocation -> 177 | CompletableFuture.failedFuture(new RuntimeException("putRecordRequestEx"))); 178 | 179 | given(mock.putRecords(any(PutRecordsRequest.class))) 180 | .willAnswer(invocation -> CompletableFuture.completedFuture(PutRecordsResponse.builder().build())) 181 | .willAnswer(invocation -> 182 | CompletableFuture.failedFuture(new RuntimeException("putRecordsRequestEx"))); 183 | 184 | return mock; 185 | } 186 | 187 | @Bean 188 | public PollableChannel errorChannel() { 189 | return new QueueChannel(); 190 | } 191 | 192 | @Bean 193 | public PollableChannel successChannel() { 194 | return new QueueChannel(); 195 | } 196 | 197 | @Bean 198 | @ServiceActivator(inputChannel = "kinesisSendChannel") 199 | public MessageHandler kinesisMessageHandler() { 200 | KinesisMessageHandler kinesisMessageHandler = new KinesisMessageHandler(amazonKinesis()); 201 | kinesisMessageHandler.setAsync(true); 202 | kinesisMessageHandler.setOutputChannel(successChannel()); 203 | kinesisMessageHandler.setMessageConverter(new MessageConverter() { 204 | 205 | private SerializingConverter serializingConverter = new SerializingConverter(); 206 | 207 | @Override 208 | public Object fromMessage(Message message, Class targetClass) { 209 | Object source = message.getPayload(); 210 | if (source instanceof String) { 211 | return ((String) source).getBytes(); 212 | } 213 | else { 214 | return this.serializingConverter.convert(source); 215 | } 216 | } 217 | 218 | @Override 219 | public Message toMessage(Object payload, MessageHeaders headers) { 220 | return null; 221 | } 222 | 223 | }); 224 | return kinesisMessageHandler; 225 | } 226 | 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /src/test/java/org/springframework/integration/aws/leader/DynamoDbLockRegistryLeaderInitiatorTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.integration.aws.leader; 18 | 19 | import java.time.Duration; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.concurrent.CountDownLatch; 23 | import java.util.concurrent.Executors; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | import org.junit.jupiter.api.AfterAll; 27 | import org.junit.jupiter.api.BeforeAll; 28 | import org.junit.jupiter.api.Test; 29 | import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy; 30 | import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; 31 | 32 | import org.springframework.integration.aws.LocalstackContainerTest; 33 | import org.springframework.integration.aws.lock.DynamoDbLockRegistry; 34 | import org.springframework.integration.aws.lock.DynamoDbLockRepository; 35 | import org.springframework.integration.leader.Context; 36 | import org.springframework.integration.leader.DefaultCandidate; 37 | import org.springframework.integration.leader.event.LeaderEventPublisher; 38 | import org.springframework.integration.support.leader.LockRegistryLeaderInitiator; 39 | import org.springframework.scheduling.concurrent.CustomizableThreadFactory; 40 | 41 | import static org.assertj.core.api.Assertions.assertThat; 42 | 43 | /** 44 | * @author Artem Bilan 45 | * 46 | * @since 2.0 47 | */ 48 | class DynamoDbLockRegistryLeaderInitiatorTests implements LocalstackContainerTest { 49 | 50 | private static DynamoDbAsyncClient DYNAMO_DB; 51 | 52 | @BeforeAll 53 | static void init() { 54 | DYNAMO_DB = LocalstackContainerTest.dynamoDbClient(); 55 | try { 56 | DYNAMO_DB.deleteTable(request -> request.tableName(DynamoDbLockRepository.DEFAULT_TABLE_NAME)) 57 | .thenCompose(result -> 58 | DYNAMO_DB.waiter() 59 | .waitUntilTableNotExists(request -> request 60 | .tableName(DynamoDbLockRepository.DEFAULT_TABLE_NAME), 61 | waiter -> waiter 62 | .maxAttempts(25) 63 | .backoffStrategy( 64 | FixedDelayBackoffStrategy.create(Duration.ofSeconds(1))))) 65 | .get(); 66 | } 67 | catch (Exception e) { 68 | // Ignore 69 | } 70 | } 71 | 72 | @AfterAll 73 | static void destroy() { 74 | DYNAMO_DB.deleteTable(request -> request.tableName(DynamoDbLockRepository.DEFAULT_TABLE_NAME)).join(); 75 | } 76 | 77 | @Test 78 | void distributedLeaderElection() throws Exception { 79 | CountDownLatch granted = new CountDownLatch(1); 80 | CountingPublisher countingPublisher = new CountingPublisher(granted); 81 | List repositories = new ArrayList<>(); 82 | List initiators = new ArrayList<>(); 83 | for (int i = 0; i < 2; i++) { 84 | DynamoDbLockRepository dynamoDbLockRepository = new DynamoDbLockRepository(DYNAMO_DB); 85 | dynamoDbLockRepository.setLeaseDuration(Duration.ofSeconds(2)); 86 | dynamoDbLockRepository.afterPropertiesSet(); 87 | repositories.add(dynamoDbLockRepository); 88 | DynamoDbLockRegistry lockRepository = new DynamoDbLockRegistry(dynamoDbLockRepository); 89 | 90 | LockRegistryLeaderInitiator initiator = new LockRegistryLeaderInitiator(lockRepository, 91 | new DefaultCandidate("foo#" + i, "bar")); 92 | initiator.setBusyWaitMillis(100); 93 | initiator.setHeartBeatMillis(1000); 94 | initiator.setExecutorService( 95 | Executors.newSingleThreadExecutor(new CustomizableThreadFactory("lock-leadership-" + i + "-"))); 96 | initiator.setLeaderEventPublisher(countingPublisher); 97 | initiators.add(initiator); 98 | } 99 | 100 | for (LockRegistryLeaderInitiator initiator : initiators) { 101 | initiator.start(); 102 | } 103 | 104 | assertThat(granted.await(30, TimeUnit.SECONDS)).isTrue(); 105 | 106 | LockRegistryLeaderInitiator initiator1 = countingPublisher.initiator; 107 | 108 | LockRegistryLeaderInitiator initiator2 = null; 109 | 110 | for (LockRegistryLeaderInitiator initiator : initiators) { 111 | if (initiator != initiator1) { 112 | initiator2 = initiator; 113 | break; 114 | } 115 | } 116 | 117 | assertThat(initiator2).isNotNull(); 118 | 119 | assertThat(initiator1.getContext().isLeader()).isTrue(); 120 | assertThat(initiator2.getContext().isLeader()).isFalse(); 121 | 122 | final CountDownLatch granted1 = new CountDownLatch(1); 123 | final CountDownLatch granted2 = new CountDownLatch(1); 124 | CountDownLatch revoked1 = new CountDownLatch(1); 125 | CountDownLatch revoked2 = new CountDownLatch(1); 126 | CountDownLatch acquireLockFailed1 = new CountDownLatch(1); 127 | CountDownLatch acquireLockFailed2 = new CountDownLatch(1); 128 | 129 | initiator1.setLeaderEventPublisher(new CountingPublisher(granted1, revoked1, acquireLockFailed1)); 130 | 131 | initiator2.setLeaderEventPublisher(new CountingPublisher(granted2, revoked2, acquireLockFailed2)); 132 | 133 | // It's hard to see round-robin election, so let's make the yielding initiator to 134 | // sleep long before restarting 135 | initiator1.setBusyWaitMillis(10000); 136 | 137 | initiator1.getContext().yield(); 138 | 139 | assertThat(revoked1.await(30, TimeUnit.SECONDS)).isTrue(); 140 | assertThat(granted2.await(30, TimeUnit.SECONDS)).isTrue(); 141 | 142 | assertThat(initiator2.getContext().isLeader()).isTrue(); 143 | assertThat(initiator1.getContext().isLeader()).isFalse(); 144 | 145 | initiator1.setBusyWaitMillis(100); 146 | // Interrupt the current selector and let it start with a new busy-wait period 147 | initiator1.stop(); 148 | initiator1.start(); 149 | 150 | initiator2.setBusyWaitMillis(10000); 151 | 152 | initiator2.getContext().yield(); 153 | 154 | assertThat(revoked2.await(30, TimeUnit.SECONDS)).isTrue(); 155 | assertThat(granted1.await(30, TimeUnit.SECONDS)).isTrue(); 156 | 157 | assertThat(initiator1.getContext().isLeader()).isTrue(); 158 | assertThat(initiator2.getContext().isLeader()).isFalse(); 159 | 160 | initiator2.stop(); 161 | 162 | CountDownLatch revoked11 = new CountDownLatch(1); 163 | initiator1.setLeaderEventPublisher( 164 | new CountingPublisher(new CountDownLatch(1), revoked11, new CountDownLatch(1))); 165 | 166 | initiator1.getContext().yield(); 167 | 168 | assertThat(revoked11.await(30, TimeUnit.SECONDS)).isTrue(); 169 | assertThat(initiator1.getContext().isLeader()).isFalse(); 170 | 171 | initiator1.stop(); 172 | 173 | for (DynamoDbLockRepository dynamoDbLockRepository : repositories) { 174 | dynamoDbLockRepository.close(); 175 | } 176 | } 177 | 178 | @Test 179 | void lostConnection() throws Exception { 180 | CountDownLatch granted = new CountDownLatch(1); 181 | CountingPublisher countingPublisher = new CountingPublisher(granted); 182 | 183 | DynamoDbLockRepository dynamoDbLockRepository = new DynamoDbLockRepository(DYNAMO_DB); 184 | dynamoDbLockRepository.afterPropertiesSet(); 185 | DynamoDbLockRegistry lockRepository = new DynamoDbLockRegistry(dynamoDbLockRepository); 186 | 187 | LockRegistryLeaderInitiator initiator = new LockRegistryLeaderInitiator(lockRepository); 188 | initiator.setLeaderEventPublisher(countingPublisher); 189 | 190 | initiator.start(); 191 | 192 | assertThat(granted.await(20, TimeUnit.SECONDS)).isTrue(); 193 | 194 | destroy(); 195 | 196 | assertThat(countingPublisher.revoked.await(20, TimeUnit.SECONDS)).isTrue(); 197 | 198 | granted = new CountDownLatch(1); 199 | countingPublisher = new CountingPublisher(granted); 200 | initiator.setLeaderEventPublisher(countingPublisher); 201 | 202 | init(); 203 | 204 | dynamoDbLockRepository.afterPropertiesSet(); 205 | 206 | assertThat(granted.await(20, TimeUnit.SECONDS)).isTrue(); 207 | 208 | initiator.stop(); 209 | 210 | dynamoDbLockRepository.close(); 211 | } 212 | 213 | private static class CountingPublisher implements LeaderEventPublisher { 214 | 215 | private final CountDownLatch granted; 216 | 217 | private final CountDownLatch revoked; 218 | 219 | private final CountDownLatch acquireLockFailed; 220 | 221 | private volatile LockRegistryLeaderInitiator initiator; 222 | 223 | CountingPublisher(CountDownLatch granted, CountDownLatch revoked, CountDownLatch acquireLockFailed) { 224 | this.granted = granted; 225 | this.revoked = revoked; 226 | this.acquireLockFailed = acquireLockFailed; 227 | } 228 | 229 | CountingPublisher(CountDownLatch granted) { 230 | this(granted, new CountDownLatch(1), new CountDownLatch(1)); 231 | } 232 | 233 | @Override 234 | public void publishOnRevoked(Object source, Context context, String role) { 235 | this.revoked.countDown(); 236 | } 237 | 238 | @Override 239 | public void publishOnFailedToAcquire(Object source, Context context, String role) { 240 | this.acquireLockFailed.countDown(); 241 | } 242 | 243 | @Override 244 | public void publishOnGranted(Object source, Context context, String role) { 245 | this.initiator = (LockRegistryLeaderInitiator) source; 246 | this.granted.countDown(); 247 | } 248 | 249 | } 250 | 251 | } 252 | --------------------------------------------------------------------------------