├── .github └── dependabot.yml ├── .gitignore ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── amazon │ └── sqs │ └── javamessaging │ ├── AmazonSQSMessagingClientWrapper.java │ ├── PrefetchManager.java │ ├── ProviderConfiguration.java │ ├── SQSConnection.java │ ├── SQSConnectionFactory.java │ ├── SQSConnectionMetaData.java │ ├── SQSMessageConsumer.java │ ├── SQSMessageConsumerPrefetch.java │ ├── SQSMessageProducer.java │ ├── SQSMessagingClientConstants.java │ ├── SQSQueueDestination.java │ ├── SQSSession.java │ ├── SQSSessionCallbackScheduler.java │ ├── acknowledge │ ├── AcknowledgeMode.java │ ├── Acknowledger.java │ ├── AutoAcknowledger.java │ ├── BulkSQSOperation.java │ ├── NegativeAcknowledger.java │ ├── RangedAcknowledger.java │ ├── SQSMessageIdentifier.java │ └── UnorderedAcknowledger.java │ ├── message │ ├── SQSBytesMessage.java │ ├── SQSMessage.java │ ├── SQSObjectMessage.java │ └── SQSTextMessage.java │ └── util │ ├── ExponentialBackoffStrategy.java │ ├── SQSMessagingClientThreadFactory.java │ └── SQSMessagingClientUtil.java └── test └── java └── com └── amazon └── sqs └── javamessaging ├── AcknowledgerCommon.java ├── AmazonSQSMessagingClientWrapperTest.java ├── AutoAcknowledgerTest.java ├── BulkSQSOperationTest.java ├── ExponentialBackoffStrategyTest.java ├── MessageListenerConcurrentOperationTest.java ├── ModifyWaitTimeSecondsTest.java ├── NegativeAcknowledgerTest.java ├── RangedAcknowledgerTest.java ├── SQSConnectionFactoryTest.java ├── SQSConnectionTest.java ├── SQSMessageConsumerPrefetchFifoTest.java ├── SQSMessageConsumerPrefetchTest.java ├── SQSMessageConsumerTest.java ├── SQSMessageProducerFifoTest.java ├── SQSMessageProducerTest.java ├── SQSMessagingClientUtilTest.java ├── SQSQueueDestinationTest.java ├── SQSSessionCallbackSchedulerTest.java ├── SQSSessionTest.java ├── UnorderedAcknowledgerTest.java └── message ├── SQSBytesMessageTest.java ├── SQSMessageTest.java ├── SQSObjectMessageTest.java └── SQSTextMessageTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | 4 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 5 | 6 | 1. Definitions. 7 | 8 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 9 | 10 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 11 | 12 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 13 | 14 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 15 | 16 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 17 | 18 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 19 | 20 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 21 | 22 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 23 | 24 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 25 | 26 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 27 | 28 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 29 | 30 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 31 | 32 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 33 | 34 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 35 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 36 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 37 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 38 | 39 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 40 | 41 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 42 | 43 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 44 | 45 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 46 | 47 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 48 | 49 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 50 | 51 | END OF TERMS AND CONDITIONS 52 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Amazon SQS Java Messaging Library 2 | Copyright 2010-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | This product includes software developed by 5 | Amazon Technologies, Inc (http://www.amazon.com/). 6 | 7 | ********************** 8 | THIRD PARTY COMPONENTS 9 | ********************** 10 | This software includes third party software subject to the following copyrights: 11 | - Apache ActiveMQ (see: org.apache.activemq.util.TypeConversionSupport, org.apache.activemq.command.ActiveMQStreamMessage). Copyright 2005-2013 Apache Software Foundation 12 | 13 | The licenses for these third party components are included in LICENSE.txt 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Amazon SQS Java Messaging Library 2 | ======================================== 3 | The **Amazon SQS Java Messaging Library** holds the Java Message Service compatible classes, that are used 4 | for communicating with Amazon Simple Queue Service. This project builds on top of the AWS SDK for Java to use Amazon SQS as the JMS (as defined in Jakarta [3.1 specification](https://jakarta.ee/specifications/messaging/3.1/)) provider for the messaging applications without running any additional software. 5 | 6 | * You can download release builds through the [releases section of this](https://github.com/awslabs/amazon-sqs-java-messaging-lib) project. 7 | * For more information on using the amazon-sqs-java-messaging-lib, see our getting started guide to SQS [here](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/jmsclient.html). 8 | 9 | ## Getting Started 10 | 11 | * **Sign up for AWS** — Before you begin, you need an AWS account. For more information about creating an AWS account and retrieving your AWS credentials, see [AWS Account and Credentials](http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-setup.html) in the AWS SDK for Java Developer Guide. 12 | * **Minimum requirements** — To use the sample application, you'll need Java 8 (or later) and [Maven 3](http://maven.apache.org/). For more information about the requirements, see the [Getting Started](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/jmsclient.html) section of the Amazon SQS Developer Guide. 13 | * **Download** — Download the [latest release](https://github.com/awslabs/amazon-sqs-java-messaging-lib/releases) or pick it up from Maven: 14 | ```xml 15 | 16 | com.amazonaws 17 | amazon-sqs-java-messaging-lib 18 | 2.1.4 19 | jar 20 | 21 | ``` 22 | * **Further information** - Read the [API documentation](http://aws.amazon.com/documentation/sqs/). 23 | 24 | ## Feedback 25 | * Give us feedback [here](https://github.com/awslabs/amazon-sqs-java-messaging-lib/issues). 26 | * If you'd like to contribute a new feature or bug fix, we'd love to see GitHub pull requests from you. 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.amazonaws 5 | amazon-sqs-java-messaging-lib 6 | 2.1.4 7 | Amazon SQS Java Messaging Library 8 | The Amazon SQS Java Messaging Library holds the Java Message Service compatible classes, that are used 9 | for communicating with Amazon Simple Queue Service. 10 | 11 | https://github.com/awslabs/amazon-sqs-java-messaging-lib 12 | 13 | https://github.com/awslabs/amazon-sqs-java-messaging-lib.git 14 | 15 | 16 | 17 | Apache License, Version 2.0 18 | https://aws.amazon.com/apache2.0 19 | repo 20 | 21 | 22 | 23 | 24 | amazonwebservices 25 | Amazon Web Services 26 | https://aws.amazon.com 27 | 28 | developer 29 | 30 | 31 | 32 | 33 | 2.29.24 34 | UTF-8 35 | UTF-8 36 | 37 | 38 | 39 | 40 | software.amazon.awssdk 41 | sqs 42 | ${aws-java-sdk.version} 43 | 44 | 45 | software.amazon.awssdk 46 | auth 47 | ${aws-java-sdk.version} 48 | 49 | 50 | software.amazon.awssdk 51 | utils 52 | ${aws-java-sdk.version} 53 | 54 | 55 | 56 | org.slf4j 57 | slf4j-api 58 | 2.0.13 59 | 60 | 61 | jakarta.jms 62 | jakarta.jms-api 63 | 3.1.0 64 | 65 | 66 | org.junit.jupiter 67 | junit-jupiter 68 | 5.10.3 69 | test 70 | 71 | 72 | org.assertj 73 | assertj-core 74 | 3.26.3 75 | test 76 | 77 | 78 | org.hamcrest 79 | hamcrest 80 | 2.2 81 | test 82 | 83 | 84 | org.mockito 85 | mockito-core 86 | 5.11.0 87 | test 88 | 89 | 90 | org.mockito 91 | mockito-junit-jupiter 92 | 5.11.0 93 | test 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-compiler-plugin 103 | 3.13.0 104 | 105 | 17 106 | 17 107 | UTF-8 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-failsafe-plugin 113 | 3.2.5 114 | 115 | 116 | 117 | integration-test 118 | verify 119 | 120 | 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-surefire-plugin 126 | 3.2.5 127 | 128 | 129 | 130 | 131 | 132 | org.apache.maven.plugins 133 | maven-source-plugin 134 | 3.3.0 135 | 136 | 137 | attach-sources 138 | 139 | jar-no-fork 140 | 141 | 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-javadoc-plugin 147 | 3.6.3 148 | 149 | 150 | attach-javadocs 151 | 152 | jar 153 | 154 | 155 | none 156 | 157 | 158 | 159 | 160 | 161 | org.sonatype.plugins 162 | nexus-staging-maven-plugin 163 | 1.6.13 164 | true 165 | 166 | ossrh 167 | https://aws.oss.sonatype.org 168 | true 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | publishing 177 | 178 | 179 | 180 | ossrh 181 | https://oss.sonatype.org/content/repositories/snapshots 182 | 183 | 184 | ossrh 185 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 186 | 187 | 188 | 189 | 190 | 191 | 192 | org.apache.maven.plugins 193 | maven-gpg-plugin 194 | 3.1.0 195 | 196 | 197 | sign-artifacts 198 | verify 199 | 200 | sign 201 | 202 | 203 | 204 | --pinentry-mode 205 | loopback 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/PrefetchManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | /** 18 | * This interface is helper to notify when the prefetchThread should be resuming 19 | * messages. 20 | */ 21 | public interface PrefetchManager { 22 | 23 | /** 24 | * Notify the prefetchThread that the message is dispatched from 25 | * messageQueue when user calls for receive or message listener onMessage is 26 | * called. 27 | */ 28 | void messageDispatched(); 29 | 30 | /** 31 | * Notify the prefetchThread that the message listener has finished with any 32 | * previous message and is ready to accept another. 33 | */ 34 | void messageListenerReady(); 35 | 36 | /** 37 | * This is used to determine the state of the consumer, when the message 38 | * listener scheduler is processing the messages. 39 | * 40 | * @return The message consumer, which owns the prefetchThread 41 | */ 42 | SQSMessageConsumer getMessageConsumer(); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/ProviderConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | public class ProviderConfiguration { 18 | private int numberOfMessagesToPrefetch; 19 | 20 | public ProviderConfiguration() { 21 | // Set default numberOfMessagesToPrefetch to MIN_BATCH. 22 | this.numberOfMessagesToPrefetch = SQSMessagingClientConstants.MIN_BATCH; 23 | } 24 | 25 | public int getNumberOfMessagesToPrefetch() { 26 | return numberOfMessagesToPrefetch; 27 | } 28 | 29 | public void setNumberOfMessagesToPrefetch(int numberOfMessagesToPrefetch) { 30 | if (numberOfMessagesToPrefetch < SQSMessagingClientConstants.MIN_PREFETCH) { 31 | throw new IllegalArgumentException(String.format("Invalid prefetch size. Provided value '%1$s' cannot be smaller than '%2$s'", numberOfMessagesToPrefetch, SQSMessagingClientConstants.MIN_PREFETCH)); 32 | } 33 | this.numberOfMessagesToPrefetch = numberOfMessagesToPrefetch; 34 | } 35 | 36 | public ProviderConfiguration withNumberOfMessagesToPrefetch(int numberOfMessagesToPrefetch) { 37 | setNumberOfMessagesToPrefetch(numberOfMessagesToPrefetch); 38 | return this; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/SQSConnectionFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import jakarta.jms.ConnectionFactory; 18 | import jakarta.jms.JMSContext; 19 | import jakarta.jms.JMSException; 20 | import jakarta.jms.JMSRuntimeException; 21 | import jakarta.jms.QueueConnection; 22 | import jakarta.jms.QueueConnectionFactory; 23 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 24 | import software.amazon.awssdk.auth.credentials.AwsCredentials; 25 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 26 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 27 | import software.amazon.awssdk.services.sqs.SqsClient; 28 | import software.amazon.awssdk.services.sqs.SqsClientBuilder; 29 | 30 | import java.util.function.Supplier; 31 | 32 | /** 33 | * A ConnectionFactory object encapsulates a set of connection configuration 34 | * parameters for SqsClient as well as setting 35 | * numberOfMessagesToPrefetch. 36 | *

37 | * The numberOfMessagesToPrefetch parameter is used to size of the 38 | * prefetched messages, which can be tuned based on the application workload. It 39 | * helps in returning messages from internal buffers(if there is any) instead of 40 | * waiting for the SQS receiveMessage call to return. 41 | *

42 | * If more physical connections than the default maximum value (that is 50 as of 43 | * today) are needed on the connection pool, 44 | * {@link software.amazon.awssdk.core.client.config.ClientOverrideConfiguration} needs to be configured. 45 | *

46 | * None of the createConnection methods set-up the physical 47 | * connection to SQS, so validity of credentials are not checked with those 48 | * methods. 49 | */ 50 | 51 | public class SQSConnectionFactory implements ConnectionFactory, QueueConnectionFactory { 52 | private final ProviderConfiguration providerConfiguration; 53 | private final Supplier amazonSQSClientSupplier; 54 | 55 | /** 56 | * Creates a SQSConnectionFactory that uses default ProviderConfiguration 57 | * and SqsClientBuilder.standard() for creating SqsClient connections. 58 | * Every SQSConnection will have its own copy of SqsClient. 59 | */ 60 | public SQSConnectionFactory() { 61 | this(new ProviderConfiguration()); 62 | } 63 | 64 | /** 65 | * Creates a SQSConnectionFactory that uses SqsClientBuilder.standard() for creating SqsClient connections. 66 | * Every SQSConnection will have its own copy of SqsClient. 67 | */ 68 | public SQSConnectionFactory(ProviderConfiguration providerConfiguration) { 69 | this(providerConfiguration, SqsClient.create()); 70 | } 71 | 72 | /** 73 | * Creates a SQSConnectionFactory that uses the provided SqsClient connection. 74 | * Every SQSConnection will use the same provided SqsClient. 75 | */ 76 | public SQSConnectionFactory(ProviderConfiguration providerConfiguration, final SqsClient client) { 77 | if (providerConfiguration == null) { 78 | throw new IllegalArgumentException("Provider configuration cannot be null"); 79 | } 80 | if (client == null) { 81 | throw new IllegalArgumentException("AmazonSQS client cannot be null"); 82 | } 83 | this.providerConfiguration = providerConfiguration; 84 | this.amazonSQSClientSupplier = () -> client; 85 | } 86 | 87 | /** 88 | * Creates a SQSConnectionFactory that uses the provided SqsClientBuilder for creating AmazonSQS client connections. 89 | * Every SQSConnection will have its own copy of AmazonSQS client created through the provided builder. 90 | */ 91 | public SQSConnectionFactory(ProviderConfiguration providerConfiguration, final SqsClientBuilder clientBuilder) { 92 | if (providerConfiguration == null) { 93 | throw new IllegalArgumentException("Provider configuration cannot be null"); 94 | } 95 | if (clientBuilder == null) { 96 | throw new IllegalArgumentException("AmazonSQS client builder cannot be null"); 97 | } 98 | this.providerConfiguration = providerConfiguration; 99 | this.amazonSQSClientSupplier = clientBuilder::build; 100 | } 101 | 102 | 103 | @Override 104 | public SQSConnection createConnection() throws JMSException { 105 | try { 106 | SqsClient amazonSQS = amazonSQSClientSupplier.get(); 107 | return createConnection(amazonSQS, null); 108 | } catch (RuntimeException e) { 109 | throw (JMSException) new JMSException("Error creating SQS client: " + e.getMessage()).initCause(e); 110 | } 111 | } 112 | 113 | @Override 114 | public SQSConnection createConnection(String awsAccessKeyId, String awsSecretKey) throws JMSException { 115 | AwsBasicCredentials basicAWSCredentials = AwsBasicCredentials.create(awsAccessKeyId, awsSecretKey); 116 | return createConnection(basicAWSCredentials); 117 | } 118 | 119 | @Override 120 | public JMSContext createContext() { 121 | throw new JMSRuntimeException(SQSMessagingClientConstants.UNSUPPORTED_METHOD); 122 | } 123 | 124 | @Override 125 | public JMSContext createContext(String userName, String password) { 126 | throw new JMSRuntimeException(SQSMessagingClientConstants.UNSUPPORTED_METHOD); 127 | } 128 | 129 | @Override 130 | public JMSContext createContext(String userName, String password, int sessionMode) { 131 | throw new JMSRuntimeException(SQSMessagingClientConstants.UNSUPPORTED_METHOD); 132 | } 133 | 134 | @Override 135 | public JMSContext createContext(int sessionMode) { 136 | throw new JMSRuntimeException(SQSMessagingClientConstants.UNSUPPORTED_METHOD); 137 | } 138 | 139 | public SQSConnection createConnection(AwsCredentials awsCredentials) throws JMSException { 140 | AwsCredentialsProvider awsCredentialsProvider = StaticCredentialsProvider.create(awsCredentials); 141 | return createConnection(awsCredentialsProvider); 142 | } 143 | 144 | public SQSConnection createConnection(AwsCredentialsProvider awsCredentialsProvider) throws JMSException { 145 | try { 146 | SqsClient amazonSQS = amazonSQSClientSupplier.get(); 147 | return createConnection(amazonSQS, awsCredentialsProvider); 148 | } catch (Exception e) { 149 | throw (JMSException) new JMSException("Error creating SQS client: " + e.getMessage()).initCause(e); 150 | } 151 | } 152 | 153 | private SQSConnection createConnection(SqsClient amazonSQS, AwsCredentialsProvider awsCredentialsProvider) throws JMSException { 154 | AmazonSQSMessagingClientWrapper amazonSQSClientJMSWrapper = new AmazonSQSMessagingClientWrapper(amazonSQS, awsCredentialsProvider); 155 | return new SQSConnection(amazonSQSClientJMSWrapper, providerConfiguration.getNumberOfMessagesToPrefetch()); 156 | } 157 | 158 | @Override 159 | public QueueConnection createQueueConnection() throws JMSException { 160 | return createConnection(); 161 | } 162 | 163 | @Override 164 | public QueueConnection createQueueConnection(String userName, String password) throws JMSException { 165 | return createConnection(userName, password); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/SQSConnectionMetaData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import jakarta.jms.ConnectionMetaData; 18 | import jakarta.jms.JMSException; 19 | 20 | import java.util.Collections; 21 | import java.util.Enumeration; 22 | import java.util.List; 23 | 24 | 25 | public class SQSConnectionMetaData implements ConnectionMetaData { 26 | 27 | private final String jmsVersion; 28 | private final int jmsMajorVersion; 29 | private final int jmsMinorVersion; 30 | 31 | private final String jmsProviderName; 32 | private final String providerVersion; 33 | private final int providerMajorVersion; 34 | private final int providerMinorVersion; 35 | 36 | private final List jmsxProperty; 37 | 38 | SQSConnectionMetaData() { 39 | this.jmsVersion = "1.1"; 40 | this.jmsMajorVersion = 1; 41 | this.jmsMinorVersion = 1; 42 | 43 | this.jmsProviderName = "Amazon"; 44 | this.providerVersion = "1.0"; 45 | this.providerMajorVersion = 1; 46 | this.providerMinorVersion = 0; 47 | 48 | this.jmsxProperty = List.of(SQSMessagingClientConstants.JMSX_DELIVERY_COUNT, 49 | SQSMessagingClientConstants.JMSX_GROUP_ID, SQSMessagingClientConstants.JMSX_GROUP_SEC); 50 | } 51 | 52 | @Override 53 | public String getJMSVersion() throws JMSException { 54 | return jmsVersion; 55 | } 56 | 57 | @Override 58 | public int getJMSMajorVersion() throws JMSException { 59 | return jmsMajorVersion; 60 | } 61 | 62 | @Override 63 | public int getJMSMinorVersion() throws JMSException { 64 | return jmsMinorVersion; 65 | } 66 | 67 | @Override 68 | public String getJMSProviderName() throws JMSException { 69 | return jmsProviderName; 70 | } 71 | 72 | @Override 73 | public String getProviderVersion() throws JMSException { 74 | return providerVersion; 75 | } 76 | 77 | @Override 78 | public int getProviderMajorVersion() throws JMSException { 79 | return providerMajorVersion; 80 | } 81 | 82 | @Override 83 | public int getProviderMinorVersion() throws JMSException { 84 | return providerMinorVersion; 85 | } 86 | 87 | @Override 88 | public Enumeration getJMSXPropertyNames() throws JMSException { 89 | return Collections.enumeration(jmsxProperty); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/SQSMessageConsumer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.Acknowledger; 18 | import com.amazon.sqs.javamessaging.acknowledge.NegativeAcknowledger; 19 | import com.amazon.sqs.javamessaging.acknowledge.SQSMessageIdentifier; 20 | import jakarta.jms.IllegalStateException; 21 | import jakarta.jms.JMSException; 22 | import jakarta.jms.Message; 23 | import jakarta.jms.MessageConsumer; 24 | import jakarta.jms.MessageListener; 25 | import jakarta.jms.Queue; 26 | import jakarta.jms.QueueReceiver; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.util.List; 31 | import java.util.Set; 32 | import java.util.concurrent.ExecutorService; 33 | import java.util.concurrent.Executors; 34 | import java.util.concurrent.ThreadFactory; 35 | import java.util.concurrent.TimeUnit; 36 | 37 | /** 38 | * A client uses a MessageConsumer object to receive messages from a 39 | * destination. A MessageConsumer object is created by passing a Destination 40 | * object to a message-consumer creation method supplied by a session. 41 | *

42 | * This message consumer does not support message selectors 43 | *

44 | * A client may either synchronously receive a message consumer's messages or 45 | * have the consumer asynchronously deliver them as they arrive via registering 46 | * a MessageListener object. 47 | *

48 | * The message consumer creates a background thread to prefetch the messages to 49 | * improve the receive turn-around times. 50 | */ 51 | public class SQSMessageConsumer implements MessageConsumer, QueueReceiver { 52 | private static final Logger LOG = LoggerFactory.getLogger(SQSMessageConsumer.class); 53 | public static final int PREFETCH_EXECUTOR_GRACEFUL_SHUTDOWN_TIME = 30; 54 | 55 | protected volatile boolean closed = false; 56 | 57 | private final SQSQueueDestination sqsDestination; 58 | private final SQSSession parentSQSSession; 59 | 60 | private final SQSSessionCallbackScheduler sqsSessionRunnable; 61 | 62 | /** 63 | * Executor for prefetch thread. 64 | */ 65 | private final ExecutorService prefetchExecutor; 66 | 67 | /** 68 | * Prefetch Runnable. This includes keeping internal message buffer filled and call MessageListener if set. 69 | */ 70 | private final SQSMessageConsumerPrefetch sqsMessageConsumerPrefetch; 71 | 72 | SQSMessageConsumer(SQSConnection parentSQSConnection, SQSSession parentSQSSession, 73 | SQSSessionCallbackScheduler sqsSessionRunnable, SQSQueueDestination destination, 74 | Acknowledger acknowledger, NegativeAcknowledger negativeAcknowledger, ThreadFactory threadFactory) { 75 | this(parentSQSConnection, parentSQSSession, 76 | sqsSessionRunnable, destination, 77 | acknowledger, negativeAcknowledger, threadFactory, 78 | new SQSMessageConsumerPrefetch(sqsSessionRunnable, acknowledger, negativeAcknowledger, destination, 79 | parentSQSConnection.getWrappedAmazonSQSClient(), 80 | parentSQSConnection.getNumberOfMessagesToPrefetch())); 81 | 82 | } 83 | 84 | SQSMessageConsumer(SQSConnection parentSQSConnection, SQSSession parentSQSSession, 85 | SQSSessionCallbackScheduler sqsSessionRunnable, SQSQueueDestination destination, 86 | Acknowledger acknowledger, NegativeAcknowledger negativeAcknowledger, ThreadFactory threadFactory, 87 | SQSMessageConsumerPrefetch sqsMessageConsumerPrefetch) { 88 | this.parentSQSSession = parentSQSSession; 89 | this.sqsDestination = destination; 90 | this.sqsSessionRunnable = sqsSessionRunnable; 91 | this.sqsMessageConsumerPrefetch = sqsMessageConsumerPrefetch; 92 | this.sqsMessageConsumerPrefetch.setMessageConsumer(this); 93 | 94 | prefetchExecutor = Executors.newSingleThreadExecutor(threadFactory); 95 | prefetchExecutor.execute(sqsMessageConsumerPrefetch); 96 | } 97 | 98 | 99 | 100 | /** 101 | * Gets the queue destination associated with this queue receiver, where the 102 | * messages are delivered from. 103 | * 104 | * @return a queue destination 105 | */ 106 | @Override 107 | public Queue getQueue() throws JMSException { 108 | return sqsDestination; 109 | } 110 | 111 | /** 112 | * Gets the message consumer's MessageListener. 113 | * 114 | * @return a message listener 115 | */ 116 | @Override 117 | public MessageListener getMessageListener() throws JMSException { 118 | return sqsMessageConsumerPrefetch.getMessageListener(); 119 | } 120 | 121 | /** 122 | * Sets the message consumer's MessageListener. 123 | * 124 | * @param listener 125 | * a message listener to use for asynchronous message delivery 126 | * @throws JMSException 127 | * If the message consumer is closed 128 | */ 129 | @Override 130 | public void setMessageListener(MessageListener listener) throws JMSException { 131 | checkClosed(); 132 | this.sqsMessageConsumerPrefetch.setMessageListener(listener); 133 | } 134 | 135 | /** 136 | * This call blocks indefinitely until a message is produced or until this 137 | * message consumer is closed. When ConnectionState is stopped receive is 138 | * paused. 139 | * 140 | * @return the next message produced for this message consumer, or null if 141 | * this message consumer is closed during the receive call 142 | * @throws JMSException 143 | * On internal error 144 | */ 145 | @Override 146 | public Message receive() throws JMSException { 147 | checkClosed(); 148 | return sqsMessageConsumerPrefetch.receive(); 149 | } 150 | 151 | /** 152 | * This call blocks until a message arrives, the timeout expires, or this 153 | * message consumer is closed. A timeout of zero never expires, and the call 154 | * blocks indefinitely. 155 | * 156 | * @param timeout 157 | * the timeout value (in milliseconds) 158 | * @return the next message produced for this message consumer, or null if 159 | * the timeout expires or this message consumer is closed during the receive call 160 | * @throws JMSException 161 | * On internal error 162 | */ 163 | @Override 164 | public Message receive(long timeout) throws JMSException { 165 | checkClosed(); 166 | return sqsMessageConsumerPrefetch.receive(timeout); 167 | } 168 | 169 | /** 170 | * Receives the next message if one is immediately available. 171 | * 172 | * @return the next message produced for this message consumer, or null if 173 | * no message is available 174 | * @throws JMSException 175 | * On internal error 176 | */ 177 | @Override 178 | public Message receiveNoWait() throws JMSException { 179 | checkClosed(); 180 | return sqsMessageConsumerPrefetch.receiveNoWait(); 181 | } 182 | 183 | /** 184 | * Closes the message consumer. 185 | *

186 | * This will not return until receives and/or message listeners in progress 187 | * have completed. A blocked message consumer receive call returns null when 188 | * this consumer is closed. 189 | *

190 | * Since consumer prefetch threads use SQS long-poll feature with 20 seconds 191 | * timeout, closing each consumer prefetch thread can take up to 20 seconds, 192 | * which in-turn will impact the time on consumer close. 193 | *

194 | * This method may be called from a message listener's onMessage method on 195 | * its own consumer. After this method returns the onMessage method will be 196 | * allowed to complete normally, and the callback scheduler thread will be 197 | * closing the message consumer. 198 | * 199 | * @throws JMSException 200 | * On internal error. 201 | */ 202 | @Override 203 | public void close() throws JMSException { 204 | if (closed) { 205 | return; 206 | } 207 | 208 | if (parentSQSSession.isActiveCallbackSessionThread()) { 209 | sqsSessionRunnable.setConsumerCloseAfterCallback(this); 210 | return; 211 | } 212 | 213 | doClose(); 214 | } 215 | 216 | void doClose() { 217 | if (closed) { 218 | return; 219 | } 220 | 221 | sqsMessageConsumerPrefetch.close(); 222 | 223 | parentSQSSession.removeConsumer(this); 224 | 225 | try { 226 | if (!prefetchExecutor.isShutdown()) { 227 | LOG.debug("Shutting down {} executor", SQSSession.CONSUMER_PREFETCH_EXECUTOR_NAME); 228 | // Shut down executor. 229 | prefetchExecutor.shutdown(); 230 | } 231 | 232 | parentSQSSession.waitForConsumerCallbackToComplete(this); 233 | 234 | if (!prefetchExecutor.awaitTermination(PREFETCH_EXECUTOR_GRACEFUL_SHUTDOWN_TIME, TimeUnit.SECONDS)) { 235 | 236 | LOG.warn("Can't terminate executor service {} after {} seconds, some running threads will be shutdown immediately", 237 | SQSSession.CONSUMER_PREFETCH_EXECUTOR_NAME, PREFETCH_EXECUTOR_GRACEFUL_SHUTDOWN_TIME); 238 | prefetchExecutor.shutdownNow(); 239 | } 240 | } catch (InterruptedException e) { 241 | LOG.error("Interrupted while closing the consumer.", e); 242 | } 243 | 244 | closed = true; 245 | } 246 | 247 | boolean isClosed() { 248 | return closed; 249 | } 250 | 251 | /** This method is not supported. */ 252 | @Override 253 | public String getMessageSelector() throws JMSException { 254 | throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD); 255 | } 256 | 257 | /** This stops the prefetching */ 258 | protected void stopPrefetch() { 259 | if (!closed) { 260 | sqsMessageConsumerPrefetch.stop(); 261 | } 262 | } 263 | 264 | /** This starts the prefetching */ 265 | protected void startPrefetch() { 266 | if (!closed) { 267 | sqsMessageConsumerPrefetch.start(); 268 | } 269 | } 270 | 271 | private void checkClosed() throws IllegalStateException { 272 | if (closed) { 273 | throw new IllegalStateException("Consumer is closed"); 274 | } 275 | } 276 | 277 | List purgePrefetchedMessagesWithGroups(Set affectedGroups) throws JMSException { 278 | return sqsMessageConsumerPrefetch.purgePrefetchedMessagesWithGroups(affectedGroups); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/SQSMessagingClientConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import jakarta.jms.ConnectionMetaData; 18 | import jakarta.jms.JMSException; 19 | 20 | 21 | public class SQSMessagingClientConstants { 22 | 23 | public static final String UNSUPPORTED_METHOD = "Unsupported Method"; 24 | 25 | public static final ConnectionMetaData CONNECTION_METADATA = new SQSConnectionMetaData(); 26 | 27 | public static final int MAX_BATCH = 10; 28 | 29 | public static final int MIN_BATCH = 1; 30 | 31 | public static final int MIN_PREFETCH = 0; 32 | 33 | /** 34 | * JMSMessage available user property types, which are mapped to message 35 | * attribute data types 36 | */ 37 | public static final String STRING = "String"; 38 | 39 | public static final String NUMBER = "Number"; 40 | 41 | public static final String INT = "Number.int"; 42 | 43 | public static final String BOOLEAN = "Number.Boolean"; 44 | 45 | public static final String BYTE = "Number.byte"; 46 | 47 | public static final String DOUBLE = "Number.double"; 48 | 49 | public static final String FLOAT = "Number.float"; 50 | 51 | public static final String LONG = "Number.long"; 52 | 53 | public static final String SHORT = "Number.short"; 54 | 55 | public static final String BINARY = "Binary"; 56 | 57 | public static final String INT_FALSE = "0"; 58 | 59 | public static final String INT_TRUE = "1"; 60 | 61 | public static final String MESSAGE_ID_FORMAT = "ID:%s"; 62 | 63 | public static final String JMSX_DELIVERY_COUNT = "JMSXDeliveryCount"; 64 | 65 | public static final String JMSX_GROUP_ID = "JMSXGroupID"; 66 | 67 | public static final String JMSX_GROUP_SEC = "JMSXGroupSeq"; 68 | 69 | public static final String JMS_SQS_DEDUPLICATION_ID = "JMS_SQS_DeduplicationId"; 70 | 71 | public static final String JMS_SQS_SEQUENCE_NUMBER = "JMS_SQS_SequenceNumber"; 72 | 73 | public static final String APPROXIMATE_RECEIVE_COUNT = "ApproximateReceiveCount"; 74 | 75 | public static final String SENT_TIMESTAMP = "SentTimestamp"; 76 | 77 | public static final String MESSAGE_DEDUPLICATION_ID = "MessageDeduplicationId"; 78 | 79 | public static final String MESSAGE_GROUP_ID = "MessageGroupId"; 80 | 81 | public static final String SEQUENCE_NUMBER = "SequenceNumber"; 82 | 83 | static final String APPENDED_USER_AGENT_HEADER_VERSION; 84 | static { 85 | try { 86 | APPENDED_USER_AGENT_HEADER_VERSION = String.format( 87 | "/SQS Java Messaging Client v%s", CONNECTION_METADATA.getProviderVersion()); 88 | } catch (JMSException e) { 89 | throw new ExceptionInInitializerError(e); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/SQSQueueDestination.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import jakarta.jms.Destination; 18 | import jakarta.jms.Queue; 19 | 20 | /** 21 | * A SQSQueueDestination object encapsulates a queue name and SQS specific queue 22 | * URL. This is the way a client specifies the identity of a queue to JMS API 23 | * methods. 24 | */ 25 | public class SQSQueueDestination implements Destination, Queue { 26 | 27 | private final String queueName; 28 | 29 | private final String queueUrl; 30 | 31 | private final boolean isFifo; 32 | 33 | SQSQueueDestination(String queueName, String queueUrl) { 34 | this.queueName = queueName; 35 | this.queueUrl = queueUrl; 36 | this.isFifo = this.queueName.endsWith(".fifo"); 37 | } 38 | 39 | /** 40 | * Returns the name of this queue. 41 | * 42 | * @return queueName 43 | */ 44 | @Override 45 | public String getQueueName() { 46 | return this.queueName; 47 | } 48 | 49 | /** 50 | * Returns the queueUrl of this queue. 51 | * 52 | * @return queueUrl 53 | */ 54 | public String getQueueUrl() { 55 | return this.queueUrl; 56 | } 57 | 58 | public boolean isFifo() { 59 | return this.isFifo; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "SQSDestination [queueName=" + queueName + ", queueUrl=" + queueUrl + "]"; 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | final int prime = 31; 70 | int result = 1; 71 | result = prime * result + ((queueName == null) ? 0 : queueName.hashCode()); 72 | result = prime * result + ((queueUrl == null) ? 0 : queueUrl.hashCode()); 73 | return result; 74 | } 75 | 76 | @Override 77 | public boolean equals(Object obj) { 78 | if (this == obj) 79 | return true; 80 | if (obj == null) 81 | return false; 82 | if (getClass() != obj.getClass()) 83 | return false; 84 | SQSQueueDestination other = (SQSQueueDestination) obj; 85 | if (queueName == null) { 86 | if (other.queueName != null) 87 | return false; 88 | } else if (!queueName.equals(other.queueName)) 89 | return false; 90 | if (queueUrl == null) { 91 | return other.queueUrl == null; 92 | } else return queueUrl.equals(other.queueUrl); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/SQSSessionCallbackScheduler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.SQSMessageConsumerPrefetch.MessageManager; 18 | import com.amazon.sqs.javamessaging.SQSSession.CallbackEntry; 19 | import com.amazon.sqs.javamessaging.acknowledge.AcknowledgeMode; 20 | import com.amazon.sqs.javamessaging.acknowledge.Acknowledger; 21 | import com.amazon.sqs.javamessaging.acknowledge.NegativeAcknowledger; 22 | import com.amazon.sqs.javamessaging.acknowledge.SQSMessageIdentifier; 23 | import com.amazon.sqs.javamessaging.message.SQSMessage; 24 | import jakarta.jms.JMSException; 25 | import jakarta.jms.MessageListener; 26 | import jakarta.jms.Session; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.util.ArrayDeque; 31 | import java.util.ArrayList; 32 | import java.util.Collections; 33 | import java.util.Iterator; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.Set; 37 | 38 | /** 39 | * Used internally to guarantee serial execution of message processing on 40 | * consumer message listeners. 41 | */ 42 | public class SQSSessionCallbackScheduler implements Runnable { 43 | private static final Logger LOG = LoggerFactory.getLogger(SQSSessionCallbackScheduler.class); 44 | 45 | protected ArrayDeque callbackQueue; 46 | 47 | private final AcknowledgeMode acknowledgeMode; 48 | 49 | private final SQSSession session; 50 | 51 | private final NegativeAcknowledger negativeAcknowledger; 52 | 53 | private final Acknowledger acknowledger; 54 | 55 | /** 56 | * Only set from the callback thread to transfer the ownership of closing 57 | * the consumer. The message listener's onMessage method can call 58 | * close on its own consumer. After message consumer 59 | * close returns the onMessage method should be allowed to 60 | * complete normally and this thread will be responsible to finish the 61 | * close on message consumer. 62 | */ 63 | private SQSMessageConsumer consumerCloseAfterCallback; 64 | 65 | private volatile boolean closed = false; 66 | 67 | SQSSessionCallbackScheduler(SQSSession session, AcknowledgeMode acknowledgeMode, Acknowledger acknowledger, NegativeAcknowledger negativeAcknowledger) { 68 | this.session = session; 69 | this.acknowledgeMode = acknowledgeMode; 70 | this.acknowledger = acknowledger; 71 | this.negativeAcknowledger = negativeAcknowledger; 72 | callbackQueue = new ArrayDeque<>(); 73 | } 74 | 75 | /** 76 | * Used in case no consumers have started, and session needs to terminate 77 | * the thread 78 | */ 79 | void close() { 80 | closed = true; 81 | /* Wake-up the thread in case it was blocked on empty queue */ 82 | synchronized (callbackQueue) { 83 | callbackQueue.notify(); 84 | } 85 | } 86 | 87 | @Override 88 | public void run() { 89 | CallbackEntry callbackEntry = null; 90 | try { 91 | while (true) { 92 | try { 93 | if (closed) { 94 | break; 95 | } 96 | synchronized (callbackQueue) { 97 | callbackEntry = callbackQueue.pollFirst(); 98 | if (callbackEntry == null) { 99 | try { 100 | callbackQueue.wait(); 101 | } catch (InterruptedException e) { 102 | /* 103 | * Will be retried on the next loop, and 104 | * break if the callback scheduler is closed. 105 | */ 106 | LOG.debug("wait on empty callback queue interrupted: {}", e.getMessage()); 107 | } 108 | continue; 109 | } 110 | } 111 | 112 | MessageListener messageListener = callbackEntry.messageListener(); 113 | MessageManager messageManager = callbackEntry.messageManager(); 114 | SQSMessage message = (SQSMessage) messageManager.message(); 115 | SQSMessageConsumer messageConsumer = messageManager.prefetchManager().getMessageConsumer(); 116 | if (messageConsumer.isClosed()) { 117 | nackReceivedMessage(message); 118 | continue; 119 | } 120 | 121 | try { 122 | // this takes care of start and stop 123 | session.startingCallback(messageConsumer); 124 | } catch (JMSException e) { 125 | LOG.debug("Not running callback: {}", e.getMessage()); 126 | break; 127 | } 128 | 129 | try { 130 | /* 131 | * Notifying consumer prefetch thread so that it can 132 | * continue to prefetch 133 | */ 134 | messageManager.prefetchManager().messageDispatched(); 135 | int ackMode = acknowledgeMode.getOriginalAcknowledgeMode(); 136 | boolean tryNack = true; 137 | try { 138 | if (messageListener != null) { 139 | if (ackMode != Session.AUTO_ACKNOWLEDGE) { 140 | acknowledger.notifyMessageReceived(message); 141 | } 142 | boolean callbackFailed = false; 143 | try { 144 | messageListener.onMessage(message); 145 | } catch (Throwable ex) { 146 | LOG.warn("Exception thrown from onMessage callback for message {}", 147 | message.getSQSMessageId(), ex); 148 | callbackFailed = true; 149 | } finally { 150 | if (!callbackFailed) { 151 | if (ackMode == Session.AUTO_ACKNOWLEDGE) { 152 | message.acknowledge(); 153 | } 154 | tryNack = false; 155 | } 156 | } 157 | } 158 | } catch (JMSException ex) { 159 | LOG.warn("Unable to complete message dispatch for the message {}", 160 | message.getSQSMessageId(), ex); 161 | } finally { 162 | if (tryNack) { 163 | nackReceivedMessage(message); 164 | } 165 | } 166 | 167 | /* 168 | * The consumer close is delegated to the session thread 169 | * if consumer close is called by its message listener's 170 | * onMessage method on its own consumer. 171 | */ 172 | if (consumerCloseAfterCallback != null) { 173 | consumerCloseAfterCallback.doClose(); 174 | consumerCloseAfterCallback = null; 175 | } 176 | } finally { 177 | session.finishedCallback(); 178 | 179 | // Let the prefetch manager know we're available to 180 | // process another message (if there is a still a listener attached). 181 | messageManager.prefetchManager().messageListenerReady(); 182 | } 183 | } catch (Throwable ex) { 184 | LOG.error("Unexpected exception thrown during the run of the scheduled callback", ex); 185 | } 186 | } 187 | } finally { 188 | if (callbackEntry != null) { 189 | nackReceivedMessage((SQSMessage) callbackEntry.messageManager().message()); 190 | } 191 | nackQueuedMessages(); 192 | } 193 | } 194 | 195 | void setConsumerCloseAfterCallback(SQSMessageConsumer messageConsumer) { 196 | consumerCloseAfterCallback = messageConsumer; 197 | } 198 | 199 | void scheduleCallBacks(MessageListener messageListener, List messageManagers) { 200 | synchronized (callbackQueue) { 201 | try { 202 | for (MessageManager messageManager : messageManagers) { 203 | CallbackEntry callbackEntry = new CallbackEntry(messageListener, messageManager); 204 | callbackQueue.addLast(callbackEntry); 205 | } 206 | } finally { 207 | callbackQueue.notify(); 208 | } 209 | } 210 | } 211 | 212 | void nackQueuedMessages() { 213 | synchronized (callbackQueue) { 214 | try { 215 | List nackMessageIdentifiers = new ArrayList<>(); 216 | while (!callbackQueue.isEmpty()) { 217 | SQSMessage nackMessage = (SQSMessage) callbackQueue.pollFirst().messageManager().message(); 218 | nackMessageIdentifiers.add(SQSMessageIdentifier.fromSQSMessage(nackMessage)); 219 | } 220 | 221 | if (!nackMessageIdentifiers.isEmpty()) { 222 | negativeAcknowledger.bulkAction(nackMessageIdentifiers, nackMessageIdentifiers.size()); 223 | } 224 | } catch (JMSException e) { 225 | LOG.warn("Caught exception while nacking the remaining messages on session callback queue", e); 226 | } 227 | } 228 | } 229 | 230 | private void nackReceivedMessage(SQSMessage message) { 231 | try { 232 | SQSMessageIdentifier messageIdentifier = SQSMessageIdentifier.fromSQSMessage(message); 233 | List nackMessageIdentifiers = new ArrayList<>(); 234 | nackMessageIdentifiers.add(messageIdentifier); 235 | 236 | //failing to process a message with a specific group id means we have to nack all the pending messages with the same group id 237 | //to prevent processing messages out of order 238 | if (messageIdentifier.getGroupId() != null) { 239 | //prepare a map of single queueUrl with single group to purge 240 | Map> queueToGroupsMapping = Collections.singletonMap(messageIdentifier.getQueueUrl(), Collections.singleton(messageIdentifier.getGroupId())); 241 | nackMessageIdentifiers.addAll(purgeScheduledCallbacksForQueuesAndGroups(queueToGroupsMapping)); 242 | } 243 | 244 | negativeAcknowledger.bulkAction(nackMessageIdentifiers, nackMessageIdentifiers.size()); 245 | } catch (JMSException e) { 246 | LOG.warn("Unable to nack the message {}", message.getSQSMessageId(), e); 247 | } 248 | } 249 | 250 | List purgeScheduledCallbacksForQueuesAndGroups(Map> queueToGroupsMapping) throws JMSException { 251 | List purgedCallbacks = new ArrayList<>(); 252 | synchronized (callbackQueue) { 253 | //let's walk over the callback queue 254 | Iterator callbackIterator = callbackQueue.iterator(); 255 | while (callbackIterator.hasNext()) { 256 | CallbackEntry callbackEntry = callbackIterator.next(); 257 | SQSMessageIdentifier pendingCallbackIdentifier = SQSMessageIdentifier.fromSQSMessage((SQSMessage) callbackEntry.messageManager().message()); 258 | 259 | //is the callback entry for one of the affected queues? 260 | Set affectedGroupsInQueue = queueToGroupsMapping.get(pendingCallbackIdentifier.getQueueUrl()); 261 | if (affectedGroupsInQueue != null) { 262 | 263 | //is the callback entry for one of the affected group ids? 264 | if (affectedGroupsInQueue.contains(pendingCallbackIdentifier.getGroupId())) { 265 | //we will purge this callback 266 | purgedCallbacks.add(pendingCallbackIdentifier); 267 | //remove from callback queue 268 | callbackIterator.remove(); 269 | //notify prefetcher for that message that we are done with it and it can prefetch more messages 270 | callbackEntry.messageManager().prefetchManager().messageDispatched(); 271 | } 272 | } 273 | } 274 | } 275 | return purgedCallbacks; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/AcknowledgeMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.AmazonSQSMessagingClientWrapper; 18 | import com.amazon.sqs.javamessaging.SQSSession; 19 | 20 | /** 21 | *

22 | * Specifies the different possible modes of acknowledgment: 23 | *

33 | */ 34 | public enum AcknowledgeMode { 35 | ACK_AUTO, ACK_UNORDERED, ACK_RANGE; 36 | 37 | private int originalAcknowledgeMode; 38 | 39 | /** 40 | * Sets the acknowledgment mode. 41 | */ 42 | public AcknowledgeMode withOriginalAcknowledgeMode(int originalAcknowledgeMode) { 43 | this.originalAcknowledgeMode = originalAcknowledgeMode; 44 | return this; 45 | } 46 | 47 | /** 48 | * Returns the acknowledgment mode. 49 | */ 50 | public int getOriginalAcknowledgeMode() { 51 | return originalAcknowledgeMode; 52 | } 53 | 54 | /** 55 | * Creates the acknowledger associated with the session, which will be used 56 | * to acknowledge the delivered messages on consumers of the session. 57 | * 58 | * @param amazonSQSClient 59 | * the SQS client to delete messages 60 | * @param parentSQSSession 61 | * the associated session for the acknowledger 62 | */ 63 | public Acknowledger createAcknowledger(AmazonSQSMessagingClientWrapper amazonSQSClient, SQSSession parentSQSSession) { 64 | return switch (this) { 65 | case ACK_AUTO -> new AutoAcknowledger(amazonSQSClient, parentSQSSession); 66 | case ACK_RANGE -> new RangedAcknowledger(amazonSQSClient, parentSQSSession); 67 | case ACK_UNORDERED -> new UnorderedAcknowledger(amazonSQSClient, parentSQSSession); 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/Acknowledger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.message.SQSMessage; 18 | import jakarta.jms.JMSException; 19 | 20 | import java.util.List; 21 | 22 | public interface Acknowledger { 23 | 24 | /** 25 | * Generic Acknowledge method. This method will delete message(s) in SQS Queue. 26 | * 27 | * @param message 28 | * message to acknowledge. 29 | * @throws JMSException 30 | */ 31 | void acknowledge(SQSMessage message) throws JMSException; 32 | 33 | /** 34 | * Used when receiving messages. Depending on acknowledge mode this will 35 | * help create list of message backlog. 36 | * 37 | * @param message 38 | * notify acknowledger message is received 39 | * @throws JMSException 40 | */ 41 | void notifyMessageReceived(SQSMessage message) throws JMSException; 42 | 43 | /** 44 | * Used in negative acknowledge. Gets all delivered but not acknowledged 45 | * messages. 46 | */ 47 | List getUnAckMessages(); 48 | 49 | /** 50 | * Deletes all not acknowledged delivered messages. 51 | */ 52 | void forgetUnAckMessages(); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/AutoAcknowledger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.AmazonSQSMessagingClientWrapper; 18 | import com.amazon.sqs.javamessaging.SQSSession; 19 | import com.amazon.sqs.javamessaging.message.SQSMessage; 20 | import jakarta.jms.JMSException; 21 | import software.amazon.awssdk.services.sqs.model.DeleteMessageRequest; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | /** 27 | * Used by session to automatically acknowledge a client's receipt of a message 28 | * either when the session has successfully returned from a call to receive or 29 | * when the message listener the session has called to process the message 30 | * successfully returns. 31 | */ 32 | public class AutoAcknowledger implements Acknowledger { 33 | 34 | private final AmazonSQSMessagingClientWrapper amazonSQSClient; 35 | private final SQSSession session; 36 | 37 | public AutoAcknowledger(AmazonSQSMessagingClientWrapper amazonSQSClient, SQSSession session) { 38 | this.amazonSQSClient = amazonSQSClient; 39 | this.session = session; 40 | } 41 | 42 | /** Acknowledges the consumed message via calling deleteMessage */ 43 | @Override 44 | public void acknowledge(SQSMessage message) throws JMSException { 45 | session.checkClosed(); 46 | amazonSQSClient.deleteMessage(DeleteMessageRequest.builder() 47 | .queueUrl(message.getQueueUrl()) 48 | .receiptHandle(message.getReceiptHandle()) 49 | .build()); 50 | } 51 | 52 | /** 53 | * When notify message is received, it will acknowledge the message. 54 | */ 55 | @Override 56 | public void notifyMessageReceived(SQSMessage message) throws JMSException { 57 | acknowledge(message); 58 | } 59 | 60 | /** 61 | * AutoAcknowledge doesn't need to do anything in this method. Return an 62 | * empty list. 63 | */ 64 | @Override 65 | public List getUnAckMessages() { 66 | return new ArrayList<>(); 67 | } 68 | 69 | /** AutoAcknowledge doesn't need to do anything in this method. */ 70 | @Override 71 | public void forgetUnAckMessages() { 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/BulkSQSOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.SQSMessagingClientConstants; 18 | import jakarta.jms.JMSException; 19 | 20 | import java.util.ArrayList; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Map.Entry; 25 | 26 | /** 27 | * This is used by different acknowledgers that requires partitioning of the 28 | * list, and execute actions on the partitions 29 | */ 30 | public abstract class BulkSQSOperation { 31 | 32 | /** 33 | * Bulk action on list of message identifiers up to the provided index 34 | * 35 | * @param messageIdentifierList 36 | * Container for the list of message identifiers 37 | * @param indexOfMessage 38 | * The action will apply to all messages up to this index 39 | * @throws JMSException 40 | * if action throws 41 | */ 42 | public void bulkAction(List messageIdentifierList, int indexOfMessage) 43 | throws JMSException { 44 | 45 | assert indexOfMessage > 0; 46 | assert indexOfMessage <= messageIdentifierList.size(); 47 | 48 | Map> receiptHandleWithSameQueueUrl = new HashMap<>(); 49 | 50 | // Add all messages up to and including requested message into Map. 51 | // Map contains key as queueUrl and value as list receiptHandles from 52 | // that queueUrl. 53 | 54 | for (int i = 0; i < indexOfMessage; i++) { 55 | SQSMessageIdentifier messageIdentifier = messageIdentifierList.get(i); 56 | String queueUrl = messageIdentifier.getQueueUrl(); 57 | List receiptHandles = receiptHandleWithSameQueueUrl.computeIfAbsent(queueUrl, k -> new ArrayList<>()); 58 | // if value of queueUrl is null create new list. 59 | // add receiptHandle to the list. 60 | receiptHandles.add(messageIdentifier.getReceiptHandle()); 61 | // Once there are 10 messages in messageBatch, apply the batch action 62 | if (receiptHandles.size() == SQSMessagingClientConstants.MAX_BATCH) { 63 | action(queueUrl, receiptHandles); 64 | receiptHandles.clear(); 65 | } 66 | } 67 | 68 | // Flush rest of messages in map. 69 | for (Entry> entry : receiptHandleWithSameQueueUrl.entrySet()) { 70 | action(entry.getKey(), entry.getValue()); 71 | } 72 | } 73 | 74 | /** 75 | * Action call block. This action can be applied on multiple messages for 76 | * the same queue. 77 | * 78 | * @param queueUrl 79 | * queueUrl of the queue, which the receipt handles belong 80 | * @param receiptHandles 81 | * the list of handles, which is be used to (negative)acknowledge 82 | * the messages. 83 | * @throws JMSException 84 | */ 85 | public abstract void action(String queueUrl, List receiptHandles) throws JMSException; 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/NegativeAcknowledger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.AmazonSQSMessagingClientWrapper; 18 | import com.amazon.sqs.javamessaging.SQSMessageConsumerPrefetch.MessageManager; 19 | import com.amazon.sqs.javamessaging.SQSMessagingClientConstants; 20 | import com.amazon.sqs.javamessaging.message.SQSMessage; 21 | import jakarta.jms.JMSException; 22 | import software.amazon.awssdk.services.sqs.model.ChangeMessageVisibilityBatchRequest; 23 | import software.amazon.awssdk.services.sqs.model.ChangeMessageVisibilityBatchRequestEntry; 24 | 25 | import java.util.ArrayDeque; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | /** 30 | * Used to negative acknowledge of group of messages. 31 | *

32 | * Negative acknowledge resets the visibility timeout of a message, so that the 33 | * message can be immediately available to consume. This is mostly used on 34 | * recover and close methods. 35 | *

36 | * Negative acknowledge can potentially cause duplicate deliveries. 37 | */ 38 | public class NegativeAcknowledger extends BulkSQSOperation { 39 | 40 | private static final int NACK_TIMEOUT = 0; 41 | 42 | private final AmazonSQSMessagingClientWrapper amazonSQSClient; 43 | 44 | public NegativeAcknowledger(AmazonSQSMessagingClientWrapper amazonSQSClient) { 45 | this.amazonSQSClient = amazonSQSClient; 46 | } 47 | 48 | /** 49 | * Bulk action for negative acknowledge on the list of messages of a 50 | * specific queue. 51 | * 52 | * @param messageQueue 53 | * Container for the list of message managers. 54 | * @param queueUrl 55 | * The queueUrl of the messages, which they received from. 56 | * @throws JMSException 57 | * If action throws. 58 | */ 59 | public void bulkAction(ArrayDeque messageQueue, String queueUrl) throws JMSException { 60 | List receiptHandles = new ArrayList<>(); 61 | while (!messageQueue.isEmpty()) { 62 | receiptHandles.add(((SQSMessage) (messageQueue.pollFirst().message())).getReceiptHandle()); 63 | 64 | // If there is more than 10 stop can call action 65 | if (receiptHandles.size() == SQSMessagingClientConstants.MAX_BATCH) { 66 | action(queueUrl, receiptHandles); 67 | receiptHandles.clear(); 68 | } 69 | } 70 | action(queueUrl, receiptHandles); 71 | } 72 | 73 | /** 74 | * Action call block for negative acknowledge for the list of receipt 75 | * handles. This action can be applied on multiple messages for the same 76 | * queue. 77 | * 78 | * @param queueUrl 79 | * The queueUrl of the queue, which the receipt handles belong. 80 | * @param receiptHandles 81 | * The list of handles, which is be used to negative acknowledge 82 | * the messages via using 83 | * changeMessageVisibilityBatch. 84 | * @throws JMSException 85 | * If changeMessageVisibilityBatch throws. 86 | */ 87 | @Override 88 | public void action(String queueUrl, List receiptHandles) throws JMSException { 89 | if (receiptHandles == null || receiptHandles.isEmpty()) { 90 | return; 91 | } 92 | 93 | List nackEntries = new ArrayList<>(receiptHandles.size()); 94 | int batchId = 0; 95 | for (String messageReceiptHandle : receiptHandles) { 96 | ChangeMessageVisibilityBatchRequestEntry changeMessageVisibilityBatchRequestEntry = ChangeMessageVisibilityBatchRequestEntry.builder() 97 | .id(Integer.toString(batchId)) 98 | .receiptHandle(messageReceiptHandle) 99 | .visibilityTimeout(NACK_TIMEOUT) 100 | .build(); 101 | nackEntries.add(changeMessageVisibilityBatchRequestEntry); 102 | batchId++; 103 | } 104 | amazonSQSClient.changeMessageVisibilityBatch(ChangeMessageVisibilityBatchRequest.builder() 105 | .queueUrl(queueUrl) 106 | .entries(nackEntries) 107 | .build()); 108 | } 109 | 110 | } 111 | 112 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/RangedAcknowledger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.AmazonSQSMessagingClientWrapper; 18 | import com.amazon.sqs.javamessaging.SQSSession; 19 | import com.amazon.sqs.javamessaging.message.SQSMessage; 20 | import jakarta.jms.JMSException; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; 24 | import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequestEntry; 25 | 26 | import java.util.ArrayList; 27 | import java.util.LinkedList; 28 | import java.util.List; 29 | import java.util.Queue; 30 | 31 | /** 32 | * Used to acknowledge group of messages. Acknowledging a consumed message 33 | * acknowledges all messages that the session has consumed before and including 34 | * that message. 35 | *

36 | * A big backlog of consumed messages can cause memory pressure, as well as an 37 | * increase on the probability of duplicates. 38 | *

39 | * This class is not safe for concurrent use. 40 | */ 41 | public class RangedAcknowledger extends BulkSQSOperation implements Acknowledger { 42 | private static final Logger LOG = LoggerFactory.getLogger(RangedAcknowledger.class); 43 | 44 | private final AmazonSQSMessagingClientWrapper amazonSQSClient; 45 | 46 | private final SQSSession session; 47 | 48 | private final Queue unAckMessages; 49 | 50 | public RangedAcknowledger(AmazonSQSMessagingClientWrapper amazonSQSClient, SQSSession session) { 51 | this.amazonSQSClient = amazonSQSClient; 52 | this.session = session; 53 | this.unAckMessages = new LinkedList<>(); 54 | } 55 | 56 | /** 57 | * Acknowledges all the consumed messages as well as the previously consumed 58 | * messages on the session via calling deleteMessageBatch until 59 | * all the messages are deleted. 60 | */ 61 | @Override 62 | public void acknowledge(SQSMessage message) throws JMSException { 63 | session.checkClosed(); 64 | 65 | SQSMessageIdentifier ackMessage = SQSMessageIdentifier.fromSQSMessage(message); 66 | 67 | int indexOfMessage = indexOf(ackMessage); 68 | 69 | /* 70 | * In case the message has already been deleted, warn user about it and 71 | * return. If not then it should continue with acknowledging all 72 | * the messages received before that 73 | */ 74 | if (indexOfMessage == -1) { 75 | LOG.warn("SQSMessageID: {} with SQSMessageReceiptHandle: {} does not exist.", message.getSQSMessageId(), 76 | message.getReceiptHandle()); 77 | } else { 78 | bulkAction(getUnAckMessages(), indexOfMessage); 79 | } 80 | } 81 | 82 | /** 83 | * Return the index of message if the message is in queue. Return -1 if 84 | * message does not exist in queue. 85 | */ 86 | private int indexOf(SQSMessageIdentifier findMessage) { 87 | int i = 0; 88 | for (SQSMessageIdentifier sqsMessageIdentifier : unAckMessages) { 89 | i++; 90 | if (sqsMessageIdentifier.equals(findMessage)) { 91 | return i; 92 | } 93 | } 94 | return -1; 95 | } 96 | 97 | /** 98 | * Updates the internal queue for the consumed but not acknowledged 99 | * messages if the message was not already on queue. 100 | */ 101 | @Override 102 | public void notifyMessageReceived(SQSMessage message) throws JMSException { 103 | SQSMessageIdentifier messageIdentifier = SQSMessageIdentifier.fromSQSMessage(message); 104 | if (!unAckMessages.contains(messageIdentifier)) { 105 | unAckMessages.add(messageIdentifier); 106 | } 107 | } 108 | 109 | /** 110 | * Returns the list of all consumed but not acknowledged messages. 111 | */ 112 | @Override 113 | public List getUnAckMessages() { 114 | return new ArrayList<>(unAckMessages); 115 | } 116 | 117 | /** 118 | * Clears the list of not acknowledged messages. 119 | */ 120 | @Override 121 | public void forgetUnAckMessages() { 122 | unAckMessages.clear(); 123 | } 124 | 125 | /** 126 | * Acknowledges up to 10 messages via calling 127 | * deleteMessageBatch. 128 | */ 129 | @Override 130 | public void action(String queueUrl, List receiptHandles) throws JMSException { 131 | if (receiptHandles == null || receiptHandles.isEmpty()) { 132 | return; 133 | } 134 | 135 | List deleteMessageBatchRequestEntries = new ArrayList<>(); 136 | int batchId = 0; 137 | for (String receiptHandle : receiptHandles) { 138 | // Remove the message from queue of unAckMessages 139 | unAckMessages.poll(); 140 | 141 | DeleteMessageBatchRequestEntry entry = DeleteMessageBatchRequestEntry.builder() 142 | .id( Integer.toString(batchId)) 143 | .receiptHandle(receiptHandle) 144 | .build(); 145 | deleteMessageBatchRequestEntries.add(entry); 146 | batchId++; 147 | } 148 | 149 | DeleteMessageBatchRequest deleteMessageBatchRequest = DeleteMessageBatchRequest 150 | .builder() 151 | .queueUrl(queueUrl) 152 | .entries(deleteMessageBatchRequestEntries) 153 | .build(); 154 | /* 155 | * TODO: If one of the batch calls fail, then the remaining messages on 156 | * the batch will not be deleted, and will be visible and delivered as 157 | * duplicate after visibility timeout expires. 158 | */ 159 | amazonSQSClient.deleteMessageBatch(deleteMessageBatchRequest); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/SQSMessageIdentifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.message.SQSMessage; 18 | import jakarta.jms.JMSException; 19 | 20 | /** 21 | * Identifies an SQS message, when (negative)acknowledging the message 22 | */ 23 | public class SQSMessageIdentifier { 24 | 25 | // The queueUrl where the message was sent or received from 26 | private final String queueUrl; 27 | 28 | // The receipt handle returned after the delivery of the message from SQS 29 | private final String receiptHandle; 30 | 31 | // The SQS message id assigned on send. 32 | private final String sqsMessageId; 33 | 34 | // The group id to which the message belongs 35 | private String groupId; 36 | 37 | public SQSMessageIdentifier(String queueUrl, String receiptHandle, String sqsMessageId) { 38 | this(queueUrl, receiptHandle, sqsMessageId, null); 39 | } 40 | 41 | public SQSMessageIdentifier(String queueUrl, String receiptHandle, String sqsMessageId, String groupId) { 42 | this.queueUrl = queueUrl; 43 | this.receiptHandle = receiptHandle; 44 | this.sqsMessageId = sqsMessageId; 45 | this.groupId = groupId; 46 | if (this.groupId != null && this.groupId.isEmpty()) { 47 | this.groupId = null; 48 | } 49 | } 50 | 51 | public static SQSMessageIdentifier fromSQSMessage(SQSMessage sqsMessage) throws JMSException { 52 | return new SQSMessageIdentifier(sqsMessage.getQueueUrl(), sqsMessage.getReceiptHandle(), sqsMessage.getSQSMessageId(), sqsMessage.getSQSMessageGroupId()); 53 | } 54 | 55 | /** 56 | * Returns the queueUrl where the message was sent or received from. 57 | * 58 | * @return queueUrl 59 | */ 60 | public String getQueueUrl() { 61 | return this.queueUrl; 62 | } 63 | 64 | /** 65 | * Returns the receipt handle returned after the delivery of the message 66 | * from SQS. 67 | * 68 | * @return receiptHandle 69 | */ 70 | public String getReceiptHandle() { 71 | return this.receiptHandle; 72 | } 73 | 74 | /** 75 | * Returns the SQS message id assigned on send. 76 | * 77 | * @return sqsMessageId 78 | */ 79 | public String getSQSMessageID() { 80 | return this.sqsMessageId; 81 | } 82 | 83 | /** 84 | * Returns the group id to which the message belongs. Non-null only for messages received from FIFO queues. 85 | * 86 | * @return groupId 87 | */ 88 | public String getGroupId() { 89 | return this.groupId; 90 | } 91 | 92 | @Override 93 | public int hashCode() { 94 | final int prime = 31; 95 | int result = 1; 96 | result = prime * result + ((queueUrl == null) ? 0 : queueUrl.hashCode()); 97 | result = prime * result + ((receiptHandle == null) ? 0 : receiptHandle.hashCode()); 98 | result = prime * result + ((sqsMessageId == null) ? 0 : sqsMessageId.hashCode()); 99 | return result; 100 | } 101 | 102 | @Override 103 | public boolean equals(Object obj) { 104 | if (this == obj) 105 | return true; 106 | if (obj == null) 107 | return false; 108 | if (getClass() != obj.getClass()) 109 | return false; 110 | SQSMessageIdentifier other = (SQSMessageIdentifier) obj; 111 | if (queueUrl == null) { 112 | if (other.queueUrl != null) 113 | return false; 114 | } else if (!queueUrl.equals(other.queueUrl)) 115 | return false; 116 | if (receiptHandle == null) { 117 | if (other.receiptHandle != null) 118 | return false; 119 | } else if (!receiptHandle.equals(other.receiptHandle)) 120 | return false; 121 | if (sqsMessageId == null) { 122 | return other.sqsMessageId == null; 123 | } else return sqsMessageId.equals(other.sqsMessageId); 124 | } 125 | 126 | @Override 127 | public String toString() { 128 | return "SQSMessageIdentifier [queueUrl=" + queueUrl + ", receiptHandle=" + receiptHandle + 129 | ", sqsMessageId=" + sqsMessageId + "]"; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/acknowledge/UnorderedAcknowledger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.acknowledge; 16 | 17 | import com.amazon.sqs.javamessaging.AmazonSQSMessagingClientWrapper; 18 | import com.amazon.sqs.javamessaging.SQSSession; 19 | import com.amazon.sqs.javamessaging.message.SQSMessage; 20 | import jakarta.jms.JMSException; 21 | import software.amazon.awssdk.services.sqs.model.DeleteMessageRequest; 22 | 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | /** 29 | * Used to acknowledge messages in any order one at a time. 30 | *

31 | * This class is not safe for concurrent use. 32 | */ 33 | public class UnorderedAcknowledger implements Acknowledger { 34 | 35 | private final AmazonSQSMessagingClientWrapper amazonSQSClient; 36 | 37 | private final SQSSession session; 38 | 39 | // key is the receipt handle of the message and value is the message 40 | // identifier 41 | private final Map unAckMessages; 42 | 43 | public UnorderedAcknowledger (AmazonSQSMessagingClientWrapper amazonSQSClient, SQSSession session) { 44 | this.amazonSQSClient = amazonSQSClient; 45 | this.session = session; 46 | this.unAckMessages = new HashMap<>(); 47 | } 48 | 49 | /** 50 | * Acknowledges the consumed message via calling deleteMessage. 51 | */ 52 | @Override 53 | public void acknowledge(SQSMessage message) throws JMSException { 54 | session.checkClosed(); 55 | amazonSQSClient.deleteMessage(DeleteMessageRequest.builder() 56 | .queueUrl(message.getQueueUrl()) 57 | .receiptHandle(message.getReceiptHandle()) 58 | .build()); 59 | unAckMessages.remove(message.getReceiptHandle()); 60 | } 61 | 62 | /** 63 | * Updates the internal data structure for the consumed but not acknowledged 64 | * message. 65 | */ 66 | @Override 67 | public void notifyMessageReceived(SQSMessage message) throws JMSException { 68 | SQSMessageIdentifier messageIdentifier = SQSMessageIdentifier.fromSQSMessage(message); 69 | unAckMessages.put(message.getReceiptHandle(), messageIdentifier); 70 | } 71 | 72 | /** 73 | * Returns the list of all consumed but not acknowledged messages. 74 | */ 75 | @Override 76 | public List getUnAckMessages() { 77 | return new ArrayList<>(unAckMessages.values()); 78 | } 79 | 80 | /** 81 | * Clears the list of not acknowledged messages. 82 | */ 83 | @Override 84 | public void forgetUnAckMessages() { 85 | unAckMessages.clear(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/message/SQSObjectMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.message; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.Acknowledger; 18 | import jakarta.jms.JMSException; 19 | import jakarta.jms.ObjectMessage; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import software.amazon.awssdk.services.sqs.model.Message; 23 | import software.amazon.awssdk.utils.BinaryUtils; 24 | 25 | import java.io.ByteArrayInputStream; 26 | import java.io.ByteArrayOutputStream; 27 | import java.io.IOException; 28 | import java.io.ObjectInputStream; 29 | import java.io.ObjectOutputStream; 30 | import java.io.Serializable; 31 | 32 | /** 33 | * An ObjectMessage object is used to send a message that contains a Java 34 | * serializable object. 35 | *

36 | * It inherits from the Message interface and adds a body containing a single 37 | * reference to an object. Only Serializable Java objects can be used. 38 | *

39 | * When a client receives an ObjectMessage, it is in read-only mode. If a client 40 | * attempts to write to the message at this point, a 41 | * MessageNotWriteableException is thrown. If clearBody is called, the message 42 | * can now be both read from and written to. 43 | */ 44 | public class SQSObjectMessage extends SQSMessage implements ObjectMessage { 45 | private static final Logger LOG = LoggerFactory.getLogger(SQSObjectMessage.class); 46 | 47 | /** 48 | * Serialized message body 49 | */ 50 | private String body; 51 | 52 | /** 53 | * Convert received SQSMessage into ObjectMessage 54 | */ 55 | public SQSObjectMessage(Acknowledger acknowledger, String queueUrl, Message sqsMessage) throws JMSException { 56 | super(acknowledger, queueUrl, sqsMessage); 57 | body = sqsMessage.body(); 58 | } 59 | 60 | /** 61 | * Create new empty ObjectMessage to send. 62 | */ 63 | public SQSObjectMessage() throws JMSException { 64 | super(); 65 | } 66 | 67 | /** 68 | * Create new ObjectMessage with payload to send. 69 | */ 70 | public SQSObjectMessage(Serializable payload) throws JMSException { 71 | super(); 72 | body = serialize(payload); 73 | } 74 | 75 | /** 76 | * Sets the Serializable containing this message's body 77 | * 78 | * @param payload 79 | * The Serializable containing the message's body 80 | * @throws jakarta.jms.MessageNotWriteableException 81 | * If the message is in read-only mode. 82 | * @throws jakarta.jms.MessageFormatException 83 | * If object serialization fails. 84 | */ 85 | @Override 86 | public void setObject(Serializable payload) throws JMSException { 87 | checkBodyWritePermissions(); 88 | body = serialize(payload); 89 | } 90 | 91 | /** 92 | * Gets the Serializable containing this message's body 93 | * 94 | * @throws jakarta.jms.MessageFormatException 95 | * If object deserialization fails. 96 | */ 97 | @Override 98 | public Serializable getObject() throws JMSException { 99 | return deserialize(body); 100 | } 101 | 102 | /** 103 | * Sets the message body to write mode, and sets the object body to null 104 | */ 105 | @Override 106 | public void clearBody() throws JMSException { 107 | body = null; 108 | setBodyWritePermissions(true); 109 | } 110 | 111 | /** 112 | * Deserialize the String into Serializable 113 | * object. 114 | */ 115 | protected static Serializable deserialize(String serialized) throws JMSException { 116 | if (serialized == null) { 117 | return null; 118 | } 119 | Serializable deserializedObject; 120 | ObjectInputStream objectInputStream = null; 121 | try { 122 | byte[] bytes = BinaryUtils.fromBase64(serialized); 123 | objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes)); 124 | deserializedObject = (Serializable) objectInputStream.readObject(); 125 | } catch (IOException e) { 126 | LOG.error("IOException: Message cannot be written", e); 127 | throw convertExceptionToMessageFormatException(e); 128 | } catch (Exception e) { 129 | LOG.error("Unexpected exception: ", e); 130 | throw convertExceptionToMessageFormatException(e); 131 | } finally { 132 | if (objectInputStream != null) { 133 | try { 134 | objectInputStream.close(); 135 | } catch (IOException e) { 136 | LOG.warn(e.getMessage()); 137 | } 138 | } 139 | } 140 | return deserializedObject; 141 | } 142 | 143 | /** 144 | * Serialize the Serializable object to String. 145 | */ 146 | protected static String serialize(Serializable serializable) throws JMSException { 147 | if (serializable == null) { 148 | return null; 149 | } 150 | String serializedString; 151 | ObjectOutputStream objectOutputStream = null; 152 | try { 153 | ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); 154 | objectOutputStream = new ObjectOutputStream(bytesOut); 155 | objectOutputStream.writeObject(serializable); 156 | objectOutputStream.flush(); 157 | serializedString = BinaryUtils.toBase64(bytesOut.toByteArray()); 158 | } catch (IOException e) { 159 | LOG.error("IOException: cannot serialize objectMessage", e); 160 | throw convertExceptionToMessageFormatException(e); 161 | } finally { 162 | if (objectOutputStream != null) { 163 | try { 164 | objectOutputStream.close(); 165 | } catch (IOException e) { 166 | LOG.warn(e.getMessage()); 167 | } 168 | } 169 | } 170 | return serializedString; 171 | } 172 | 173 | public String getMessageBody() { 174 | return body; 175 | } 176 | 177 | void setMessageBody(String body) { 178 | this.body = body; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/message/SQSTextMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.message; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.Acknowledger; 18 | import jakarta.jms.JMSException; 19 | import jakarta.jms.TextMessage; 20 | import software.amazon.awssdk.services.sqs.model.Message; 21 | 22 | /** 23 | * A TextMessage object is used to send a message body containing a 24 | * java.lang.String. It inherits from the Message interface and adds a text 25 | * message body. SQS does not accept empty or null message body 26 | *

27 | * When a client receives a TextMessage, it is in read-only mode. If a client 28 | * attempts to write to the message at this point, a 29 | * MessageNotWriteableException is thrown. If clearBody is called, the message 30 | * can now be both read from and written to. 31 | */ 32 | public class SQSTextMessage extends SQSMessage implements TextMessage { 33 | 34 | /** 35 | * Text of the message. Assume this is safe from SQS invalid characters. 36 | */ 37 | private String text; 38 | 39 | /** 40 | * Convert received SQSMessage into TextMessage. 41 | */ 42 | public SQSTextMessage(Acknowledger acknowledger, String queueUrl, Message sqsMessage) throws JMSException{ 43 | super(acknowledger, queueUrl, sqsMessage); 44 | this.text = sqsMessage.body(); 45 | } 46 | 47 | /** 48 | * Create new empty TextMessage to send. 49 | */ 50 | public SQSTextMessage() throws JMSException { 51 | super(); 52 | } 53 | 54 | /** 55 | * Create new TextMessage with payload to send. 56 | */ 57 | public SQSTextMessage(String payload) throws JMSException { 58 | super(); 59 | this.text = payload; 60 | } 61 | 62 | /** 63 | * Sets the text containing this message's body. 64 | * 65 | * @param string 66 | * The String containing the message's body 67 | * @throws jakarta.jms.MessageNotWriteableException 68 | * If the message is in read-only mode. 69 | */ 70 | @Override 71 | public void setText(String string) throws JMSException { 72 | checkBodyWritePermissions(); 73 | this.text = string; 74 | } 75 | 76 | /** 77 | * Gets the text containing this message's body. 78 | * 79 | * @return The String containing the message's body 80 | */ 81 | @Override 82 | public String getText() throws JMSException { 83 | return text; 84 | } 85 | 86 | /** 87 | * Sets the message body to write mode, and sets the text to null 88 | */ 89 | @Override 90 | public void clearBody() throws JMSException { 91 | text = null; 92 | setBodyWritePermissions(true); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/util/ExponentialBackoffStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.util; 16 | 17 | /** 18 | * Simple exponential back-off strategy, that is used for re-tries on SQS 19 | * interactions. 20 | */ 21 | public class ExponentialBackoffStrategy { 22 | 23 | private final long delayInterval; 24 | private final long initialDelay; 25 | private final long maxDelay; 26 | 27 | public ExponentialBackoffStrategy(long delayInterval, long initialDelay, long maxDelay) { 28 | this.delayInterval = delayInterval; 29 | this.initialDelay = initialDelay; 30 | this.maxDelay = maxDelay; 31 | } 32 | 33 | /** 34 | * Returns the delay before the next attempt. 35 | * 36 | * @param retriesAttempted 37 | * @return The delay before the next attempt. 38 | */ 39 | public long delayBeforeNextRetry(int retriesAttempted) { 40 | if (retriesAttempted < 1) { 41 | return initialDelay; 42 | } 43 | 44 | 45 | if (retriesAttempted > 63) { 46 | return maxDelay; 47 | } 48 | 49 | long multiplier = ((long)1 << (retriesAttempted - 1)); 50 | if (multiplier > Long.MAX_VALUE / delayInterval) { 51 | return maxDelay; 52 | } 53 | 54 | long delay = multiplier * delayInterval; 55 | delay = Math.min(delay, maxDelay); 56 | return delay; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/util/SQSMessagingClientThreadFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.util; 16 | 17 | import java.util.concurrent.ThreadFactory; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | 20 | /** Simple thread factory that supports ThreadGroups */ 21 | 22 | public class SQSMessagingClientThreadFactory implements ThreadFactory { 23 | 24 | private final String threadBaseName; 25 | 26 | private final AtomicInteger threadCounter; 27 | 28 | private final boolean isDaemon; 29 | 30 | private ThreadGroup threadGroup; 31 | 32 | public SQSMessagingClientThreadFactory(String taskName, boolean isDaemon) { 33 | this(taskName, isDaemon, false); 34 | } 35 | 36 | public SQSMessagingClientThreadFactory(String taskName, boolean isDaemon, boolean createWithThreadGroup) { 37 | this.threadBaseName = taskName + "Thread-"; 38 | this.threadCounter = new AtomicInteger(0); 39 | this.isDaemon = isDaemon; 40 | if (createWithThreadGroup) { 41 | threadGroup = new ThreadGroup(taskName + "ThreadGroup"); 42 | threadGroup.setDaemon(isDaemon); 43 | } 44 | } 45 | 46 | public SQSMessagingClientThreadFactory(String taskName, ThreadGroup threadGroup) { 47 | this.threadBaseName = taskName + "Thread-"; 48 | this.threadCounter = new AtomicInteger(0); 49 | this.isDaemon = threadGroup.isDaemon(); 50 | this.threadGroup = threadGroup; 51 | } 52 | 53 | /** 54 | * Constructs a new Thread. Initializes name, daemon status, and ThreadGroup 55 | * if there is any. 56 | * 57 | * @param r 58 | * A runnable to be executed by new thread instance 59 | * @return The constructed thread 60 | */ 61 | public Thread newThread(Runnable r) { 62 | Thread t; 63 | if (threadGroup == null) { 64 | t = new Thread(r, threadBaseName + threadCounter.incrementAndGet()); 65 | t.setDaemon(isDaemon); 66 | } else { 67 | t = new Thread(threadGroup, r, threadBaseName + threadCounter.incrementAndGet()); 68 | t.setDaemon(isDaemon); 69 | } 70 | return t; 71 | } 72 | 73 | /** 74 | * Checks if the thread is member of the thread group 75 | * 76 | * @param thread 77 | * @return True If there is a thread group and the given thread is member of 78 | * the group 79 | */ 80 | public boolean wasThreadCreatedWithThisThreadGroup(Thread thread) { 81 | if (threadGroup == null) { 82 | return false; 83 | } 84 | return thread.getThreadGroup() == threadGroup; 85 | } 86 | 87 | } 88 | 89 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/sqs/javamessaging/util/SQSMessagingClientUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.util; 16 | 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | 20 | /** 21 | * Includes utility classes to use when serializing property names to SQS 22 | * message attribute names. SQS message attribute names accept limited set of 23 | * characters, so if the property names include any characters that is not 24 | * accepted, they can be serialized through these basic utility classes. 25 | */ 26 | public final class SQSMessagingClientUtil { 27 | 28 | private static final char HYPHEN = '-'; 29 | 30 | private static final char UNDERSCORE = '_'; 31 | 32 | private static final char DOT = '.'; 33 | 34 | private static final Pattern ILLEGAL_ATTRIBUTE_NAME_PATTERN = Pattern.compile(UNDERSCORE + "([0-9]+)" + 35 | UNDERSCORE); 36 | 37 | /** 38 | * Keeping alphabet, numeric characters, hyphens, underscores, or dots. 39 | *

40 | * Changes everything to underscores Unicode number underscores, e.g., 41 | * (*attr* -> _42_attr_42_). 42 | * 43 | * @param name 44 | * The name of the property to serialize. 45 | * @return The serialized name 46 | */ 47 | public static String serializePropertyName(String name) { 48 | 49 | StringBuilder stringBuilder = new StringBuilder(); 50 | for (char ch : name.toCharArray()) { 51 | if (Character.isLetterOrDigit(ch) || HYPHEN == ch || DOT == ch) { 52 | stringBuilder.append(ch); 53 | } else { 54 | stringBuilder.append(UNDERSCORE).append(Integer.toString(ch)).append(UNDERSCORE); 55 | } 56 | } 57 | return stringBuilder.toString(); 58 | } 59 | 60 | /** 61 | * Changes everything from underscores Unicode number underscores back to 62 | * original character, e.g., (_42_attr_42_ -> *attr*). 63 | * 64 | * @param name 65 | * The name of the property to deserialize. 66 | * @return The deserialized name 67 | */ 68 | public static String deserializePropertyName(String name) { 69 | String result = name; 70 | Matcher m = ILLEGAL_ATTRIBUTE_NAME_PATTERN.matcher(result); 71 | 72 | while (m.find()) { 73 | int charValue = Integer.parseInt(m.group(1)); 74 | result = result.replace("_" + charValue + "_", Character.toString((char) charValue)); 75 | } 76 | return result; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/AcknowledgerCommon.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.Acknowledger; 18 | import com.amazon.sqs.javamessaging.message.SQSMessage; 19 | import com.amazon.sqs.javamessaging.message.SQSTextMessage; 20 | import jakarta.jms.JMSException; 21 | import software.amazon.awssdk.services.sqs.model.Message; 22 | import software.amazon.awssdk.services.sqs.model.MessageSystemAttributeName; 23 | 24 | import java.util.ArrayList; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | import static org.junit.jupiter.api.Assertions.assertEquals; 30 | 31 | /** 32 | * Parent class for the Acknowledger tests 33 | */ 34 | public class AcknowledgerCommon { 35 | 36 | protected String baseQueueUrl = "queueUrl"; 37 | protected Acknowledger acknowledger; 38 | protected AmazonSQSMessagingClientWrapper amazonSQSClient; 39 | protected List populatedMessages = new ArrayList<>(); 40 | 41 | /* 42 | * Generate and populate the list with sqs message from different queues 43 | */ 44 | public void populateMessage(int populateMessageSize) throws JMSException { 45 | String queueUrl = baseQueueUrl + 0; 46 | for (int i = 0; i < populateMessageSize; i++) { 47 | // Change queueUrl depending on how many messages there are. 48 | if (i == 11) { 49 | queueUrl = baseQueueUrl + 1; 50 | } else if (i == 22) { 51 | queueUrl = baseQueueUrl + 2; 52 | } else if (i == 33) { 53 | queueUrl = baseQueueUrl + 3; 54 | } else if (i == 44) { 55 | queueUrl = baseQueueUrl + 4; 56 | } 57 | // Add mock Attributes 58 | Map mockAttributes = new HashMap<>(); 59 | mockAttributes.put(MessageSystemAttributeName.fromValue(SQSMessagingClientConstants.APPROXIMATE_RECEIVE_COUNT), "2"); 60 | 61 | Message sqsMessage = Message.builder() 62 | .receiptHandle("ReceiptHandle" + i) 63 | .messageId("MessageId" + i) 64 | .attributes(mockAttributes) 65 | .build(); 66 | 67 | SQSMessage message = new SQSTextMessage(acknowledger, queueUrl, sqsMessage); 68 | 69 | populatedMessages.add(message); 70 | acknowledger.notifyMessageReceived(message); 71 | } 72 | assertEquals(populateMessageSize, acknowledger.getUnAckMessages().size()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/AutoAcknowledgerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.AcknowledgeMode; 18 | import com.amazon.sqs.javamessaging.acknowledge.AutoAcknowledger; 19 | import com.amazon.sqs.javamessaging.message.SQSMessage; 20 | import jakarta.jms.IllegalStateException; 21 | import org.junit.jupiter.api.BeforeEach; 22 | import org.junit.jupiter.api.Test; 23 | import org.mockito.ArgumentCaptor; 24 | import software.amazon.awssdk.services.sqs.model.DeleteMessageRequest; 25 | 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 28 | import static org.junit.jupiter.api.Assertions.assertEquals; 29 | import static org.mockito.Mockito.doThrow; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.spy; 32 | import static org.mockito.Mockito.verify; 33 | import static org.mockito.Mockito.when; 34 | 35 | /** 36 | * Test the AutoAcknowledger class 37 | */ 38 | public class AutoAcknowledgerTest { 39 | 40 | private static final String QUEUE_URL = "QueueUrl"; 41 | private static final String RECEIPT_HANDLE = "ReceiptHandle"; 42 | 43 | private AutoAcknowledger acknowledger; 44 | private AmazonSQSMessagingClientWrapper amazonSQSClient; 45 | private SQSSession session; 46 | 47 | @BeforeEach 48 | public void before() throws Exception { 49 | amazonSQSClient = mock(AmazonSQSMessagingClientWrapper.class); 50 | session = mock(SQSSession.class); 51 | acknowledger = (AutoAcknowledger) spy(AcknowledgeMode.ACK_AUTO.createAcknowledger(amazonSQSClient, session)); 52 | } 53 | 54 | /** 55 | * Test acknowledging message with auto acknowledger 56 | */ 57 | @Test 58 | public void testAcknowledge() throws Exception { 59 | /* 60 | * Set up message mock 61 | */ 62 | SQSMessage message = mock(SQSMessage.class); 63 | when(message.getQueueUrl()) 64 | .thenReturn(QUEUE_URL); 65 | when(message.getReceiptHandle()) 66 | .thenReturn(RECEIPT_HANDLE); 67 | 68 | /* 69 | * Use the acknowledger to ack the message 70 | */ 71 | acknowledger.acknowledge(message); 72 | 73 | /* 74 | * Verify results 75 | */ 76 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DeleteMessageRequest.class); 77 | verify(amazonSQSClient).deleteMessage(argumentCaptor.capture()); 78 | assertEquals(1, argumentCaptor.getAllValues().size()); 79 | 80 | DeleteMessageRequest input = argumentCaptor.getAllValues().get(0); 81 | assertEquals(QUEUE_URL, input.queueUrl()); 82 | assertEquals(RECEIPT_HANDLE, input.receiptHandle()); 83 | } 84 | 85 | /** 86 | * Test attempt to acknowledge when the session is already closed 87 | */ 88 | @Test 89 | public void testAcknowledgeWhenSessionClosed() throws Exception { 90 | /* 91 | * Set up mocks 92 | */ 93 | doThrow(new IllegalStateException("ise")) 94 | .when(session).checkClosed(); 95 | 96 | SQSMessage message = mock(SQSMessage.class); 97 | when(message.getQueueUrl()) 98 | .thenReturn(QUEUE_URL); 99 | when(message.getReceiptHandle()) 100 | .thenReturn(RECEIPT_HANDLE); 101 | 102 | /* 103 | * Use the acknowledger to ack the message 104 | */ 105 | assertThatThrownBy(() -> acknowledger.acknowledge(message)) 106 | .isInstanceOf(IllegalStateException.class); 107 | } 108 | 109 | /** 110 | * Test notify message received 111 | */ 112 | @Test 113 | public void testNotifyMessageReceived() throws Exception { 114 | SQSMessage message = mock(SQSMessage.class); 115 | acknowledger.notifyMessageReceived(message); 116 | verify(acknowledger).acknowledge(message); 117 | } 118 | 119 | /** 120 | * Test get UnAckMessages 121 | */ 122 | @Test 123 | public void testGetUnAckMessages() { 124 | assertThat(acknowledger.getUnAckMessages()).isEmpty(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/BulkSQSOperationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.BulkSQSOperation; 18 | import com.amazon.sqs.javamessaging.acknowledge.SQSMessageIdentifier; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.mockito.Mockito.spy; 28 | import static org.mockito.Mockito.verify; 29 | 30 | /** 31 | * Test the BulkSQSOperation class 32 | */ 33 | public class BulkSQSOperationTest { 34 | 35 | private static final String QUEUE_URL = "queueUrl"; 36 | private static final String MESSAGE_ID_PREFIX = "sqsMessageId"; 37 | private static final String RECEIPT_HANDLE_PREFIX = "receiptHandle"; 38 | 39 | private BulkSQSOperation bulkAction; 40 | 41 | @BeforeEach 42 | public void before() { 43 | bulkAction = spy(new BulkSQSOperation() { 44 | @Override 45 | public void action(String queueUrl, List receiptHandles) { 46 | } 47 | }); 48 | } 49 | 50 | /** 51 | * Test illegal index value cases 52 | */ 53 | @Test 54 | public void testBulkActionIllegalIndexValue() { 55 | List messageIdentifierList = List.of( 56 | new SQSMessageIdentifier(QUEUE_URL, "receiptHandle1", "sqsMessageId1"), 57 | new SQSMessageIdentifier(QUEUE_URL, "receiptHandle2", "sqsMessageId2"), 58 | new SQSMessageIdentifier(QUEUE_URL, "receiptHandle3", "sqsMessageId3")); 59 | 60 | int negativeSize = -10; 61 | assertThatThrownBy(() -> bulkAction.bulkAction(messageIdentifierList, negativeSize)) 62 | .isInstanceOf(AssertionError.class); 63 | 64 | assertThatThrownBy(() -> bulkAction.bulkAction(messageIdentifierList, 0)) 65 | .isInstanceOf(AssertionError.class); 66 | 67 | assertThatThrownBy(() -> bulkAction.bulkAction(messageIdentifierList, messageIdentifierList.size() + 1)) 68 | .isInstanceOf(AssertionError.class); 69 | } 70 | 71 | /** 72 | * Test message flushed if number of message is below max batch size 73 | */ 74 | @Test 75 | public void testBulkActionBelowBatchSize() throws Exception { 76 | List messageIdentifierList = new ArrayList<>(); 77 | 78 | int numMessagesFromQueue = (SQSMessagingClientConstants.MAX_BATCH - 2) / 2; 79 | 80 | // Create message from the first queue 81 | int i = 0; 82 | List receiptHandles1 = new ArrayList<>(); 83 | for (; i < numMessagesFromQueue; ++i) { 84 | messageIdentifierList.add(new SQSMessageIdentifier(QUEUE_URL + 1, 85 | RECEIPT_HANDLE_PREFIX + i, MESSAGE_ID_PREFIX + i)); 86 | receiptHandles1.add(RECEIPT_HANDLE_PREFIX + i); 87 | } 88 | 89 | // Create message from the second queue 90 | List receiptHandles2 = new ArrayList<>(); 91 | for (; i < numMessagesFromQueue * 2; ++i) { 92 | messageIdentifierList.add(new SQSMessageIdentifier(QUEUE_URL + 2, 93 | RECEIPT_HANDLE_PREFIX + i, MESSAGE_ID_PREFIX + i)); 94 | receiptHandles2.add(RECEIPT_HANDLE_PREFIX + i); 95 | } 96 | 97 | bulkAction.bulkAction(messageIdentifierList, messageIdentifierList.size()); 98 | 99 | verify(bulkAction).action(QUEUE_URL + 1, receiptHandles1); 100 | verify(bulkAction).action(QUEUE_URL + 2, receiptHandles2); 101 | } 102 | 103 | 104 | /** 105 | * Test message are send if number of message from a single queue is above max batch size 106 | */ 107 | @Test 108 | public void testBulkActionAboveBatchSize() throws Exception { 109 | List messageIdentifierList = new ArrayList<>(); 110 | 111 | int numMessagesFromQueue = SQSMessagingClientConstants.MAX_BATCH * 2 + 3; 112 | 113 | // Create messages from the first batch 114 | int i = 0; 115 | List firstBatchReceiptHandles = new ArrayList<>(); 116 | for (; i < SQSMessagingClientConstants.MAX_BATCH; ++i) { 117 | messageIdentifierList.add(new SQSMessageIdentifier(QUEUE_URL + 1, 118 | RECEIPT_HANDLE_PREFIX + i, MESSAGE_ID_PREFIX + i)); 119 | firstBatchReceiptHandles.add(RECEIPT_HANDLE_PREFIX + i); 120 | } 121 | 122 | // Create messages from the second batch 123 | List secondBatchReceiptHandles = new ArrayList<>(); 124 | for (; i < SQSMessagingClientConstants.MAX_BATCH * 2; ++i) { 125 | messageIdentifierList.add(new SQSMessageIdentifier(QUEUE_URL + 1, 126 | RECEIPT_HANDLE_PREFIX + i, MESSAGE_ID_PREFIX + i)); 127 | secondBatchReceiptHandles.add(RECEIPT_HANDLE_PREFIX + i); 128 | } 129 | 130 | // Create messages from the third batch 131 | List thirdBatchReceiptHandles = new ArrayList<>(); 132 | for (; i < numMessagesFromQueue; ++i) { 133 | messageIdentifierList.add(new SQSMessageIdentifier(QUEUE_URL + 1, 134 | RECEIPT_HANDLE_PREFIX + i, MESSAGE_ID_PREFIX + i)); 135 | thirdBatchReceiptHandles.add(RECEIPT_HANDLE_PREFIX + i); 136 | } 137 | 138 | // Create messages from a different queue 139 | List receiptHandles2 = new ArrayList<>(); 140 | for (i = 0; i < SQSMessagingClientConstants.MAX_BATCH / 2; ++i) { 141 | messageIdentifierList.add(new SQSMessageIdentifier(QUEUE_URL + 2, 142 | RECEIPT_HANDLE_PREFIX + i, MESSAGE_ID_PREFIX + i)); 143 | receiptHandles2.add(RECEIPT_HANDLE_PREFIX + i); 144 | } 145 | 146 | final List> receiptHandlesList = new ArrayList<>(); 147 | final List queueUrlList = new ArrayList<>(); 148 | bulkAction = new BulkSQSOperation() { 149 | @Override 150 | public void action(String queueUrl, List receiptHandles) { 151 | receiptHandlesList.add(new ArrayList<>(receiptHandles)); 152 | queueUrlList.add(queueUrl); 153 | } 154 | }; 155 | 156 | bulkAction.bulkAction(messageIdentifierList, messageIdentifierList.size()); 157 | 158 | assertEquals(firstBatchReceiptHandles, receiptHandlesList.get(0)); 159 | assertEquals(QUEUE_URL + 1, queueUrlList.get(0)); 160 | 161 | assertEquals(secondBatchReceiptHandles, receiptHandlesList.get(1)); 162 | assertEquals(QUEUE_URL + 1, queueUrlList.get(1)); 163 | 164 | assertEquals(thirdBatchReceiptHandles, receiptHandlesList.get(2)); 165 | assertEquals(QUEUE_URL + 1, queueUrlList.get(2)); 166 | 167 | assertEquals(receiptHandles2, receiptHandlesList.get(3)); 168 | assertEquals(QUEUE_URL + 2, queueUrlList.get(3)); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/ExponentialBackoffStrategyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.util.ExponentialBackoffStrategy; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | /** 23 | * Test the ExponentialBackoffStrategy class 24 | */ 25 | public class ExponentialBackoffStrategyTest { 26 | 27 | private static final int DELAY_INTERVAL = 10; 28 | private static final int INITIAL_DELAY = 20; 29 | private static final int MAX_DELAY = 300; 30 | 31 | /** 32 | * test delay with illegal value of retry attempts 33 | */ 34 | @Test 35 | public void testDelayBeforeNextRetryNonPositiveRetryAttempt() { 36 | ExponentialBackoffStrategy backoff = new ExponentialBackoffStrategy(DELAY_INTERVAL, INITIAL_DELAY, MAX_DELAY); 37 | 38 | assertEquals(INITIAL_DELAY, backoff.delayBeforeNextRetry(0)); 39 | assertEquals(INITIAL_DELAY, backoff.delayBeforeNextRetry(-10)); 40 | } 41 | 42 | /** 43 | * test first delay result in delay initial value 44 | */ 45 | @Test 46 | public void testDelayBeforeNextRetryFirstAttempt() { 47 | ExponentialBackoffStrategy backoff = new ExponentialBackoffStrategy(DELAY_INTERVAL, INITIAL_DELAY, MAX_DELAY); 48 | 49 | assertEquals(DELAY_INTERVAL, backoff.delayBeforeNextRetry(1)); 50 | } 51 | 52 | /** 53 | * test delay from 2 to 1000 54 | */ 55 | @Test 56 | public void testDelayBeforeNextRetry() { 57 | ExponentialBackoffStrategy backoff = new ExponentialBackoffStrategy(DELAY_INTERVAL, INITIAL_DELAY, MAX_DELAY); 58 | 59 | assertEquals(20, backoff.delayBeforeNextRetry(2)); 60 | assertEquals(40, backoff.delayBeforeNextRetry(3)); 61 | assertEquals(80, backoff.delayBeforeNextRetry(4)); 62 | assertEquals(160, backoff.delayBeforeNextRetry(5)); 63 | for (int i = 6; i < 1000; i++) { 64 | assertEquals(MAX_DELAY, backoff.delayBeforeNextRetry(i)); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/MessageListenerConcurrentOperationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.AcknowledgeMode; 18 | import com.amazon.sqs.javamessaging.acknowledge.Acknowledger; 19 | import com.amazon.sqs.javamessaging.acknowledge.NegativeAcknowledger; 20 | import com.amazon.sqs.javamessaging.message.SQSMessage; 21 | import jakarta.jms.IllegalStateException; 22 | import jakarta.jms.JMSException; 23 | import jakarta.jms.Message; 24 | import jakarta.jms.MessageListener; 25 | import jakarta.jms.Session; 26 | import org.junit.jupiter.api.BeforeEach; 27 | import org.junit.jupiter.api.Test; 28 | 29 | import java.util.Collections; 30 | import java.util.concurrent.BrokenBarrierException; 31 | import java.util.concurrent.CountDownLatch; 32 | import java.util.concurrent.CyclicBarrier; 33 | 34 | import static org.junit.jupiter.api.Assertions.assertFalse; 35 | import static org.junit.jupiter.api.Assertions.assertTrue; 36 | import static org.mockito.Mockito.mock; 37 | 38 | /** 39 | * Test concurrent operation of message listener on session and connections 40 | */ 41 | public class MessageListenerConcurrentOperationTest { 42 | 43 | private static final String QUEUE_URL = "queueUrl"; 44 | private static final String QUEUE_NAME = "queueName"; 45 | private static final int NUMBER_OF_MESSAGES_TO_PREFETCH = 10; 46 | 47 | private AmazonSQSMessagingClientWrapper amazonSQSClient; 48 | private SQSMessageConsumerPrefetch.MessageManager msgManager; 49 | private volatile SQSSession session; 50 | private volatile SQSConnection connection; 51 | 52 | /** 53 | * Message listener that creates a producer on the session 54 | */ 55 | private final MessageListener msgListenerCreatesProducer = new MessageListener() { 56 | @Override 57 | public void onMessage(Message message) { 58 | try { 59 | session.createProducer(new SQSQueueDestination(QUEUE_NAME, QUEUE_URL)); 60 | } catch (JMSException e) { 61 | e.printStackTrace(); 62 | } 63 | } 64 | }; 65 | 66 | /** 67 | * Message listener that creates a consumer on the session 68 | */ 69 | private final MessageListener msgListenerCreatesConsumer = new MessageListener() { 70 | @Override 71 | public void onMessage(Message message) { 72 | try { 73 | session.createProducer(new SQSQueueDestination(QUEUE_NAME, QUEUE_URL)); 74 | } catch (JMSException e) { 75 | e.printStackTrace(); 76 | } 77 | } 78 | }; 79 | 80 | @BeforeEach 81 | public void Setup() throws JMSException { 82 | Acknowledger acknowledger = mock(Acknowledger.class); 83 | NegativeAcknowledger negativeAcknowledger = mock(NegativeAcknowledger.class); 84 | SQSQueueDestination sqsDestination = new SQSQueueDestination(QUEUE_NAME, QUEUE_URL); 85 | amazonSQSClient = mock(AmazonSQSMessagingClientWrapper.class); 86 | 87 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 88 | session = new SQSSession(connection, AcknowledgeMode.ACK_AUTO); 89 | SQSSessionCallbackScheduler sqsSessionRunnable = new SQSSessionCallbackScheduler(session, 90 | AcknowledgeMode.ACK_AUTO, acknowledger, negativeAcknowledger); 91 | 92 | SQSMessageConsumer consumer = mock(SQSMessageConsumer.class); 93 | 94 | SQSMessageConsumerPrefetch preFetcher = new SQSMessageConsumerPrefetch(sqsSessionRunnable, acknowledger, 95 | negativeAcknowledger, sqsDestination, amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 96 | preFetcher.setMessageConsumer(consumer); 97 | 98 | msgManager = new SQSMessageConsumerPrefetch.MessageManager(preFetcher, mock(SQSMessage.class)); 99 | } 100 | 101 | /** 102 | * Test concurrent operation on message listener creating producer and consumer when 103 | * session is concurrently closed 104 | */ 105 | @Test 106 | public void testConcurrentSessionCloseOperation() throws JMSException, InterruptedException { 107 | 108 | ConcurrentOperation closeSessionOperation = new ConcurrentOperation() { 109 | @Override 110 | public void setup() throws IllegalStateException { 111 | session.start(); 112 | } 113 | 114 | @Override 115 | public void applyOperation() throws JMSException { 116 | session.close(); 117 | } 118 | 119 | @Override 120 | public void verify() { 121 | assertTrue(session.isClosed()); 122 | assertFalse(session.isRunning()); 123 | } 124 | }; 125 | 126 | // Test session close operation with create producer operation 127 | for (int i = 0; i < 10; ++i) { 128 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 129 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 130 | 131 | testConcurrentExecution(msgListenerCreatesProducer, closeSessionOperation); 132 | connection.close(); 133 | } 134 | 135 | // Test session close operation with create consumer operation 136 | for (int i = 0; i < 10; ++i) { 137 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 138 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 139 | 140 | testConcurrentExecution(msgListenerCreatesConsumer, closeSessionOperation); 141 | connection.close(); 142 | } 143 | } 144 | 145 | /** 146 | * Test concurrent operation on message listener creating producer and consumer when 147 | * connection is concurrently started 148 | */ 149 | @Test 150 | public void testConcurrentConnectionStartOperation() throws JMSException, InterruptedException { 151 | 152 | ConcurrentOperation startConnectionOperation = new ConcurrentOperation() { 153 | @Override 154 | public void setup() throws JMSException { 155 | session.start(); 156 | } 157 | 158 | @Override 159 | public void applyOperation() throws JMSException { 160 | connection.start(); 161 | } 162 | 163 | @Override 164 | public void verify() { 165 | assertFalse(connection.isClosed()); 166 | assertTrue(connection.isRunning()); 167 | } 168 | }; 169 | 170 | // Test connection start operation with create producer operation 171 | for (int i = 0; i < 10; ++i) { 172 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 173 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 174 | 175 | testConcurrentExecution(msgListenerCreatesProducer, startConnectionOperation); 176 | connection.close(); 177 | } 178 | 179 | // Test connection start operation with create consumer operation 180 | for (int i = 0; i < 10; ++i) { 181 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 182 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 183 | 184 | testConcurrentExecution(msgListenerCreatesConsumer, startConnectionOperation); 185 | connection.close(); 186 | } 187 | } 188 | 189 | /** 190 | * Test concurrent operation on message listener creating producer and consumer when 191 | * connection is concurrently closed 192 | */ 193 | @Test 194 | public void testConcurrentConnectionCloseOperation() throws JMSException, InterruptedException { 195 | ConcurrentOperation closeConnectionOperation = new ConcurrentOperation() { 196 | @Override 197 | public void setup() throws JMSException { 198 | connection.start(); 199 | } 200 | 201 | @Override 202 | public void applyOperation() throws JMSException { 203 | connection.close(); 204 | } 205 | 206 | @Override 207 | public void verify() { 208 | assertTrue(connection.isClosed()); 209 | } 210 | }; 211 | 212 | // Test connection close operation with create producer operation 213 | for (int i = 0; i < 10; ++i) { 214 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 215 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 216 | 217 | testConcurrentExecution(msgListenerCreatesProducer, closeConnectionOperation); 218 | } 219 | 220 | // Test connection close operation with create consumer operation 221 | for (int i = 0; i < 10; ++i) { 222 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 223 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 224 | 225 | testConcurrentExecution(msgListenerCreatesConsumer, closeConnectionOperation); 226 | } 227 | } 228 | 229 | /** 230 | * Test concurrent operation on message listener creating producer and consumer when 231 | * connection is concurrently stopped 232 | */ 233 | @Test 234 | public void testConcurrentConnectionStopOperation() throws JMSException, InterruptedException { 235 | ConcurrentOperation stopConnectionOperation = new ConcurrentOperation() { 236 | @Override 237 | public void setup() throws JMSException { 238 | session.start(); 239 | connection.start(); 240 | } 241 | 242 | @Override 243 | public void applyOperation() throws JMSException { 244 | connection.stop(); 245 | } 246 | 247 | @Override 248 | public void verify() { 249 | assertFalse(connection.isClosed()); 250 | assertFalse(connection.isRunning()); 251 | } 252 | }; 253 | 254 | 255 | // Test connection stop operation with create producer operation 256 | for (int i = 0; i < 10; ++i) { 257 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 258 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 259 | 260 | testConcurrentExecution(msgListenerCreatesProducer, stopConnectionOperation); 261 | connection.close(); 262 | } 263 | 264 | // Test connection stop operation with create consumer operation 265 | for (int i = 0; i < 10; ++i) { 266 | connection = new SQSConnection(amazonSQSClient, NUMBER_OF_MESSAGES_TO_PREFETCH); 267 | session = (SQSSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 268 | 269 | testConcurrentExecution(msgListenerCreatesConsumer, stopConnectionOperation); 270 | connection.close(); 271 | } 272 | } 273 | 274 | public void testConcurrentExecution(final MessageListener msgListener, final ConcurrentOperation operation) 275 | throws JMSException, InterruptedException { 276 | // Start the session 277 | operation.setup(); 278 | 279 | final CyclicBarrier startBarrier = new CyclicBarrier(2); 280 | final CountDownLatch finishLatch = new CountDownLatch(1); 281 | 282 | Thread t1 = new Thread(() -> { 283 | try { 284 | startBarrier.await(); 285 | operation.applyOperation(); 286 | } catch (JMSException | InterruptedException | BrokenBarrierException e) { 287 | e.printStackTrace(); 288 | } 289 | finishLatch.countDown(); 290 | }); 291 | 292 | // Start the callback scheduler 293 | Thread t2 = new Thread(() -> { 294 | try { 295 | startBarrier.await(); 296 | } catch (InterruptedException | BrokenBarrierException e) { 297 | e.printStackTrace(); 298 | } 299 | session.getSqsSessionRunnable().scheduleCallBacks(msgListener, Collections.singletonList(msgManager)); 300 | }); 301 | 302 | t1.start(); 303 | t2.start(); 304 | 305 | finishLatch.await(); 306 | 307 | operation.verify(); 308 | } 309 | 310 | /** 311 | * Interface for the tested concurrent operation 312 | */ 313 | private interface ConcurrentOperation { 314 | 315 | void setup() throws JMSException; 316 | 317 | void applyOperation() throws JMSException; 318 | 319 | void verify(); 320 | 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/ModifyWaitTimeSecondsTest.java: -------------------------------------------------------------------------------- 1 | package com.amazon.sqs.javamessaging; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class ModifyWaitTimeSecondsTest { 11 | 12 | @DisplayName("Should be able to modify SQSMessageConsumerPrefetch.WAIT_TIME_SECONDS via Reflection") 13 | @Test 14 | public void waitTimeSecondsShouldBeModifiableViaReflection() throws NoSuchFieldException, IllegalAccessException { 15 | Field wait_time_seconds = SQSMessageConsumerPrefetch.class.getDeclaredField("WAIT_TIME_SECONDS"); 16 | wait_time_seconds.setAccessible(true); 17 | wait_time_seconds.setInt(null,5); 18 | assertEquals(5,SQSMessageConsumerPrefetch.WAIT_TIME_SECONDS); 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/NegativeAcknowledgerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.NegativeAcknowledger; 18 | import com.amazon.sqs.javamessaging.message.SQSMessage; 19 | import jakarta.jms.JMSException; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | import org.mockito.ArgumentCaptor; 23 | import software.amazon.awssdk.services.sqs.model.ChangeMessageVisibilityBatchRequest; 24 | import software.amazon.awssdk.services.sqs.model.ChangeMessageVisibilityBatchRequestEntry; 25 | 26 | import java.util.ArrayDeque; 27 | import java.util.Collections; 28 | import java.util.List; 29 | 30 | import static org.junit.jupiter.api.Assertions.assertEquals; 31 | import static org.junit.jupiter.api.Assertions.assertTrue; 32 | import static org.mockito.Mockito.any; 33 | import static org.mockito.Mockito.anyList; 34 | import static org.mockito.Mockito.eq; 35 | import static org.mockito.Mockito.mock; 36 | import static org.mockito.Mockito.never; 37 | import static org.mockito.Mockito.spy; 38 | import static org.mockito.Mockito.times; 39 | import static org.mockito.Mockito.verify; 40 | import static org.mockito.Mockito.when; 41 | 42 | /** 43 | * Test the NegativeAcknowledger class 44 | */ 45 | public class NegativeAcknowledgerTest extends AcknowledgerCommon { 46 | 47 | private static final String QUEUE_URL = "queueUrl"; 48 | 49 | private NegativeAcknowledger negativeAcknowledger; 50 | 51 | @BeforeEach 52 | public void setupRanded() { 53 | amazonSQSClient = mock(AmazonSQSMessagingClientWrapper.class); 54 | negativeAcknowledger = spy(new NegativeAcknowledger(amazonSQSClient)); 55 | } 56 | 57 | /** 58 | * Test NegativeAcknowledger bulk action 59 | */ 60 | @Test 61 | public void testNackBulkAction() throws JMSException { 62 | /* 63 | * Set up the message queue 64 | */ 65 | List receiptHandles = List.of("r0", "r1", "r2"); 66 | 67 | ArrayDeque messageQueue = addSQSMessageToQueue(3); 68 | 69 | /* 70 | * Nack the messages in bulk actions 71 | */ 72 | negativeAcknowledger.bulkAction(messageQueue, QUEUE_URL); 73 | 74 | /* 75 | * Verify results 76 | */ 77 | verify(negativeAcknowledger).action(QUEUE_URL, receiptHandles); 78 | } 79 | 80 | /** 81 | * Test NegativeAcknowledger bulk action with a large batch 82 | */ 83 | @Test 84 | public void testNackBulkActionLargeBatch() throws JMSException { 85 | /* 86 | * Set up the message queue 87 | */ 88 | ArrayDeque messageQueue = addSQSMessageToQueue(13); 89 | 90 | /* 91 | * Nack the messages in bulk actions 92 | */ 93 | negativeAcknowledger.bulkAction(messageQueue, QUEUE_URL); 94 | 95 | /* 96 | * Verify results 97 | */ 98 | verify(negativeAcknowledger, times(2)).action(eq(QUEUE_URL), anyList()); 99 | } 100 | 101 | /** 102 | * Test NegativeAcknowledger action 103 | */ 104 | @Test 105 | public void testAction() throws JMSException { 106 | List receiptHandles = List.of("r0", "r1", "r2"); 107 | 108 | negativeAcknowledger.action(QUEUE_URL, receiptHandles); 109 | 110 | ArgumentCaptor argumentCaptor = 111 | ArgumentCaptor.forClass(ChangeMessageVisibilityBatchRequest.class); 112 | verify(amazonSQSClient).changeMessageVisibilityBatch(argumentCaptor.capture()); 113 | 114 | assertEquals(1, argumentCaptor.getAllValues().size()); 115 | 116 | assertEquals(QUEUE_URL, argumentCaptor.getAllValues().get(0).queueUrl()); 117 | List captureList = argumentCaptor.getAllValues().get(0).entries(); 118 | assertEquals(receiptHandles.size(), captureList.size()); 119 | 120 | for (ChangeMessageVisibilityBatchRequestEntry item : captureList) { 121 | assertTrue(receiptHandles.contains(item.receiptHandle())); 122 | } 123 | } 124 | 125 | /** 126 | * Test NegativeAcknowledger action withe empty receipt handles 127 | */ 128 | @Test 129 | public void testActionEmptyReceiptHandles() throws JMSException { 130 | negativeAcknowledger.action(QUEUE_URL, null); 131 | 132 | negativeAcknowledger.action(QUEUE_URL, Collections.emptyList()); 133 | 134 | verify(amazonSQSClient, never()).changeMessageVisibilityBatch(any(ChangeMessageVisibilityBatchRequest.class)); 135 | } 136 | 137 | /** 138 | * Utility function 139 | */ 140 | private ArrayDeque addSQSMessageToQueue(int count) { 141 | ArrayDeque messageQueue = new ArrayDeque<>(); 142 | 143 | PrefetchManager prefetchManager = mock(PrefetchManager.class); 144 | for (int i = 0; i < count; ++i) { 145 | SQSMessage msg = mock(SQSMessage.class); 146 | when(msg.getReceiptHandle()).thenReturn("r" + i); 147 | messageQueue.add(new SQSMessageConsumerPrefetch.MessageManager(prefetchManager, msg)); 148 | } 149 | 150 | return messageQueue; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/RangedAcknowledgerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.AcknowledgeMode; 18 | import com.amazon.sqs.javamessaging.acknowledge.SQSMessageIdentifier; 19 | import com.amazon.sqs.javamessaging.message.SQSMessage; 20 | import jakarta.jms.JMSException; 21 | import org.junit.jupiter.api.BeforeEach; 22 | import org.junit.jupiter.api.Test; 23 | import org.mockito.ArgumentCaptor; 24 | import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchRequest; 25 | 26 | import java.util.ArrayList; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | import static org.junit.jupiter.api.Assertions.assertEquals; 32 | import static org.junit.jupiter.api.Assertions.assertFalse; 33 | import static org.junit.jupiter.api.Assertions.assertNotNull; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | import static org.mockito.Mockito.mock; 36 | import static org.mockito.Mockito.times; 37 | import static org.mockito.Mockito.verify; 38 | 39 | 40 | /** 41 | * Test the RangedAcknowledger class 42 | */ 43 | public class RangedAcknowledgerTest extends AcknowledgerCommon { 44 | 45 | @BeforeEach 46 | public void setupRanded() throws JMSException { 47 | amazonSQSClient = mock(AmazonSQSMessagingClientWrapper.class); 48 | acknowledger = AcknowledgeMode.ACK_RANGE.createAcknowledger(amazonSQSClient, mock(SQSSession.class)); 49 | } 50 | 51 | /** 52 | * Test forget un-acknowledge messages 53 | */ 54 | @Test 55 | public void testForget() throws JMSException { 56 | int populateMessageSize = 33; 57 | populateMessage(populateMessageSize); 58 | 59 | acknowledger.forgetUnAckMessages(); 60 | assertEquals(0, acknowledger.getUnAckMessages().size()); 61 | } 62 | 63 | /** 64 | * Test acknowledge all un-acknowledge messages 65 | */ 66 | @Test 67 | public void testFullAck() throws JMSException { 68 | int populateMessageSize = 34; 69 | populateMessage(populateMessageSize); 70 | int ackMessage = 33; 71 | 72 | testAcknowledge(populateMessageSize, ackMessage); 73 | 74 | assertEquals(0, acknowledger.getUnAckMessages().size()); 75 | } 76 | 77 | /** 78 | * Test acknowledge 25 first un-acknowledge messages 79 | */ 80 | @Test 81 | public void testOneAck() throws JMSException { 82 | int populateMessageSize = 34; 83 | populateMessage(populateMessageSize); 84 | int ackMessage = 25; 85 | 86 | testAcknowledge(populateMessageSize, ackMessage); 87 | 88 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DeleteMessageBatchRequest.class); 89 | verify(amazonSQSClient, times(5)).deleteMessageBatch(argumentCaptor.capture()); 90 | 91 | //key is the queue url 92 | //value is the sequence of sizes of expected batches 93 | Map> expectedCalls = new HashMap<>(); 94 | List queue0Calls = new ArrayList<>(); 95 | queue0Calls.add(10); 96 | queue0Calls.add(1); 97 | expectedCalls.put(baseQueueUrl + 0, queue0Calls); 98 | List queue1Calls = new ArrayList<>(); 99 | queue1Calls.add(10); 100 | queue1Calls.add(1); 101 | expectedCalls.put(baseQueueUrl + 1, queue1Calls); 102 | List queue2Calls = new ArrayList<>(); 103 | queue2Calls.add(4); 104 | expectedCalls.put(baseQueueUrl + 2, queue2Calls); 105 | 106 | for (DeleteMessageBatchRequest request : argumentCaptor.getAllValues()) { 107 | String queueUrl = request.queueUrl(); 108 | List expectedSequence = expectedCalls.get(queueUrl); 109 | assertNotNull(expectedSequence); 110 | assertTrue(expectedSequence.size() > 0); 111 | assertEquals(expectedSequence.get(0).intValue(), request.entries().size()); 112 | expectedSequence.remove(0); 113 | if (expectedSequence.isEmpty()) { 114 | expectedCalls.remove(queueUrl); 115 | } 116 | } 117 | 118 | assertTrue(expectedCalls.isEmpty()); 119 | } 120 | 121 | /** 122 | * Test two acknowledge calls 123 | */ 124 | @Test 125 | public void testTwoAck() throws JMSException { 126 | int populateMessageSize = 44; 127 | populateMessage(populateMessageSize); 128 | int firstAckMessage = 10; 129 | 130 | testAcknowledge(populateMessageSize, firstAckMessage); 131 | 132 | int secondAckMessage = 20; 133 | testAcknowledge(populateMessageSize, secondAckMessage); 134 | } 135 | 136 | /** 137 | * Utility function to acknowledge the first indexOfMessageToAck un-acknowledge messages 138 | * 139 | * @param populateMessageSize current un-acknowledge messages size 140 | * @param indexOfMessageToAck index of message to acknowledge 141 | */ 142 | public void testAcknowledge(int populateMessageSize, int indexOfMessageToAck) throws JMSException { 143 | SQSMessage messageToAck = populatedMessages.get(indexOfMessageToAck); 144 | messageToAck.acknowledge(); 145 | 146 | for (int i = 0; i < indexOfMessageToAck; i++) { 147 | SQSMessage message = populatedMessages.get(i); 148 | SQSMessageIdentifier sqsMessageIdentifier = new SQSMessageIdentifier( 149 | message.getQueueUrl(), message.getReceiptHandle(), message.getSQSMessageId()); 150 | assertFalse(acknowledger.getUnAckMessages().contains(sqsMessageIdentifier)); 151 | } 152 | 153 | assertEquals((populateMessageSize - indexOfMessageToAck - 1), acknowledger.getUnAckMessages().size()); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/SQSConnectionFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import jakarta.jms.JMSException; 18 | import org.junit.jupiter.api.Test; 19 | import software.amazon.awssdk.regions.Region; 20 | import software.amazon.awssdk.services.sqs.SqsClient; 21 | import software.amazon.awssdk.services.sqs.SqsClientBuilder; 22 | 23 | import static org.junit.jupiter.api.Assertions.assertNotSame; 24 | import static org.junit.jupiter.api.Assertions.assertSame; 25 | import static org.mockito.Mockito.mock; 26 | 27 | 28 | public class SQSConnectionFactoryTest { 29 | 30 | @Test 31 | public void canCreateFactoryWithNoSetting() throws JMSException{ 32 | System.setProperty("aws.region", "eu-central-1"); 33 | SQSConnectionFactory factory= new SQSConnectionFactory(); 34 | SQSConnection connection = factory.createConnection(); 35 | connection.close(); 36 | } 37 | 38 | @Test 39 | public void canCreateFactoryWithDefaultProviderSettings() { 40 | System.setProperty("aws.region", "eu-central-1"); 41 | new SQSConnectionFactory(new ProviderConfiguration()); 42 | //cannot actually attempt to create a connection because the default client builder depends on environment settings or instance configuration to be present 43 | //which we cannot guarantee on the builder fleet 44 | } 45 | 46 | @Test 47 | public void canCreateFactoryWithCustomClient() throws JMSException { 48 | SqsClient client = mock(SqsClient.class); 49 | SQSConnectionFactory factory = new SQSConnectionFactory(new ProviderConfiguration(), client); 50 | SQSConnection connection = factory.createConnection(); 51 | connection.close(); 52 | } 53 | 54 | @Test 55 | public void factoryWithCustomClientWillUseTheSameClient() throws JMSException { 56 | SqsClient client = mock(SqsClient.class); 57 | SQSConnectionFactory factory = new SQSConnectionFactory(new ProviderConfiguration(), client); 58 | SQSConnection connection1 = factory.createConnection(); 59 | SQSConnection connection2 = factory.createConnection(); 60 | 61 | assertSame(client, connection1.getAmazonSQSClient()); 62 | assertSame(client, connection2.getAmazonSQSClient()); 63 | assertSame(connection1.getAmazonSQSClient(), connection2.getAmazonSQSClient()); 64 | 65 | connection1.close(); 66 | connection2.close(); 67 | } 68 | 69 | @Test 70 | public void canCreateFactoryWithCustomBuilder() throws JMSException { 71 | SqsClientBuilder clientBuilder = SqsClient.builder().region(Region.US_EAST_1); 72 | SQSConnectionFactory factory = new SQSConnectionFactory(new ProviderConfiguration(), clientBuilder); 73 | SQSConnection connection = factory.createConnection(); 74 | connection.close(); 75 | } 76 | 77 | @Test 78 | public void factoryWithCustomBuilderWillCreateNewClient() throws JMSException { 79 | SqsClientBuilder clientBuilder = SqsClient.builder().region(Region.US_EAST_1); 80 | SQSConnectionFactory factory = new SQSConnectionFactory(new ProviderConfiguration(), clientBuilder); 81 | SQSConnection connection1 = factory.createConnection(); 82 | SQSConnection connection2 = factory.createConnection(); 83 | 84 | assertNotSame(connection1.getAmazonSQSClient(), connection2.getAmazonSQSClient()); 85 | 86 | connection1.close(); 87 | connection2.close(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/SQSMessageConsumerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.acknowledge.AcknowledgeMode; 18 | import com.amazon.sqs.javamessaging.acknowledge.Acknowledger; 19 | import com.amazon.sqs.javamessaging.acknowledge.NegativeAcknowledger; 20 | import com.amazon.sqs.javamessaging.util.SQSMessagingClientThreadFactory; 21 | import jakarta.jms.JMSException; 22 | import jakarta.jms.MessageListener; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import java.util.concurrent.CountDownLatch; 27 | import java.util.concurrent.Executors; 28 | import java.util.concurrent.ScheduledExecutorService; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 32 | import static org.junit.jupiter.api.Assertions.assertEquals; 33 | import static org.junit.jupiter.api.Assertions.assertFalse; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | import static org.junit.jupiter.api.Assertions.fail; 36 | import static org.mockito.Mockito.any; 37 | import static org.mockito.Mockito.mock; 38 | import static org.mockito.Mockito.never; 39 | import static org.mockito.Mockito.spy; 40 | import static org.mockito.Mockito.verify; 41 | import static org.mockito.Mockito.verifyNoMoreInteractions; 42 | import static org.mockito.Mockito.when; 43 | 44 | /** 45 | * Test the SQSMessageConsumerPrefetchTest class 46 | */ 47 | public class SQSMessageConsumerTest { 48 | 49 | private static final String QUEUE_URL_1 = "QueueUrl1"; 50 | private static final String QUEUE_NAME = "QueueName"; 51 | 52 | private SQSMessageConsumer consumer; 53 | private SQSConnection sqsConnection; 54 | private SQSSession sqsSession; 55 | private SQSSessionCallbackScheduler sqsSessionRunnable; 56 | private Acknowledger acknowledger; 57 | 58 | private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5); 59 | private SQSMessageConsumerPrefetch sqsMessageConsumerPrefetch; 60 | private NegativeAcknowledger negativeAcknowledger; 61 | private SQSMessagingClientThreadFactory threadFactory; 62 | private SQSQueueDestination destination; 63 | 64 | @BeforeEach 65 | public void setup() throws JMSException { 66 | sqsConnection = mock(SQSConnection.class); 67 | 68 | sqsSession = spy(new SQSSession(sqsConnection, AcknowledgeMode.ACK_AUTO));//mock(SQSSession.class); 69 | sqsSessionRunnable = mock(SQSSessionCallbackScheduler.class); 70 | 71 | acknowledger = mock(Acknowledger.class); 72 | 73 | negativeAcknowledger = mock(NegativeAcknowledger.class); 74 | 75 | threadFactory = new SQSMessagingClientThreadFactory("testTask", true); 76 | 77 | destination = new SQSQueueDestination(QUEUE_NAME, QUEUE_URL_1); 78 | 79 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 80 | destination, acknowledger, negativeAcknowledger, threadFactory)); 81 | 82 | 83 | sqsMessageConsumerPrefetch = mock(SQSMessageConsumerPrefetch.class); 84 | } 85 | 86 | /** 87 | * Test the message selector is not supported 88 | */ 89 | @Test 90 | public void testGetMessageSelectorNotSupported() { 91 | assertThatThrownBy(() -> consumer.getMessageSelector()) 92 | .isInstanceOf(JMSException.class) 93 | .hasMessage("Unsupported Method"); 94 | } 95 | 96 | /** 97 | * Test stop is a no op if already closed 98 | */ 99 | @Test 100 | public void testStopNoOpIfAlreadyClosed() throws JMSException { 101 | /* 102 | * Set up consumer 103 | */ 104 | consumer.close(); 105 | 106 | /* 107 | * stop consumer 108 | */ 109 | consumer.stopPrefetch(); 110 | 111 | /* 112 | * Verify results 113 | */ 114 | verifyNoMoreInteractions(sqsMessageConsumerPrefetch); 115 | } 116 | 117 | /** 118 | * Test close blocks on in progress callback 119 | */ 120 | @Test 121 | public void testCloseBlocksInProgressCallback() throws InterruptedException, JMSException { 122 | /* 123 | * Set up the latches 124 | */ 125 | final CountDownLatch beforeConsumerStopCall = new CountDownLatch(1); 126 | final CountDownLatch passedConsumerStopCall = new CountDownLatch(1); 127 | 128 | consumer = new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 129 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch); 130 | 131 | sqsSession.start(); 132 | sqsSession.startingCallback(consumer); 133 | 134 | // Run another thread that tries to close the consumer while activeConsumerInCallback is set 135 | executorService.execute(() -> { 136 | beforeConsumerStopCall.countDown(); 137 | try { 138 | consumer.close(); 139 | } catch (JMSException e) { 140 | fail(); 141 | } 142 | passedConsumerStopCall.countDown(); 143 | }); 144 | 145 | beforeConsumerStopCall.await(); 146 | Thread.sleep(10); 147 | // Ensure that we wait on activeConsumerInCallback 148 | assertFalse(passedConsumerStopCall.await(2, TimeUnit.SECONDS)); 149 | 150 | // Release the activeConsumerInCallback 151 | sqsSession.finishedCallback(); 152 | 153 | // Ensure that the consumer close completed 154 | passedConsumerStopCall.await(); 155 | 156 | assertTrue(consumer.closed); 157 | } 158 | 159 | 160 | /** 161 | * Test Start is a no op if already closed 162 | */ 163 | @Test 164 | public void testStartNoOpIfAlreadyClosed() throws JMSException { 165 | /* 166 | * Set up consumer 167 | */ 168 | consumer.close(); 169 | 170 | /* 171 | * start consumer 172 | */ 173 | consumer.startPrefetch(); 174 | 175 | /* 176 | * Verify results 177 | */ 178 | verifyNoMoreInteractions(sqsMessageConsumerPrefetch); 179 | } 180 | 181 | /** 182 | * Test do close results in no op when the consumer is already closed 183 | */ 184 | @Test 185 | public void testDoCloseNoOpWhenAlreadyClosed() { 186 | /* 187 | * Set up consumer 188 | */ 189 | consumer.closed = true; 190 | 191 | /* 192 | * Do close consumer 193 | */ 194 | consumer.doClose(); 195 | 196 | /* 197 | * Verify results 198 | */ 199 | verifyNoMoreInteractions(sqsSession); 200 | } 201 | 202 | 203 | /** 204 | * Test do close 205 | */ 206 | @Test 207 | public void testDoClose() { 208 | consumer = new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 209 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch); 210 | 211 | /* 212 | * Do close consumer 213 | */ 214 | consumer.doClose(); 215 | 216 | /* 217 | * Verify results 218 | */ 219 | verify(sqsSession).removeConsumer(consumer); 220 | verify(sqsMessageConsumerPrefetch).close(); 221 | } 222 | 223 | 224 | /** 225 | * Test close results in no op when the consumer is already closed 226 | */ 227 | @Test 228 | public void testCloseNoOpWhenAlreadyClosed() throws JMSException { 229 | /* 230 | * Set up consumer 231 | */ 232 | consumer.closed = true; 233 | 234 | /* 235 | * Close consumer 236 | */ 237 | consumer.close(); 238 | 239 | /* 240 | * Verify results 241 | */ 242 | 243 | verify(consumer, never()).doClose(); 244 | verify(sqsSessionRunnable, never()).setConsumerCloseAfterCallback(any(SQSMessageConsumer.class)); 245 | } 246 | 247 | /** 248 | * Test when consumer is closed by the message listener that is running on the callback thread 249 | * we do not close but set a consumer close after callback 250 | */ 251 | @Test 252 | public void testCloseCalledFromCallbackExecutionThread() throws JMSException { 253 | /* 254 | * Set up consumer 255 | */ 256 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 257 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 258 | 259 | when(sqsSession.isActiveCallbackSessionThread()) 260 | .thenReturn(true); 261 | 262 | /* 263 | * Close consumer 264 | */ 265 | consumer.close(); 266 | 267 | /* 268 | * Verify results 269 | */ 270 | verify(consumer, never()).doClose(); 271 | verify(sqsSessionRunnable).setConsumerCloseAfterCallback(consumer); 272 | } 273 | 274 | /** 275 | * Test consumer close 276 | */ 277 | @Test 278 | public void testClose() throws JMSException { 279 | /* 280 | * Set up consumer 281 | */ 282 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 283 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 284 | 285 | /* 286 | * Close consumer 287 | */ 288 | consumer.close(); 289 | 290 | /* 291 | * Verify results 292 | */ 293 | verify(consumer).doClose(); 294 | verify(sqsSessionRunnable, never()).setConsumerCloseAfterCallback(consumer); 295 | } 296 | 297 | /** 298 | * Test set message listener fails when consumer is already closed 299 | */ 300 | @Test 301 | public void testSetMessageListenerAlreadyClosed() throws JMSException { 302 | /* 303 | * Set up consumer 304 | */ 305 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 306 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 307 | 308 | consumer.close(); 309 | 310 | MessageListener msgListener = mock(MessageListener.class); 311 | 312 | /* 313 | * Set message listener on a consumer 314 | */ 315 | assertThatThrownBy(() -> consumer.setMessageListener(msgListener)) 316 | .isInstanceOf(JMSException.class) 317 | .hasMessage("Consumer is closed"); 318 | } 319 | 320 | /** 321 | * Test receive fails when consumer is already closed 322 | */ 323 | @Test 324 | public void testReceiveAlreadyClosed() throws JMSException { 325 | /* 326 | * Set up consumer 327 | */ 328 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 329 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 330 | 331 | consumer.close(); 332 | 333 | /* 334 | * Call receive 335 | */ 336 | assertThatThrownBy(() -> consumer.receive()) 337 | .isInstanceOf(JMSException.class) 338 | .hasMessage("Consumer is closed"); 339 | 340 | } 341 | 342 | /** 343 | * Test set message listener fails when consumer is already closed 344 | */ 345 | @Test 346 | public void testReceiveWithTimeoutAlreadyClosed() throws JMSException { 347 | /* 348 | * Set up consumer 349 | */ 350 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 351 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 352 | consumer.close(); 353 | 354 | long timeout = 10; 355 | 356 | /* 357 | * Call receive with timeout 358 | */ 359 | assertThatThrownBy(() -> consumer.receive(timeout)) 360 | .isInstanceOf(JMSException.class) 361 | .hasMessage("Consumer is closed"); 362 | 363 | } 364 | 365 | /** 366 | * Test set message listener fails when consumer is already closed 367 | */ 368 | @Test 369 | public void testReceiveNoWaitAlreadyClosed() throws JMSException { 370 | /* 371 | * Set up consumer 372 | */ 373 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 374 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 375 | consumer.close(); 376 | 377 | /* 378 | * Call receive no wait 379 | */ 380 | assertThatThrownBy(() -> consumer.receiveNoWait()) 381 | .isInstanceOf(JMSException.class) 382 | .hasMessage("Consumer is closed"); 383 | } 384 | 385 | /** 386 | * Test set message listener 387 | */ 388 | @Test 389 | public void testSetMessageListener() throws JMSException { 390 | /* 391 | * Set up consumer 392 | */ 393 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 394 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 395 | MessageListener msgListener = mock(MessageListener.class); 396 | 397 | /* 398 | * Set message listener on a consumer 399 | */ 400 | consumer.setMessageListener(msgListener); 401 | 402 | /* 403 | * Verify results 404 | */ 405 | verify(sqsMessageConsumerPrefetch).setMessageListener(msgListener); 406 | } 407 | 408 | /** 409 | * Test get message listener 410 | */ 411 | @Test 412 | public void testGetMessageListener() throws JMSException { 413 | /* 414 | * Set up consumer 415 | */ 416 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 417 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 418 | 419 | /* 420 | * Get message listener on a consumer 421 | */ 422 | consumer.getMessageListener(); 423 | 424 | /* 425 | * Verify results 426 | */ 427 | verify(sqsMessageConsumerPrefetch).getMessageListener(); 428 | } 429 | 430 | /** 431 | * Test get message listener 432 | */ 433 | @Test 434 | public void testGetQueue() throws JMSException { 435 | /* 436 | * Set up consumer 437 | */ 438 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 439 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 440 | 441 | assertEquals(destination, consumer.getQueue()); 442 | } 443 | 444 | /** 445 | * Test receive 446 | */ 447 | @Test 448 | public void testReceive() throws JMSException { 449 | /* 450 | * Set up consumer 451 | */ 452 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 453 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 454 | 455 | /* 456 | * Call receive 457 | */ 458 | consumer.receive(); 459 | 460 | /* 461 | * Verify results 462 | */ 463 | verify(sqsMessageConsumerPrefetch).receive(); 464 | } 465 | 466 | /** 467 | * Test receive with timeout 468 | */ 469 | @Test 470 | public void testReceiveWithTimeout() throws JMSException { 471 | /* 472 | * Set up consumer 473 | */ 474 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 475 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 476 | 477 | long timeout = 10; 478 | 479 | /* 480 | * Call receive with timeout 481 | */ 482 | consumer.receive(timeout); 483 | 484 | /* 485 | * Verify results 486 | */ 487 | verify(sqsMessageConsumerPrefetch).receive(timeout); 488 | } 489 | 490 | /** 491 | * Test receive no wait 492 | */ 493 | @Test 494 | public void testReceiveNoWait() throws JMSException { 495 | /* 496 | * Set up consumer 497 | */ 498 | consumer = spy(new SQSMessageConsumer(sqsConnection, sqsSession, sqsSessionRunnable, 499 | destination, acknowledger, negativeAcknowledger, threadFactory, sqsMessageConsumerPrefetch)); 500 | 501 | /* 502 | * Call receive no wait 503 | */ 504 | consumer.receiveNoWait(); 505 | 506 | /* 507 | * Verify results 508 | */ 509 | verify(sqsMessageConsumerPrefetch).receiveNoWait(); 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/SQSMessagingClientUtilTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging; 16 | 17 | import com.amazon.sqs.javamessaging.util.SQSMessagingClientUtil; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | /** 23 | * Test the SQSMessagingClientUtilTest class 24 | */ 25 | public class SQSMessagingClientUtilTest { 26 | 27 | String text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-.~!@#$%^&*()`][}{\\|\";'> populatedMessagesCopy = new ArrayList<>(populatedMessages); 68 | while (!populatedMessagesCopy.isEmpty()) { 69 | 70 | int rand = new Random().nextInt(populatedMessagesCopy.size()); 71 | SQSMessage message = populatedMessagesCopy.remove(rand); 72 | message.acknowledge(); 73 | assertEquals(populateMessageSize - (++counter), acknowledger.getUnAckMessages().size()); 74 | } 75 | assertEquals(0, acknowledger.getUnAckMessages().size()); 76 | 77 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DeleteMessageRequest.class); 78 | verify(amazonSQSClient, times(populateMessageSize)).deleteMessage(argumentCaptor.capture()); 79 | 80 | for (SQSMessage msg : populatedMessages) { 81 | DeleteMessageRequest deleteRequest = DeleteMessageRequest.builder() 82 | .queueUrl(msg.getQueueUrl()) 83 | .receiptHandle(msg.getReceiptHandle()).build(); 84 | assertTrue(argumentCaptor.getAllValues().contains(deleteRequest)); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/message/SQSBytesMessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.message; 16 | 17 | import com.amazon.sqs.javamessaging.SQSMessagingClientConstants; 18 | import com.amazon.sqs.javamessaging.SQSSession; 19 | import jakarta.jms.JMSException; 20 | import jakarta.jms.MessageEOFException; 21 | import jakarta.jms.MessageFormatException; 22 | import jakarta.jms.MessageNotReadableException; 23 | import jakarta.jms.MessageNotWriteableException; 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | import software.amazon.awssdk.services.sqs.model.Message; 27 | import software.amazon.awssdk.services.sqs.model.MessageSystemAttributeName; 28 | import software.amazon.awssdk.utils.BinaryUtils; 29 | 30 | import java.io.DataInputStream; 31 | import java.io.DataOutputStream; 32 | import java.io.IOException; 33 | import java.io.InputStream; 34 | import java.io.OutputStream; 35 | import java.util.Map; 36 | import java.util.Set; 37 | 38 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 39 | import static org.junit.jupiter.api.Assertions.assertEquals; 40 | import static org.junit.jupiter.api.Assertions.assertTrue; 41 | import static org.junit.jupiter.api.Assertions.fail; 42 | import static org.mockito.Mockito.any; 43 | import static org.mockito.Mockito.anyInt; 44 | import static org.mockito.Mockito.doNothing; 45 | import static org.mockito.Mockito.doThrow; 46 | import static org.mockito.Mockito.mock; 47 | import static org.mockito.Mockito.spy; 48 | import static org.mockito.Mockito.when; 49 | 50 | /** 51 | * Test the SQSBytesMessageTest class 52 | */ 53 | public class SQSBytesMessageTest { 54 | 55 | private SQSSession mockSQSSession; 56 | 57 | private final static Map MESSAGE_SYSTEM_ATTRIBUTES = Map.of( 58 | MessageSystemAttributeName.fromValue(SQSMessagingClientConstants.APPROXIMATE_RECEIVE_COUNT), "1"); 59 | 60 | @BeforeEach 61 | public void setUp() { 62 | mockSQSSession = mock(SQSSession.class); 63 | } 64 | 65 | /** 66 | * Test message ability to write and read different types 67 | */ 68 | @Test 69 | public void testReadWrite() throws JMSException { 70 | when(mockSQSSession.createBytesMessage()).thenReturn(new SQSBytesMessage()); 71 | SQSBytesMessage msg = (SQSBytesMessage) mockSQSSession.createBytesMessage(); 72 | 73 | byte[] byteArray = new byte[]{1, 0, 'a', 65}; 74 | byte byteData = 'a'; 75 | short shortVal = 123; 76 | 77 | msg.writeBytes(byteArray); 78 | msg.writeChar('b'); 79 | msg.writeBoolean(true); 80 | msg.writeInt(100); 81 | msg.writeDouble(2.1768); 82 | msg.writeFloat(3.1457f); 83 | msg.writeLong(1290772974281L); 84 | msg.writeShort(shortVal); 85 | msg.writeByte(byteData); 86 | msg.writeUTF("UTF-String"); 87 | msg.writeObject("test"); 88 | 89 | msg.reset(); 90 | 91 | int length = (int) msg.getBodyLength(); 92 | byte[] body = new byte[length]; 93 | int i = msg.readBytes(body); 94 | if (i == -1) { 95 | fail("failed to readByte"); 96 | } 97 | 98 | Message message = Message.builder() 99 | .body(BinaryUtils.toBase64(body)) 100 | .attributes(MESSAGE_SYSTEM_ATTRIBUTES) 101 | .build(); 102 | 103 | SQSBytesMessage receivedByteMsg = new SQSBytesMessage(null, "", message); 104 | byte[] byteArray1 = new byte[4]; 105 | receivedByteMsg.readBytes(byteArray1); 106 | 107 | assertEquals(1, byteArray1[0]); 108 | assertEquals(0, byteArray1[1]); 109 | assertEquals('a', byteArray1[2]); 110 | assertEquals(65, byteArray1[3]); 111 | assertEquals('b', receivedByteMsg.readChar()); 112 | assertTrue(receivedByteMsg.readBoolean()); 113 | assertEquals(100, receivedByteMsg.readInt()); 114 | assertEquals(2.1768, receivedByteMsg.readDouble(), 0); 115 | assertEquals(3.1457f, receivedByteMsg.readFloat(), 0); 116 | assertEquals(1290772974281L, receivedByteMsg.readLong()); 117 | assertEquals(shortVal, receivedByteMsg.readShort()); 118 | assertEquals(97, receivedByteMsg.readByte()); 119 | assertEquals("UTF-String", receivedByteMsg.readUTF()); 120 | assertEquals("test", receivedByteMsg.readUTF()); 121 | 122 | assertThatThrownBy(receivedByteMsg::readBoolean).isInstanceOf(MessageEOFException.class); 123 | assertThatThrownBy(receivedByteMsg::readUnsignedByte).isInstanceOf(MessageEOFException.class); 124 | 125 | byte[] arr = new byte[10]; 126 | assertEquals(-1, receivedByteMsg.readBytes(arr, 10)); 127 | 128 | assertThatThrownBy(receivedByteMsg::readByte).isInstanceOf(MessageEOFException.class); 129 | assertThatThrownBy(receivedByteMsg::readShort).isInstanceOf(MessageEOFException.class); 130 | assertThatThrownBy(receivedByteMsg::readUnsignedShort).isInstanceOf(MessageEOFException.class); 131 | assertThatThrownBy(receivedByteMsg::readInt).isInstanceOf(MessageEOFException.class); 132 | assertThatThrownBy(receivedByteMsg::readLong).isInstanceOf(MessageEOFException.class); 133 | assertThatThrownBy(receivedByteMsg::readFloat).isInstanceOf(MessageEOFException.class); 134 | assertThatThrownBy(receivedByteMsg::readDouble).isInstanceOf(MessageEOFException.class); 135 | assertThatThrownBy(receivedByteMsg::readChar).isInstanceOf(MessageEOFException.class); 136 | assertThatThrownBy(receivedByteMsg::readUTF).isInstanceOf(MessageEOFException.class); 137 | } 138 | 139 | /** 140 | * Test byte message ability to handle io exception from the read operation 141 | */ 142 | @Test 143 | public void testReadIOException() throws JMSException, IOException { 144 | /* 145 | * Set up mocks 146 | */ 147 | Throwable ioException = new IOException(); 148 | 149 | InputStream is = mock(InputStream.class); 150 | DataInputStream dis = new DataInputStream(is); 151 | 152 | when(is.read()) 153 | .thenThrow(ioException); 154 | 155 | when(is.read(any(byte[].class), anyInt(), anyInt())) 156 | .thenThrow(ioException); 157 | 158 | SQSBytesMessage msg = spy(new SQSBytesMessage()); 159 | doNothing() 160 | .when(msg).checkCanRead(); 161 | 162 | msg.setDataIn(dis); 163 | 164 | assertThatThrownBy(msg::readBoolean).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 165 | assertThatThrownBy(msg::readByte).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 166 | assertThatThrownBy(msg::readUnsignedByte).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 167 | assertThatThrownBy(msg::readShort).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 168 | assertThatThrownBy(msg::readUnsignedShort).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 169 | assertThatThrownBy(msg::readInt).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 170 | assertThatThrownBy(msg::readLong).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 171 | assertThatThrownBy(msg::readFloat).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 172 | assertThatThrownBy(msg::readDouble).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 173 | assertThatThrownBy(msg::readChar).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 174 | assertThatThrownBy(msg::readUTF).isInstanceOf(JMSException.class).cause().isEqualTo(ioException); 175 | } 176 | 177 | /** 178 | * Test byte message ability to handle io exception from the write operation 179 | */ 180 | @Test 181 | public void testWriteIOException() throws JMSException, IOException { 182 | /* 183 | * Set up mocks 184 | */ 185 | Throwable ioException = new IOException(); 186 | 187 | OutputStream os = mock(OutputStream.class); 188 | DataOutputStream dos = new DataOutputStream(os); 189 | 190 | SQSBytesMessage msg = spy(new SQSBytesMessage()); 191 | doNothing() 192 | .when(msg).checkCanRead(); 193 | 194 | msg.setDataOut(dos); 195 | 196 | doThrow(ioException) 197 | .when(os).write(anyInt()); 198 | 199 | doThrow(ioException) 200 | .when(os).write(any(byte[].class), anyInt(), anyInt()); 201 | try { 202 | msg.writeBoolean(true); 203 | fail(); 204 | } catch (JMSException exception) { 205 | assertEquals(ioException, exception.getCause()); 206 | } 207 | 208 | assertThatThrownBy(() -> msg.writeByte((byte) 1)) 209 | .isInstanceOf(JMSException.class) 210 | .cause().isEqualTo(ioException); 211 | 212 | assertThatThrownBy(() -> msg.writeShort((short) 1)) 213 | .isInstanceOf(JMSException.class) 214 | .cause().isEqualTo(ioException); 215 | 216 | assertThatThrownBy(() -> msg.writeInt(1)) 217 | .isInstanceOf(JMSException.class) 218 | .cause().isEqualTo(ioException); 219 | 220 | assertThatThrownBy(() -> msg.writeLong(1290772974281L)) 221 | .isInstanceOf(JMSException.class) 222 | .cause().isEqualTo(ioException); 223 | 224 | assertThatThrownBy(() -> msg.writeFloat(3.1457f)) 225 | .isInstanceOf(JMSException.class) 226 | .cause().isEqualTo(ioException); 227 | 228 | assertThatThrownBy(() -> msg.writeDouble(2.1768)) 229 | .isInstanceOf(JMSException.class) 230 | .cause().isEqualTo(ioException); 231 | 232 | assertThatThrownBy(() -> msg.writeChar('a')) 233 | .isInstanceOf(JMSException.class) 234 | .cause().isEqualTo(ioException); 235 | 236 | assertThatThrownBy(() -> msg.writeUTF("test")) 237 | .isInstanceOf(JMSException.class) 238 | .cause().isEqualTo(ioException); 239 | } 240 | 241 | /** 242 | * Test write object function 243 | */ 244 | @Test 245 | public void testWriteObject() throws JMSException { 246 | SQSBytesMessage msg = new SQSBytesMessage(); 247 | 248 | byte[] byteArray = new byte[]{1, 0, 'a', 65}; 249 | byte byteData = 'a'; 250 | short shortVal = 123; 251 | 252 | msg.writeObject(byteArray); 253 | msg.writeObject('b'); 254 | msg.writeObject(true); 255 | msg.writeObject(100); 256 | msg.writeObject(2.1768); 257 | msg.writeObject(3.1457f); 258 | msg.writeObject(1290772974281L); 259 | msg.writeObject(shortVal); 260 | msg.writeObject(byteData); 261 | msg.writeObject("test"); 262 | 263 | msg.reset(); 264 | 265 | int length = (int) msg.getBodyLength(); 266 | byte[] body = new byte[length]; 267 | int i = msg.readBytes(body); 268 | if (i == -1) { 269 | fail("failed to readByte"); 270 | } 271 | 272 | Message message = Message.builder() 273 | .attributes(MESSAGE_SYSTEM_ATTRIBUTES) 274 | .body(BinaryUtils.toBase64(body)) 275 | .build(); 276 | SQSBytesMessage receivedByteMsg = new SQSBytesMessage(null, "", message); 277 | byte[] byteArray1 = new byte[4]; 278 | receivedByteMsg.readBytes(byteArray1); 279 | 280 | assertEquals(1, byteArray1[0]); 281 | assertEquals(0, byteArray1[1]); 282 | assertEquals('a', byteArray1[2]); 283 | assertEquals(65, byteArray1[3]); 284 | assertEquals('b', receivedByteMsg.readChar()); 285 | assertTrue(receivedByteMsg.readBoolean()); 286 | assertEquals(100, receivedByteMsg.readInt()); 287 | assertEquals(2.1768, receivedByteMsg.readDouble(), 0); 288 | assertEquals(3.1457f, receivedByteMsg.readFloat(), 0); 289 | assertEquals(1290772974281L, receivedByteMsg.readLong()); 290 | assertEquals(shortVal, receivedByteMsg.readShort()); 291 | assertEquals(97, receivedByteMsg.readByte()); 292 | assertEquals("test", receivedByteMsg.readUTF()); 293 | 294 | /* 295 | * Check write object error cases 296 | */ 297 | assertThatThrownBy(() -> msg.writeObject(Set.of())).isInstanceOf(MessageFormatException.class); 298 | 299 | /* 300 | * Check write object error cases 301 | */ 302 | assertThatThrownBy(() -> msg.writeObject(null)).isInstanceOf(NullPointerException.class); 303 | } 304 | 305 | /** 306 | * Test clear body 307 | */ 308 | @Test 309 | public void testClearBody() throws JMSException { 310 | SQSBytesMessage msg = new SQSBytesMessage(); 311 | 312 | byte[] byteArray = new byte[]{1, 0, 'a', 65}; 313 | msg.writeBytes(byteArray); 314 | 315 | msg.clearBody(); 316 | 317 | byte[] readByteArray = new byte[4]; 318 | 319 | /* 320 | * Verify message is in write-only mode 321 | */ 322 | assertThatThrownBy(() -> msg.readBytes(readByteArray)) 323 | .isInstanceOf(MessageNotReadableException.class) 324 | .hasMessage("Message is not readable"); 325 | 326 | msg.writeBytes(byteArray); 327 | } 328 | 329 | /** 330 | * Test after reset the message is read only mode 331 | */ 332 | @Test 333 | public void testNotWriteable() throws JMSException { 334 | SQSBytesMessage msg = new SQSBytesMessage(); 335 | 336 | byte[] byteArray = new byte[]{'a', 0, 34, 65}; 337 | msg.writeBytes(byteArray); 338 | msg.reset(); 339 | assertEquals('a', msg.readByte()); 340 | 341 | assertThatThrownBy(() -> msg.writeInt(10)) 342 | .isInstanceOf(MessageNotWriteableException.class); 343 | } 344 | 345 | /** 346 | * Test before reset the message is not readable 347 | */ 348 | @Test 349 | public void testReadable() throws JMSException { 350 | when(mockSQSSession.createBytesMessage()).thenReturn(new SQSBytesMessage()); 351 | SQSBytesMessage msg = (SQSBytesMessage) mockSQSSession.createBytesMessage(); 352 | 353 | byte[] byteArray = new byte[]{'a', 0, 34, 65}; 354 | msg.writeBytes(byteArray); 355 | 356 | assertThatThrownBy(msg::readInt).isInstanceOf(MessageNotReadableException.class); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/message/SQSObjectMessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.message; 16 | 17 | import jakarta.jms.JMSException; 18 | import jakarta.jms.ObjectMessage; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.io.Serializable; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 28 | import static org.junit.jupiter.api.Assertions.assertNotNull; 29 | import static org.junit.jupiter.api.Assertions.assertNull; 30 | 31 | /** 32 | * Test the SQSObjectMessageTest class 33 | */ 34 | public class SQSObjectMessageTest { 35 | 36 | /** 37 | * Test set object 38 | */ 39 | @Test 40 | public void testSetObject() throws JMSException { 41 | Map expectedPayload = Map.of("testKey", "testValue"); 42 | 43 | ObjectMessage objectMessage = new SQSObjectMessage(); 44 | objectMessage.setObject((Serializable) expectedPayload); 45 | 46 | Map actualPayload = (Map) objectMessage.getObject(); 47 | assertEquals(expectedPayload, actualPayload); 48 | } 49 | 50 | /** 51 | * Test create message with object 52 | */ 53 | @Test 54 | public void testCreateMessageWithObject() throws JMSException { 55 | Map expectedPayload = Map.of("testKey", "testValue"); 56 | 57 | ObjectMessage objectMessage = new SQSObjectMessage((Serializable) expectedPayload); 58 | 59 | Map actualPayload = (Map) objectMessage.getObject(); 60 | assertEquals(expectedPayload, actualPayload); 61 | } 62 | 63 | /** 64 | * Test object serialization 65 | */ 66 | @Test 67 | public void testObjectSerialization() throws JMSException { 68 | Map expectedPayload = new HashMap<>(); 69 | expectedPayload.put("testKey", "testValue"); 70 | 71 | SQSObjectMessage sqsObjectMessage = new SQSObjectMessage(); 72 | 73 | String serialized = SQSObjectMessage.serialize((Serializable) expectedPayload); 74 | 75 | assertNotNull(serialized, "Serialized object should not be null."); 76 | assertNotEquals("", serialized, "Serialized object should not be empty."); 77 | 78 | Map deserialized = (Map) SQSObjectMessage.deserialize(serialized); 79 | 80 | assertNotNull(deserialized, "Deserialized object should not be null."); 81 | assertEquals(expectedPayload, deserialized, "Serialized object should be equal to original object."); 82 | 83 | sqsObjectMessage.clearBody(); 84 | 85 | assertNull(sqsObjectMessage.getMessageBody()); 86 | 87 | } 88 | 89 | /** 90 | * Test serialization and deserialization with illegal input 91 | */ 92 | @Test 93 | public void testDeserializeIllegalInput() throws JMSException { 94 | assertThatThrownBy(() -> SQSObjectMessage.deserialize("Wrong String")) 95 | .isInstanceOf(JMSException.class); 96 | 97 | assertNull(SQSObjectMessage.deserialize(null)); 98 | assertNull(SQSObjectMessage.serialize(null)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/sqs/javamessaging/message/SQSTextMessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 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 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazon.sqs.javamessaging.message; 16 | 17 | import jakarta.jms.JMSException; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | import static org.junit.jupiter.api.Assertions.assertNull; 22 | 23 | /** 24 | * Test the SQSTextMessageTest class 25 | */ 26 | public class SQSTextMessageTest { 27 | 28 | @Test 29 | public void testSetText() throws JMSException { 30 | String expectedPayload = "testing set payload"; 31 | SQSTextMessage sqsTextMessage = new SQSTextMessage(); 32 | sqsTextMessage.setText(expectedPayload); 33 | String actualPayload = sqsTextMessage.getText(); 34 | assertEquals(expectedPayload, actualPayload); 35 | } 36 | 37 | /** 38 | * Test create message with text 39 | */ 40 | @Test 41 | public void testCreateMessageWithText() throws JMSException { 42 | String expectedPayload = "testing message with payload"; 43 | SQSTextMessage sqsTextMessage = new SQSTextMessage(expectedPayload); 44 | 45 | String actualPayload = sqsTextMessage.getText(); 46 | assertEquals(expectedPayload, actualPayload); 47 | } 48 | 49 | /** 50 | * Test create message and setting text 51 | */ 52 | @Test 53 | public void testClearBody() throws JMSException { 54 | String expectedPayload = "testing set payload"; 55 | SQSTextMessage sqsTextMessage = new SQSTextMessage(); 56 | sqsTextMessage.setText(expectedPayload); 57 | assertEquals(expectedPayload, sqsTextMessage.getText()); 58 | 59 | sqsTextMessage.clearBody(); 60 | assertNull(sqsTextMessage.getText()); 61 | } 62 | } 63 | --------------------------------------------------------------------------------