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 extends AwsResponse> 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 |
--------------------------------------------------------------------------------