├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── diagrams ├── senderclient_class.txt └── sendermanager_class.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lombok.config ├── settings.gradle ├── sonar-project.properties └── src ├── main ├── java │ └── com │ │ └── github │ │ └── mikesafonov │ │ └── smpp │ │ ├── api │ │ ├── BaseSenderManager.java │ │ ├── IndexDetectionStrategy.java │ │ ├── NoSenderClientException.java │ │ ├── RandomIndexDetectionStrategy.java │ │ ├── RoundRobinIndexDetectionStrategy.java │ │ ├── SenderManager.java │ │ └── StrategySenderManager.java │ │ ├── config │ │ ├── ConnectionMode.java │ │ ├── ConnectionType.java │ │ ├── SmppAutoConfiguration.java │ │ ├── SmppProperties.java │ │ ├── SmscConnection.java │ │ ├── SmscConnectionFactoryBean.java │ │ └── SmscConnectionsHolder.java │ │ └── core │ │ ├── ClientFactory.java │ │ ├── connection │ │ ├── BaseSenderConnectionManager.java │ │ ├── BaseSmppSessionConfiguration.java │ │ ├── ConnectionManager.java │ │ ├── ConnectionManagerFactory.java │ │ ├── ReceiverConfiguration.java │ │ ├── ReceiverConnectionManager.java │ │ ├── ResponseClientRebindTask.java │ │ ├── SessionReconnector.java │ │ ├── TransceiverConfiguration.java │ │ ├── TransceiverConnectionManager.java │ │ ├── TransmitterConfiguration.java │ │ └── TransmitterConnectionManager.java │ │ ├── dto │ │ ├── CancelMessage.java │ │ ├── CancelMessageResponse.java │ │ ├── DeliveryReport.java │ │ ├── Message.java │ │ ├── MessageErrorInformation.java │ │ ├── MessageResponse.java │ │ └── MessageType.java │ │ ├── exceptions │ │ ├── ClientNameSmppException.java │ │ ├── IllegalAddressException.java │ │ ├── ResponseClientBindException.java │ │ ├── SenderClientBindException.java │ │ ├── SmppException.java │ │ ├── SmppMessageBuildingException.java │ │ └── SmppSessionException.java │ │ ├── generators │ │ ├── AlwaysSuccessSmppResultGenerator.java │ │ └── SmppResultGenerator.java │ │ ├── reciever │ │ ├── DeliveryReportConsumer.java │ │ ├── NullDeliveryReportConsumer.java │ │ ├── ResponseClient.java │ │ ├── ResponseSmppSessionHandler.java │ │ └── StandardResponseClient.java │ │ ├── sender │ │ ├── AddressBuilder.java │ │ ├── DataCoding.java │ │ ├── DefaultTypeOfAddressParser.java │ │ ├── FlashSubmitSmEncoder.java │ │ ├── MessageBuilder.java │ │ ├── MockSenderClient.java │ │ ├── SenderClient.java │ │ ├── SilentSubmitSmEncoder.java │ │ ├── SimpleSubmitSmEncoder.java │ │ ├── StandardSenderClient.java │ │ ├── SubmitSmEncoder.java │ │ ├── SubmitSmEncoderFactory.java │ │ ├── TestSenderClient.java │ │ ├── TypeOfAddressParser.java │ │ └── UnknownTypeOfAddressParser.java │ │ └── utils │ │ ├── CountWithEncoding.java │ │ ├── JodaJavaConverter.java │ │ ├── MessageUtil.java │ │ └── Utils.java └── resources │ └── META-INF │ ├── spring-configuration-metadata.json │ └── spring.factories ├── test └── java │ └── com │ └── github │ └── mikesafonov │ └── smpp │ ├── api │ ├── RandomIndexDetectionStrategyTest.java │ ├── RoundRobinIndexDetectionStrategyTest.java │ └── StrategySenderManagerTest.java │ ├── config │ └── SmscConnectionFactoryBeanTest.java │ ├── core │ ├── ClientFactoryTest.java │ ├── connection │ │ ├── ConnectionManagerFactoryTest.java │ │ ├── ReceiverConfigurationTest.java │ │ ├── ReceiverConnectionManagerTest.java │ │ ├── ResponseClientRebindTaskTest.java │ │ ├── TransceiverConfigurationTest.java │ │ ├── TransceiverConnectionManagerTest.java │ │ ├── TransmitterConfigurationTest.java │ │ └── TransmitterConnectionManagerTest.java │ ├── dto │ │ ├── DeliveryReportTest.java │ │ └── MessageBuilderTest.java │ ├── generators │ │ └── AlwaysSuccessSmppResultGeneratorTest.java │ ├── reciever │ │ ├── ResponseClientRebindTaskTest.java │ │ ├── ResponseSmppSessionHandlerTest.java │ │ └── StandardResponseClientTest.java │ ├── sender │ │ ├── AddressBuilderTest.java │ │ ├── BaseStandardSenderClientTest.java │ │ ├── DefaultTypeOfAddressParserTest.java │ │ ├── MessageBuilderTest.java │ │ ├── MockSenderClientTest.java │ │ ├── StandardSenderClientCancelMessageTest.java │ │ ├── StandardSenderClientSendMessageTest.java │ │ ├── StandardSenderClientTest.java │ │ ├── SubmitSmEncoderFactoryTest.java │ │ ├── TestSenderClientTest.java │ │ └── UnknownTypeOfAddressParserTest.java │ └── utils │ │ ├── JodaJavaConverterTest.java │ │ └── MessageUtilTest.java │ └── util │ └── Randomizer.java └── testIntegration ├── java └── com │ └── github │ └── mikesafonov │ └── smpp │ ├── SingleClientTest.java │ ├── TestUtils.java │ ├── handler │ ├── CustomHandlerConfiguration.java │ ├── CustomHandlerTest.java │ └── SmppSessionListenerImpl.java │ ├── roundrobin │ ├── RoundRobinApplicationConfiguration.java │ ├── RoundRobinApplicationService.java │ └── RoundRobinSmppTest.java │ └── transceiver │ ├── TransceiverConfiguration.java │ └── TransceiverTest.java └── resources ├── application-handler.properties ├── application-robin.properties ├── application-transceiver.properties ├── bootstrap-handler.properties ├── bootstrap-robin.properties └── bootstrap-transceiver.properties /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: MikeSafonov -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: org.springframework.boot:spring-boot-dependencies 10 | versions: 11 | - 2.4.2 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | # Run Gradle Wrapper Validation Action to verify the wrapper's checksum 5 | gradleValidation: 6 | name: Gradle Wrapper 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | # Check out current repository 11 | - name: Fetch Sources 12 | uses: actions/checkout@v2 13 | 14 | # Validate wrapper 15 | - name: Gradle Wrapper Validation 16 | uses: gradle/wrapper-validation-action@v1.0.3 17 | 18 | # Run verifyPlugin and test Gradle tasks 19 | test: 20 | name: Test 21 | needs: gradleValidation 22 | runs-on: ubuntu-latest 23 | steps: 24 | 25 | # Setup Java 1.8 environment for the next steps 26 | - name: Setup Java 27 | uses: actions/setup-java@v1 28 | with: 29 | java-version: 1.8 30 | 31 | # Check out current repository 32 | - name: Fetch Sources 33 | uses: actions/checkout@v2 34 | 35 | # Cache Gradle dependencies 36 | - name: Setup Gradle Dependencies Cache 37 | uses: actions/cache@v2 38 | with: 39 | path: ~/.gradle/caches 40 | key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts', 'gradle.properties') }} 41 | 42 | # Cache Gradle Wrapper 43 | - name: Setup Gradle Wrapper Cache 44 | uses: actions/cache@v2 45 | with: 46 | path: ~/.gradle/wrapper 47 | key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} 48 | 49 | # Run test 50 | - name: Run test 51 | run: ./gradlew test 52 | 53 | # Run pitest 54 | - name: Run Pitest 55 | run: ./gradlew pitest 56 | 57 | # Run testIntegration 58 | - name: Run testIntegration 59 | run: ./gradlew testIntegration 60 | 61 | - name: Cache SonarCloud packages 62 | uses: actions/cache@v1 63 | with: 64 | path: ~/.sonar/cache 65 | key: ${{ runner.os }}-sonar 66 | restore-keys: ${{ runner.os }}-sonar 67 | 68 | - name: Build and analyze 69 | run: ./gradlew build jacocoTestReport sonarqube -x signArchives --stacktrace 70 | env: 71 | # Needed to get some information about the pull request, if any 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | # SonarCloud access token should be generated from https://sonarcloud.io/account/security/ 74 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 75 | 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | .idea/ 4 | out/ 5 | target/ 6 | *.iml 7 | .pitest/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 MikeSafonov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /diagrams/senderclient_class.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | interface SenderClient { 3 | String getId() 4 | void setup() 5 | MessageResponse send(Message message) 6 | CancelMessageResponse cancel(CancelMessage cancelMessage) 7 | } 8 | 9 | 10 | SenderClient <|-- StandardSenderClient 11 | SenderClient <|-- MockSenderClient 12 | SenderClient <|-- TestSenderClient 13 | @enduml -------------------------------------------------------------------------------- /diagrams/sendermanager_class.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | interface SenderManager { 3 | SenderClient getByName(String name) 4 | SenderClient getClient() 5 | } 6 | abstract BaseSenderManager{ 7 | # List smscConnections 8 | } 9 | 10 | class StrategySenderManager{ 11 | - IndexDetectionStrategy indexDetectionStrategy 12 | } 13 | 14 | interface IndexDetectionStrategy { 15 | int next(int size) 16 | } 17 | 18 | IndexDetectionStrategy *- StrategySenderManager 19 | SenderManager <|-- BaseSenderManager 20 | BaseSenderManager <|-- StrategySenderManager 21 | IndexDetectionStrategy <|-- RandomIndexDetectionStrategy 22 | IndexDetectionStrategy <|-- RoundRobinIndexDetectionStrategy 23 | @enduml -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | signing.keyId= 2 | signing.password= 3 | signing.secretKeyRingFile= 4 | sonatypeUsername= 5 | sonatypePassword= -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSafonov/spring-boot-starter-smpp/ae387842c4aa082bffc7bb9d2e70e42d733284b7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-boot-starter-smpp' 2 | 3 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.host.url=https://sonarcloud.io 2 | sonar.projectKey=MikeSafonov_spring-boot-starter-smpp 3 | sonar.projectName=spring-boot-starter-smpp 4 | sonar.projectVersion=1.4.0 5 | sonar.sources=src/main 6 | sonar.tests=src/test 7 | sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml 8 | sonar.coverage.exclusions= 9 | sonar.exclusions=**/main/resources/db/** 10 | sonar.java.binaries = build/classes 11 | sonar.java.libraries=build/libs 12 | 13 | sonar.links.homepage=https://github.com/MikeSafonov/spring-boot-starter-smpp 14 | sonar.links.ci=https://github.com/MikeSafonov/spring-boot-starter-smpp 15 | sonar.links.scm=https://github.com/MikeSafonov/spring-boot-starter-smpp 16 | sonar.links.issue=https://github.com/MikeSafonov/spring-boot-starter-smpp/issues 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/api/BaseSenderManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | import com.github.mikesafonov.smpp.config.SmscConnection; 4 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | import java.util.List; 9 | 10 | import static java.util.Objects.requireNonNull; 11 | 12 | /** 13 | * Base implementation of {@link SenderManager} 14 | * 15 | * @author Mike Safonov 16 | */ 17 | public abstract class BaseSenderManager implements SenderManager { 18 | protected final List smscConnections; 19 | 20 | protected BaseSenderManager(@NotNull List smscConnections) { 21 | this.smscConnections = requireNonNull(smscConnections); 22 | } 23 | 24 | @Override 25 | public SenderClient getByName(@NotBlank String name) { 26 | return smscConnections.stream() 27 | .filter(smscConnection -> smscConnection.getName().equals(name)) 28 | .findFirst() 29 | .map(SmscConnection::getSenderClient) 30 | .orElseThrow(NoSenderClientException::new); 31 | } 32 | 33 | protected boolean isEmpty() { 34 | return smscConnections.isEmpty(); 35 | } 36 | 37 | protected int size() { 38 | return smscConnections.size(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/api/IndexDetectionStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | 4 | /** 5 | * Should be used for next index creation in {@link StrategySenderManager} 6 | * 7 | * @author Mike Safonov 8 | */ 9 | public interface IndexDetectionStrategy { 10 | /** 11 | * Create next index based on size of connections 12 | * 13 | * @param size size of connections 14 | * @return next index 15 | */ 16 | int next(int size); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/api/NoSenderClientException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | public class NoSenderClientException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/api/RandomIndexDetectionStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | import java.util.concurrent.ThreadLocalRandom; 4 | 5 | /** 6 | * Implementation of {@link IndexDetectionStrategy} which return random index based on incoming size 7 | * 8 | * @author Mike Safonov 9 | */ 10 | public class RandomIndexDetectionStrategy implements IndexDetectionStrategy { 11 | @Override 12 | public int next(int size) { 13 | return ThreadLocalRandom.current().nextInt(0, size); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/api/RoundRobinIndexDetectionStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | /** 6 | * Round robin strategy 7 | * 8 | * @author Mike Safonov 9 | */ 10 | public class RoundRobinIndexDetectionStrategy implements IndexDetectionStrategy { 11 | private AtomicInteger nextIndexCounter = new AtomicInteger(0); 12 | 13 | /** 14 | * Increment current index and return new index by module of {@code smscConnections} size 15 | * 16 | * @return new index 17 | */ 18 | @Override 19 | public int next(int size) { 20 | for (; ; ) { 21 | int current = nextIndexCounter.get(); 22 | int next = (current + 1) % size; 23 | if (nextIndexCounter.compareAndSet(current, next)) { 24 | return next; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/api/SenderManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | 7 | /** 8 | * Holder class for sender clients. 9 | * 10 | * @author Mike Safonov 11 | */ 12 | public interface SenderManager { 13 | 14 | /** 15 | * Return sender client based on it name 16 | * 17 | * @param name name of sender client 18 | * @return sender client 19 | */ 20 | SenderClient getByName(@NotBlank String name); 21 | 22 | /** 23 | * @return next sender client 24 | */ 25 | SenderClient getClient(); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/api/StrategySenderManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | import com.github.mikesafonov.smpp.config.SmscConnection; 4 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 5 | 6 | import javax.validation.constraints.NotNull; 7 | import java.util.List; 8 | 9 | import static java.util.Objects.requireNonNull; 10 | 11 | /** 12 | * Implementation of {@link SenderManager} which return {@link SenderClient} based on {@link IndexDetectionStrategy} 13 | * implementation 14 | * 15 | * @author Mike Safonov 16 | */ 17 | public class StrategySenderManager extends BaseSenderManager { 18 | 19 | private final IndexDetectionStrategy indexDetectionStrategy; 20 | 21 | public StrategySenderManager(@NotNull List smscConnections, 22 | @NotNull IndexDetectionStrategy indexDetectionStrategy) { 23 | super(smscConnections); 24 | this.indexDetectionStrategy = requireNonNull(indexDetectionStrategy); 25 | } 26 | 27 | @Override 28 | public SenderClient getClient() { 29 | if (isEmpty()) { 30 | throw new NoSenderClientException(); 31 | } 32 | 33 | int nextIndex = indexDetectionStrategy.next(size()); 34 | return smscConnections.get(nextIndex).getSenderClient(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/config/ConnectionMode.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.config; 2 | 3 | /** 4 | * @author Mike Safonov 5 | */ 6 | public enum ConnectionMode { 7 | STANDARD, 8 | TEST, 9 | MOCK 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/config/ConnectionType.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.config; 2 | 3 | /** 4 | * Type of smpp connection manager. 5 | */ 6 | public enum ConnectionType { 7 | TRANSCEIVER, 8 | TRANSMITTER_RECEIVER 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/config/SmppAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.config; 2 | 3 | import com.github.mikesafonov.smpp.api.RoundRobinIndexDetectionStrategy; 4 | import com.github.mikesafonov.smpp.api.SenderManager; 5 | import com.github.mikesafonov.smpp.api.StrategySenderManager; 6 | import com.github.mikesafonov.smpp.core.ClientFactory; 7 | import com.github.mikesafonov.smpp.core.connection.ConnectionManagerFactory; 8 | import com.github.mikesafonov.smpp.core.generators.AlwaysSuccessSmppResultGenerator; 9 | import com.github.mikesafonov.smpp.core.generators.SmppResultGenerator; 10 | import com.github.mikesafonov.smpp.core.reciever.DeliveryReportConsumer; 11 | import com.github.mikesafonov.smpp.core.sender.DefaultTypeOfAddressParser; 12 | import com.github.mikesafonov.smpp.core.sender.TypeOfAddressParser; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 15 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 16 | import org.springframework.context.annotation.Bean; 17 | import org.springframework.context.annotation.Configuration; 18 | 19 | import java.util.List; 20 | 21 | 22 | /** 23 | * @author Mike Safonov 24 | */ 25 | @Slf4j 26 | @Configuration 27 | @EnableConfigurationProperties(SmppProperties.class) 28 | public class SmppAutoConfiguration { 29 | 30 | @Bean 31 | @ConditionalOnMissingBean(TypeOfAddressParser.class) 32 | public TypeOfAddressParser defaultTypeOfAddressParser() { 33 | return new DefaultTypeOfAddressParser(); 34 | } 35 | 36 | @Bean 37 | public ClientFactory clientFactory (){ 38 | return new ClientFactory(); 39 | } 40 | 41 | @Bean 42 | public ConnectionManagerFactory connectionManagerFactory(){return new ConnectionManagerFactory();} 43 | 44 | @Bean 45 | public SmscConnectionFactoryBean senderClientFactoryBean(SmppProperties smppProperties, 46 | SmppResultGenerator smppResultGenerator, 47 | TypeOfAddressParser typeOfAddressParser, 48 | List deliveryReportConsumers, 49 | ClientFactory clientFactory, 50 | ConnectionManagerFactory connectionManagerFactory) { 51 | return new SmscConnectionFactoryBean(smppProperties, smppResultGenerator, deliveryReportConsumers, 52 | typeOfAddressParser, clientFactory, connectionManagerFactory); 53 | } 54 | 55 | @Bean 56 | @ConditionalOnMissingBean(SmppResultGenerator.class) 57 | public SmppResultGenerator alwaysSuccessSmppResultGenerator() { 58 | return new AlwaysSuccessSmppResultGenerator(); 59 | } 60 | 61 | @Bean 62 | @ConditionalOnMissingBean(SenderManager.class) 63 | public SenderManager roundRobinSenderManager(SmscConnectionsHolder smscConnections) { 64 | return new StrategySenderManager(smscConnections.getConnections(), new RoundRobinIndexDetectionStrategy()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/config/SmppProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.config; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | 9 | import javax.validation.constraints.NotBlank; 10 | import javax.validation.constraints.NotNull; 11 | import java.time.Duration; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * @author Mike Safonov 17 | */ 18 | @Data 19 | @ConfigurationProperties(prefix = "smpp") 20 | public class SmppProperties { 21 | 22 | private Defaults defaults = new Defaults(); 23 | private Map connections = new HashMap<>(); 24 | private boolean setupRightAway = true; 25 | 26 | @Data 27 | public static class Defaults { 28 | /** 29 | * using ucs2 only 30 | */ 31 | private boolean ucs2Only = false; 32 | /** 33 | * Number of attempts to reconnect if smpp session closed 34 | */ 35 | private int maxTry = 5; 36 | /** 37 | * Mode of client 38 | */ 39 | private ConnectionMode connectionMode = ConnectionMode.STANDARD; 40 | /** 41 | * Smpp connection window size 42 | */ 43 | private int windowSize = 90; 44 | /** 45 | * Is logging smpp pdu 46 | */ 47 | private boolean loggingPdu = false; 48 | /** 49 | * Is logging smpp bytes 50 | */ 51 | private boolean loggingBytes = false; 52 | /** 53 | * Rebind period 54 | */ 55 | private Duration rebindPeriod = Duration.ofSeconds(90); 56 | 57 | /** 58 | * Request timeout 59 | */ 60 | private Duration requestTimeout = Duration.ofSeconds(5); 61 | /** 62 | * Array of phones to send. Using only if {@link #connectionMode} is {@link ConnectionMode#TEST} 63 | */ 64 | private String[] allowedPhones = new String[0]; 65 | 66 | /** 67 | * Type of smpp connection 68 | */ 69 | private ConnectionType connectionType = ConnectionType.TRANSMITTER_RECEIVER; 70 | } 71 | 72 | @Data 73 | @NoArgsConstructor 74 | @AllArgsConstructor 75 | public static class Credentials { 76 | /** 77 | * SMSC host 78 | */ 79 | @NotBlank 80 | private String host; 81 | /** 82 | * SMSC port 83 | */ 84 | private int port; 85 | /** 86 | * SMSC username 87 | */ 88 | @NotBlank 89 | private String username; 90 | /** 91 | * SMSC password 92 | */ 93 | @NotBlank 94 | private String password; 95 | } 96 | 97 | @Data 98 | public static class SMSC { 99 | /** 100 | * SMSC connection credentials 101 | */ 102 | @NotNull 103 | private Credentials credentials; 104 | /** 105 | * using ucs2 only 106 | */ 107 | private Boolean ucs2Only; 108 | /** 109 | * Number of attempts to reconnect if smpp session closed 110 | */ 111 | private Integer maxTry; 112 | /** 113 | * Mode of client 114 | */ 115 | private ConnectionMode connectionMode; 116 | /** 117 | * Smpp connection window size 118 | */ 119 | private Integer windowSize; 120 | /** 121 | * Is logging smpp pdu 122 | */ 123 | private Boolean loggingPdu; 124 | /** 125 | * Is logging smpp bytes 126 | */ 127 | private Boolean loggingBytes; 128 | /** 129 | * Rebind period 130 | */ 131 | private Duration rebindPeriod; 132 | 133 | /** 134 | * Request timeout 135 | */ 136 | private Duration requestTimeout; 137 | /** 138 | * Array of phones to send. Using only if {@link #connectionMode} is {@link ConnectionMode#TEST} 139 | */ 140 | private String[] allowedPhones; 141 | /** 142 | * Type of smpp connection 143 | */ 144 | private ConnectionType connectionType; 145 | /** 146 | * The systemType parameter is used to categorize the type of ESME that is binding to the SMSC. 147 | * Examples include “VMS” (voice mail system) and “OTA” (over-the-air activation system). 148 | * Specification of the systemType is optional - some SMSC’s may not require ESME’s to provide 149 | * this detail. In this case, the ESME can set the systevType to NULL. 150 | */ 151 | private String systemType; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/config/SmscConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.config; 2 | 3 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 4 | import com.github.mikesafonov.smpp.core.reciever.ResponseClient; 5 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Value; 8 | 9 | import java.util.Optional; 10 | 11 | /** 12 | * This class represent configured smsc connection with {@link SenderClient} and {@link ResponseClient} (optionally) 13 | * 14 | * @author Mike Safonov 15 | * @author Mikhail Epatko 16 | */ 17 | @Value 18 | @AllArgsConstructor 19 | public class SmscConnection { 20 | private final String name; 21 | private final ResponseClient responseClient; 22 | private final SenderClient senderClient; 23 | 24 | public SmscConnection(String name, SenderClient senderClient) { 25 | this.name = name; 26 | this.senderClient = senderClient; 27 | this.responseClient = null; 28 | } 29 | 30 | public Optional getResponseClient() { 31 | return Optional.ofNullable(responseClient); 32 | } 33 | 34 | public void closeConnection() { 35 | senderClient.getConnectionManager().ifPresent(ConnectionManager::destroy); 36 | if (responseClient != null) { 37 | responseClient.destroyClient(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/config/SmscConnectionsHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.config; 2 | 3 | import lombok.Value; 4 | 5 | import javax.annotation.PreDestroy; 6 | import java.util.List; 7 | 8 | /** 9 | * 10 | * @author Mike Safonov 11 | * @author Mikhail Epatko 12 | */ 13 | @Value 14 | public class SmscConnectionsHolder { 15 | private final List connections; 16 | 17 | @PreDestroy 18 | public void closeConnections() { 19 | connections.forEach(SmscConnection::closeConnection); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/ClientFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core; 2 | 3 | import com.github.mikesafonov.smpp.config.SmppProperties; 4 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 5 | import com.github.mikesafonov.smpp.core.generators.SmppResultGenerator; 6 | import com.github.mikesafonov.smpp.core.reciever.ResponseClient; 7 | import com.github.mikesafonov.smpp.core.reciever.StandardResponseClient; 8 | import com.github.mikesafonov.smpp.core.sender.*; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.NotNull; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | import static com.github.mikesafonov.smpp.core.utils.Utils.getOrDefault; 16 | import static com.github.mikesafonov.smpp.core.utils.Utils.validateName; 17 | import static java.util.Collections.emptyList; 18 | import static java.util.Objects.requireNonNull; 19 | 20 | /** 21 | * Helper class for building {@link SenderClient} and {@link ResponseClient} 22 | */ 23 | public class ClientFactory { 24 | 25 | /** 26 | * Creates {@link MockSenderClient} with name {@code name} and {@link SmppResultGenerator} 27 | * {@code smppResultGenerator} 28 | * 29 | * @param name name of client 30 | * @param smppResultGenerator result generator 31 | * @return mock sender client 32 | */ 33 | public SenderClient mockSender(@NotBlank String name, @NotNull SmppResultGenerator smppResultGenerator) { 34 | validateName(name); 35 | requireNonNull(smppResultGenerator); 36 | 37 | return new MockSenderClient(smppResultGenerator, name); 38 | } 39 | 40 | /** 41 | * Creates {@link TestSenderClient} with base client {@code senderClient}, {@link SmppResultGenerator} 42 | * {@code smppResultGenerator} and list of allowed phones from {@code smsc} or {@code defaults} 43 | * 44 | * @param senderClient base sender client 45 | * @param defaults default smpp properties 46 | * @param smsc smpp properties 47 | * @param smppResultGenerator result generator for not allowed phones 48 | * @return test sender client 49 | */ 50 | public SenderClient testSender(@NotNull SenderClient senderClient, @NotNull SmppProperties.Defaults defaults, 51 | @NotNull SmppProperties.SMSC smsc, 52 | @NotNull SmppResultGenerator smppResultGenerator) { 53 | requireNonNull(senderClient); 54 | requireNonNull(defaults); 55 | requireNonNull(smppResultGenerator); 56 | requireNonNull(smsc); 57 | 58 | String[] phones = getOrDefault(smsc.getAllowedPhones(), defaults.getAllowedPhones()); 59 | List allowedPhones = (phones == null) ? emptyList() : Arrays.asList(phones); 60 | return new TestSenderClient(senderClient, allowedPhones, smppResultGenerator); 61 | } 62 | 63 | /** 64 | * Creates {@link StandardSenderClient} with name {@code name} and {@link TypeOfAddressParser} 65 | * {@code typeOfAddressParser}, 66 | * configured with properties from {@code smsc} or {@code defaults} 67 | * 68 | * @param name name of client 69 | * @param defaults default smpp properties 70 | * @param smsc smpp properties 71 | * @param typeOfAddressParser address parser 72 | * @param connectionManager connection manager 73 | * @return standard sender client 74 | */ 75 | public SenderClient standardSender(@NotBlank String name, 76 | @NotNull SmppProperties.Defaults defaults, 77 | @NotNull SmppProperties.SMSC smsc, 78 | @NotNull TypeOfAddressParser typeOfAddressParser, 79 | @NotNull ConnectionManager connectionManager) { 80 | validateName(name); 81 | requireNonNull(defaults); 82 | requireNonNull(smsc); 83 | requireNonNull(typeOfAddressParser); 84 | requireNonNull(connectionManager); 85 | 86 | boolean ucs2Only = getOrDefault(smsc.getUcs2Only(), defaults.isUcs2Only()); 87 | long requestTimeout = getOrDefault(smsc.getRequestTimeout(), defaults.getRequestTimeout()).toMillis(); 88 | 89 | return new StandardSenderClient(connectionManager, 90 | ucs2Only, requestTimeout, new MessageBuilder(typeOfAddressParser)); 91 | } 92 | 93 | /** 94 | * Creates {@link StandardResponseClient} with name {@code name}, configured with properties from 95 | * {@code smsc} or {@code defaults} 96 | * 97 | * @param name name of client 98 | * @param connectionManager connection manager 99 | * @return standard response client 100 | */ 101 | public ResponseClient standardResponse(@NotBlank String name, @NotNull ConnectionManager connectionManager) { 102 | validateName(name); 103 | requireNonNull(connectionManager); 104 | 105 | return new StandardResponseClient(connectionManager); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/BaseSenderConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import com.cloudhopper.smpp.SmppSessionConfiguration; 5 | import com.cloudhopper.smpp.SmppSessionHandler; 6 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 7 | import com.cloudhopper.smpp.pdu.EnquireLink; 8 | import com.github.mikesafonov.smpp.core.exceptions.SmppSessionException; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | @Slf4j 13 | @RequiredArgsConstructor 14 | public abstract class BaseSenderConnectionManager implements ConnectionManager { 15 | private static final String SENDER_SUCCESS_BINDED_MESSAGE = "SENDER SUCCESSFUL BINDED"; 16 | 17 | protected final DefaultSmppClient client; 18 | protected final BaseSmppSessionConfiguration configuration; 19 | protected final SmppSessionHandler sessionHandler; 20 | /** 21 | * Number of attempts to reconnect if smpp session closed 22 | */ 23 | protected final int maxTryCount; 24 | protected SmppSession session; 25 | 26 | public BaseSenderConnectionManager(DefaultSmppClient client, BaseSmppSessionConfiguration configuration, int maxTryCount) { 27 | this.client = client; 28 | this.configuration = configuration; 29 | this.maxTryCount = maxTryCount; 30 | this.sessionHandler = null; 31 | } 32 | 33 | @Override 34 | public SmppSession getSession() { 35 | checkSession(); 36 | return session; 37 | } 38 | 39 | @Override 40 | public void closeSession() { 41 | if (session != null) { 42 | session.close(); 43 | session.destroy(); 44 | session = null; 45 | } 46 | } 47 | 48 | @Override 49 | public BaseSmppSessionConfiguration getConfiguration() { 50 | return configuration; 51 | } 52 | 53 | @Override 54 | public void destroy() { 55 | closeSession(); 56 | client.destroy(); 57 | } 58 | 59 | /** 60 | * Checking smpp session state. If session is null - creating new session using method {@link #bind()}. 61 | * Otherwise checking bound session state by method {@link #checkBoundState()}. 62 | * 63 | * @throws SmppSessionException if session not connected 64 | */ 65 | private void checkSession() { 66 | boolean connectionAlive = false; 67 | int tryCount = 0; 68 | while (!connectionAlive && tryCount < maxTryCount) { 69 | if (session == null) { 70 | connectionAlive = bind(); 71 | } else { 72 | log.debug("Session state is " + session.getStateName() + " bound: " + session.isBound()); 73 | connectionAlive = checkBoundState(); 74 | } 75 | tryCount++; 76 | } 77 | 78 | if (!connectionAlive) { 79 | throw new SmppSessionException(); 80 | } 81 | } 82 | 83 | /** 84 | * Binding new smpp session 85 | * 86 | * @return true - if binding was successfully, false - otherwise 87 | * @see DefaultSmppClient#bind(SmppSessionConfiguration, SmppSessionHandler) 88 | */ 89 | protected boolean bind() { 90 | try { 91 | session = client.bind(configuration, sessionHandler); 92 | log.debug(SENDER_SUCCESS_BINDED_MESSAGE); 93 | return true; 94 | } catch (Exception ex) { 95 | log.error(ex.getMessage(), ex); 96 | } 97 | return false; 98 | } 99 | 100 | /** 101 | * Check is {@link #session} in bound state.
102 | *

103 | * If true, then sending ping command. If ping fails then trying to reconnect.
104 | * If false, check is smpp session in binding state then method return false, otherwise try to reconnect session. 105 | *

106 | * 107 | * @return true if session in bound state and ping/reconnection success. 108 | * @see #pingOrReconnect() 109 | * @see #isBindingOrReconnect() 110 | */ 111 | private boolean checkBoundState() { 112 | if (session.isBound()) { 113 | return pingOrReconnect(); 114 | } else { 115 | return isBindingOrReconnect(); 116 | } 117 | } 118 | 119 | /** 120 | * Check is smpp session in `binding` state. Reconnect session if session not in `binding` state 121 | * 122 | * @return is {@link #session} in binding state - return false, otherwise returns result of {@link #reconnect()} method 123 | */ 124 | private boolean isBindingOrReconnect() { 125 | if (session.isBinding()) { 126 | sleep(50); 127 | return false; 128 | } else { 129 | return reconnect(); 130 | } 131 | } 132 | 133 | private void sleep(long millis) { 134 | try { 135 | Thread.sleep(millis); 136 | } catch (InterruptedException ignore) { 137 | // ignore 138 | } 139 | } 140 | 141 | /** 142 | * Send ping command. If ping return false - then try to reconnect. 143 | * 144 | * @return if ping or reconnection was successfully 145 | * @see #pingConnection() 146 | * @see #reconnect() 147 | */ 148 | private boolean pingOrReconnect() { 149 | return pingConnection() || reconnect(); 150 | } 151 | 152 | /** 153 | * Sending test request {@link EnquireLink}. 154 | * 155 | * @return if request was successfully 156 | */ 157 | private boolean pingConnection() { 158 | try { 159 | session.enquireLink(new EnquireLink(), 1000); 160 | return true; 161 | } catch (Exception ex) { 162 | log.debug(ex.getMessage(), ex); 163 | } 164 | return false; 165 | } 166 | 167 | /** 168 | * Closing existing smpp session and create new connection. 169 | * 170 | * @return true if reconnection success, false - otherwise 171 | * @see #closeSession() 172 | * @see #bind() 173 | */ 174 | private boolean reconnect() { 175 | closeSession(); 176 | return bind(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/BaseSmppSessionConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSessionConfiguration; 4 | 5 | public abstract class BaseSmppSessionConfiguration extends SmppSessionConfiguration { 6 | public abstract String configInformation(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/ConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | 5 | /** 6 | * Manager class for smpp connection. 7 | */ 8 | public interface ConnectionManager { 9 | /** 10 | * Get current or open new smpp connection session 11 | * 12 | * @return smpp session 13 | */ 14 | SmppSession getSession(); 15 | 16 | /** 17 | * Closes current smpp connection session 18 | */ 19 | void closeSession(); 20 | 21 | /** 22 | * @return smpp connection session configuration 23 | */ 24 | BaseSmppSessionConfiguration getConfiguration(); 25 | 26 | /** 27 | * Closes current smpp connection, destroy smpp client 28 | */ 29 | void destroy(); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/ConnectionManagerFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSessionHandler; 4 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 5 | import com.github.mikesafonov.smpp.config.SmppProperties; 6 | import org.springframework.lang.Nullable; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.NotNull; 10 | import java.util.concurrent.Executors; 11 | 12 | import static com.github.mikesafonov.smpp.core.utils.Utils.getOrDefault; 13 | import static com.github.mikesafonov.smpp.core.utils.Utils.validateName; 14 | import static java.util.Objects.requireNonNull; 15 | 16 | /** 17 | * Factory for {@link ConnectionManager} 18 | */ 19 | public class ConnectionManagerFactory { 20 | 21 | public ConnectionManager transmitter(@NotBlank String name, 22 | @NotNull SmppProperties.Defaults defaults, 23 | @NotNull SmppProperties.SMSC smsc) { 24 | validateName(name); 25 | requireNonNull(defaults); 26 | requireNonNull(smsc); 27 | 28 | boolean loggingBytes = getOrDefault(smsc.getLoggingBytes(), defaults.isLoggingBytes()); 29 | boolean loggingPdu = getOrDefault(smsc.getLoggingPdu(), defaults.isLoggingPdu()); 30 | int windowsSize = getOrDefault(smsc.getWindowSize(), defaults.getWindowSize()); 31 | int maxTry = getOrDefault(smsc.getMaxTry(), defaults.getMaxTry()); 32 | 33 | TransmitterConfiguration transmitterConfiguration = new TransmitterConfiguration(name, 34 | smsc.getCredentials(), loggingBytes, loggingPdu, windowsSize, smsc.getSystemType()); 35 | 36 | DefaultSmppClient client = new DefaultSmppClient(); 37 | return new TransmitterConnectionManager(client, transmitterConfiguration, maxTry); 38 | } 39 | 40 | public ConnectionManager receiver(@NotBlank String name, 41 | @NotNull SmppProperties.Defaults defaults, 42 | @NotNull SmppProperties.SMSC smsc, 43 | @Nullable SmppSessionHandler smppSessionHandler) { 44 | validateName(name); 45 | requireNonNull(defaults); 46 | requireNonNull(smsc); 47 | 48 | boolean loggingBytes = getOrDefault(smsc.getLoggingBytes(), defaults.isLoggingBytes()); 49 | boolean loggingPdu = getOrDefault(smsc.getLoggingPdu(), defaults.isLoggingPdu()); 50 | long rebindPeriod = getOrDefault(smsc.getRebindPeriod(), defaults.getRebindPeriod()).getSeconds(); 51 | 52 | ReceiverConfiguration receiverConfiguration = new ReceiverConfiguration(name, smsc.getCredentials(), 53 | loggingBytes, loggingPdu); 54 | DefaultSmppClient client = new DefaultSmppClient(); 55 | 56 | return new ReceiverConnectionManager( 57 | client, receiverConfiguration, 58 | smppSessionHandler, 59 | rebindPeriod, 60 | Executors.newSingleThreadScheduledExecutor() 61 | ); 62 | } 63 | 64 | public ConnectionManager transceiver(@NotBlank String name, 65 | @NotNull SmppProperties.Defaults defaults, 66 | @NotNull SmppProperties.SMSC smsc, 67 | @Nullable SmppSessionHandler smppSessionHandler) { 68 | validateName(name); 69 | requireNonNull(defaults); 70 | requireNonNull(smsc); 71 | 72 | boolean loggingBytes = getOrDefault(smsc.getLoggingBytes(), defaults.isLoggingBytes()); 73 | boolean loggingPdu = getOrDefault(smsc.getLoggingPdu(), defaults.isLoggingPdu()); 74 | int windowsSize = getOrDefault(smsc.getWindowSize(), defaults.getWindowSize()); 75 | int maxTry = getOrDefault(smsc.getMaxTry(), defaults.getMaxTry()); 76 | 77 | 78 | TransceiverConfiguration configuration = new TransceiverConfiguration(name, smsc.getCredentials(), 79 | loggingBytes, loggingPdu, windowsSize, smsc.getSystemType()); 80 | DefaultSmppClient client = new DefaultSmppClient(); 81 | 82 | return new TransceiverConnectionManager(client, configuration, smppSessionHandler, maxTry); 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/ReceiverConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppBindType; 4 | import com.cloudhopper.smpp.type.LoggingOptions; 5 | import com.github.mikesafonov.smpp.config.SmppProperties; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import java.util.Objects; 9 | 10 | /** 11 | * Configuration for receiver session 12 | * 13 | * @author Mike Safonov 14 | */ 15 | public class ReceiverConfiguration extends BaseSmppSessionConfiguration { 16 | 17 | public ReceiverConfiguration(@NotNull String name, @NotNull SmppProperties.Credentials credentials, 18 | boolean loggingBytes, boolean loggingPdu) { 19 | super(); 20 | 21 | setType(SmppBindType.RECEIVER); 22 | setName(Objects.requireNonNull(name)); 23 | setHost(credentials.getHost()); 24 | setPort(credentials.getPort()); 25 | setSystemId(credentials.getUsername()); 26 | setPassword(credentials.getPassword()); 27 | LoggingOptions loggingOptions = new LoggingOptions(); 28 | loggingOptions.setLogBytes(loggingBytes); 29 | loggingOptions.setLogPdu(loggingPdu); 30 | setLoggingOptions(loggingOptions); 31 | 32 | } 33 | 34 | public String configInformation() { 35 | return String.format("%s host=%s port=%d username=%s", getName(), getHost(), getPort(), getSystemId()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/ReceiverConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppClient; 4 | import com.cloudhopper.smpp.SmppSession; 5 | import com.cloudhopper.smpp.SmppSessionConfiguration; 6 | import com.cloudhopper.smpp.SmppSessionHandler; 7 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 8 | import com.cloudhopper.smpp.type.SmppChannelException; 9 | import com.cloudhopper.smpp.type.SmppTimeoutException; 10 | import com.cloudhopper.smpp.type.UnrecoverablePduException; 11 | import com.github.mikesafonov.smpp.core.exceptions.ResponseClientBindException; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.ScheduledExecutorService; 17 | import java.util.concurrent.ScheduledFuture; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | import static java.lang.String.format; 21 | 22 | @Slf4j 23 | @RequiredArgsConstructor 24 | public class ReceiverConnectionManager implements ConnectionManager { 25 | private static final String SESSION_SUCCESS_MESSAGE = "SESSION SUCCESSFUL REBINDED"; 26 | 27 | private final DefaultSmppClient client; 28 | private final ReceiverConfiguration configuration; 29 | private final SmppSessionHandler sessionHandler; 30 | /** 31 | * reconnection period in seconds 32 | */ 33 | private final long rebindPeriod; 34 | /** 35 | * Scheduled reconnection scheduledExecutorService 36 | */ 37 | private final ScheduledExecutorService scheduledExecutorService; 38 | /** 39 | * ScheduledFuture for {@link ResponseClientRebindTask}. 40 | */ 41 | private ScheduledFuture rebindTask; 42 | private SmppSession session; 43 | 44 | 45 | @Override 46 | public SmppSession getSession() { 47 | checkSession(); 48 | return session; 49 | } 50 | 51 | @Override 52 | public void closeSession() { 53 | if (session != null) { 54 | session.close(); 55 | session.destroy(); 56 | session = null; 57 | } 58 | } 59 | 60 | @Override 61 | public BaseSmppSessionConfiguration getConfiguration() { 62 | return configuration; 63 | } 64 | 65 | @Override 66 | public void destroy() { 67 | interruptIfNotNull(); 68 | scheduledExecutorService.shutdown(); 69 | closeSession(); 70 | client.destroy(); 71 | } 72 | 73 | /** 74 | * If session is null - creating new session using method {@link #bind()}. 75 | * 76 | * @throws ResponseClientBindException if unable to obtain session 77 | */ 78 | private void checkSession() { 79 | if (session == null) { 80 | bind(); 81 | } 82 | if (session == null) { 83 | throw new ResponseClientBindException(format("Unable to bind with configuration: %s ", 84 | configuration.configInformation())); 85 | } 86 | setupRebindTask(); 87 | } 88 | 89 | /** 90 | * Creting SMPP session and connecting to SMSC. 91 | * 92 | * @see SmppClient#bind(SmppSessionConfiguration, SmppSessionHandler) 93 | */ 94 | private void bind() { 95 | try { 96 | session = client.bind(configuration, sessionHandler); 97 | log.debug(SESSION_SUCCESS_MESSAGE); 98 | } catch (SmppTimeoutException | SmppChannelException | UnrecoverablePduException | InterruptedException ex) { 99 | log.error(ex.getMessage(), ex); 100 | session = null; 101 | } 102 | } 103 | 104 | /** 105 | * Closing existing smpp session and create new connection. 106 | * 107 | * @see #closeSession() 108 | * @see #bind() 109 | */ 110 | private void reconnect() { 111 | closeSession(); 112 | bind(); 113 | } 114 | 115 | /** 116 | * Creating running {@link ResponseClientRebindTask} on single thread {@link ScheduledExecutorService} 117 | * 118 | * @see Executors#newSingleThreadScheduledExecutor() 119 | */ 120 | private void setupRebindTask() { 121 | interruptIfNotNull(); 122 | rebindTask = scheduledExecutorService.scheduleAtFixedRate(new ResponseClientRebindTask(session, this::reconnect), 123 | 5, rebindPeriod, TimeUnit.SECONDS); 124 | } 125 | 126 | /** 127 | * Interrupts {@link #rebindTask} if it not null 128 | */ 129 | private void interruptIfNotNull() { 130 | if (rebindTask != null) { 131 | rebindTask.cancel(true); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/ResponseClientRebindTask.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import lombok.AllArgsConstructor; 5 | 6 | @AllArgsConstructor 7 | public class ResponseClientRebindTask implements Runnable { 8 | private SmppSession session; 9 | private SessionReconnector reconnector; 10 | 11 | @Override 12 | public void run() { 13 | if (session == null) { 14 | reconnector.reconnect(); 15 | return; 16 | } 17 | if (session.isBound()) { 18 | reconnector.reconnect(); 19 | return; 20 | } 21 | if (session.isBinding()) { 22 | return; 23 | } 24 | reconnector.reconnect(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/SessionReconnector.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | @FunctionalInterface 4 | public interface SessionReconnector { 5 | void reconnect(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/TransceiverConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppBindType; 4 | import com.cloudhopper.smpp.type.LoggingOptions; 5 | import com.github.mikesafonov.smpp.config.SmppProperties; 6 | 7 | import javax.validation.constraints.NotNull; 8 | 9 | import static java.util.Objects.requireNonNull; 10 | 11 | public class TransceiverConfiguration extends BaseSmppSessionConfiguration { 12 | public TransceiverConfiguration(@NotNull String name, @NotNull SmppProperties.Credentials credentials, 13 | boolean loggingBytes, boolean loggingPdu, int windowsSize, String systemType) { 14 | super(); 15 | 16 | setType(SmppBindType.TRANSCEIVER); 17 | setName(requireNonNull(name)); 18 | setHost(credentials.getHost()); 19 | setPort(credentials.getPort()); 20 | setSystemId(credentials.getUsername()); 21 | setPassword(credentials.getPassword()); 22 | setWindowSize(windowsSize); 23 | setSystemType(systemType); 24 | 25 | LoggingOptions loggingOptions = new LoggingOptions(); 26 | loggingOptions.setLogBytes(loggingBytes); 27 | loggingOptions.setLogPdu(loggingPdu); 28 | setLoggingOptions(loggingOptions); 29 | } 30 | 31 | public String configInformation() { 32 | return String.format("%s host=%s port=%d username=%s windowsSize=%d", getName(), getHost(), 33 | getPort(), getSystemId(), getWindowSize()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/TransceiverConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSessionHandler; 4 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 5 | 6 | /** 7 | * Configuration for sender session 8 | * 9 | * @author Mike Safonov 10 | */ 11 | public class TransceiverConnectionManager extends BaseSenderConnectionManager { 12 | public TransceiverConnectionManager(DefaultSmppClient client, 13 | TransceiverConfiguration configuration, 14 | SmppSessionHandler sessionHandler, 15 | int maxTryCount) { 16 | super(client, configuration, sessionHandler, maxTryCount); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/TransmitterConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppBindType; 4 | import com.cloudhopper.smpp.type.LoggingOptions; 5 | import com.github.mikesafonov.smpp.config.SmppProperties; 6 | 7 | import javax.validation.constraints.NotNull; 8 | 9 | import static java.util.Objects.requireNonNull; 10 | 11 | /** 12 | * Configuration for sender session 13 | * 14 | * @author Mike Safonov 15 | */ 16 | public class TransmitterConfiguration extends BaseSmppSessionConfiguration { 17 | 18 | public TransmitterConfiguration(@NotNull String name, @NotNull SmppProperties.Credentials credentials, 19 | boolean loggingBytes, boolean loggingPdu, int windowsSize, String systemType) { 20 | super(); 21 | 22 | setType(SmppBindType.TRANSMITTER); 23 | setName(requireNonNull(name)); 24 | setHost(credentials.getHost()); 25 | setPort(credentials.getPort()); 26 | setSystemId(credentials.getUsername()); 27 | setPassword(credentials.getPassword()); 28 | setWindowSize(windowsSize); 29 | setSystemType(systemType); 30 | 31 | LoggingOptions loggingOptions = new LoggingOptions(); 32 | loggingOptions.setLogBytes(loggingBytes); 33 | loggingOptions.setLogPdu(loggingPdu); 34 | setLoggingOptions(loggingOptions); 35 | } 36 | 37 | public String configInformation() { 38 | return String.format("%s host=%s port=%d username=%s windowsSize=%d", getName(), getHost(), 39 | getPort(), getSystemId(), getWindowSize()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/connection/TransmitterConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 4 | 5 | public class TransmitterConnectionManager extends BaseSenderConnectionManager { 6 | public TransmitterConnectionManager(DefaultSmppClient client, 7 | TransmitterConfiguration configuration, 8 | int maxTryCount) { 9 | super(client, configuration, maxTryCount); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/dto/CancelMessage.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import lombok.Value; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | 7 | /** 8 | * Message to cancel 9 | * 10 | * @author Mike Safonov 11 | */ 12 | @Value 13 | public class CancelMessage { 14 | /** 15 | * Id of smsc message. 16 | */ 17 | @NotBlank 18 | private String messageId; 19 | /** 20 | * Source name (alpha name) 21 | */ 22 | @NotBlank 23 | private String source; 24 | /** 25 | * Destination phone number(msisdn) 26 | */ 27 | @NotBlank 28 | private String msisdn; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/dto/CancelMessageResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import lombok.Value; 4 | import org.springframework.lang.Nullable; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | 9 | /** 10 | * Response of cancel message 11 | * 12 | * @author Mike Safonov 13 | */ 14 | @Value 15 | public class CancelMessageResponse { 16 | /** 17 | * Original cancel message 18 | */ 19 | @NotNull 20 | private CancelMessage original; 21 | /** 22 | * Id of smsc connection 23 | */ 24 | @NotBlank 25 | private String smscConnectionId; 26 | /** 27 | * Is cancel success 28 | */ 29 | private boolean success; 30 | /** 31 | * Error information 32 | */ 33 | @Nullable 34 | private MessageErrorInformation messageErrorInformation; 35 | 36 | 37 | public static CancelMessageResponse success(@NotNull CancelMessage original, @NotBlank String smscConnectionId) { 38 | return new CancelMessageResponse(original, smscConnectionId, true, null); 39 | } 40 | 41 | public static CancelMessageResponse error(@NotNull CancelMessage original, @NotBlank String smscConnectionId, @NotNull MessageErrorInformation messageErrorInformation) { 42 | return new CancelMessageResponse(original, smscConnectionId, false, messageErrorInformation); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/dto/DeliveryReport.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import com.cloudhopper.smpp.util.DeliveryReceipt; 4 | import lombok.Data; 5 | 6 | import javax.validation.constraints.NotNull; 7 | import java.time.ZonedDateTime; 8 | 9 | import static com.github.mikesafonov.smpp.core.utils.JodaJavaConverter.convert; 10 | 11 | /** 12 | * @author Mike Safonov 13 | */ 14 | @Data 15 | public class DeliveryReport { 16 | 17 | private String messageId; 18 | private ZonedDateTime deliveryDate; 19 | private ZonedDateTime submitDate; 20 | private int deliveredCount; 21 | private int submitCount; 22 | private int errorCode; 23 | private int state; 24 | private String responseClientId; 25 | private String text; 26 | 27 | public static DeliveryReport of(@NotNull final DeliveryReceipt deliveryReceipt, @NotNull String responseClientId){ 28 | DeliveryReport deliveryReport = new DeliveryReport(); 29 | deliveryReport.setDeliveredCount(deliveryReceipt.getDeliveredCount()); 30 | deliveryReport.setMessageId(deliveryReceipt.getMessageId()); 31 | deliveryReport.setErrorCode(deliveryReceipt.getErrorCode()); 32 | deliveryReport.setState(deliveryReceipt.getState()); 33 | deliveryReport.setSubmitCount(deliveryReceipt.getSubmitCount()); 34 | deliveryReport.setDeliveryDate(convert(deliveryReceipt.getDoneDate())); 35 | deliveryReport.setSubmitDate(convert(deliveryReceipt.getSubmitDate())); 36 | deliveryReport.setResponseClientId(responseClientId); 37 | deliveryReport.setText(deliveryReceipt.getText()); 38 | return deliveryReport; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/dto/Message.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import org.springframework.lang.Nullable; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | 10 | 11 | /** 12 | * Incoming message 13 | * 14 | * @author Mike Safonov 15 | */ 16 | @Data 17 | @AllArgsConstructor 18 | public class Message { 19 | 20 | /** 21 | * Message text 22 | */ 23 | @NotBlank 24 | private String text; 25 | /** 26 | * Destination phone number(msisdn) 27 | */ 28 | @NotBlank 29 | private String msisdn; 30 | /** 31 | * Source name (alpha name) 32 | */ 33 | @NotBlank 34 | private String source; 35 | /** 36 | * Client specific id. May be null 37 | */ 38 | @Nullable 39 | private String messageId; 40 | 41 | /** 42 | * Message type 43 | */ 44 | @NotNull 45 | private MessageType messageType; 46 | 47 | /** 48 | * Message validity period 49 | */ 50 | @Nullable 51 | private String validityPeriod; 52 | 53 | public Message(@NotBlank String text, @NotBlank String msisdn, @NotBlank String source, 54 | @Nullable String messageId, @NotNull MessageType messageType) { 55 | this.text = text; 56 | this.msisdn = msisdn; 57 | this.source = source; 58 | this.messageId = messageId; 59 | this.messageType = messageType; 60 | } 61 | 62 | public boolean isSilent() { 63 | return messageType == MessageType.SILENT; 64 | } 65 | 66 | public static MessageBuilder silent(String text) { 67 | return new MessageBuilder(text, MessageType.SILENT); 68 | } 69 | 70 | public static MessageBuilder datagram(String text) { 71 | return new MessageBuilder(text, MessageType.DATAGRAM); 72 | } 73 | 74 | public static MessageBuilder simple(String text) { 75 | return new MessageBuilder(text, MessageType.SIMPLE); 76 | } 77 | 78 | public static MessageBuilder flash(String text){ 79 | return new MessageBuilder(text, MessageType.FLASH); 80 | } 81 | 82 | public static class MessageBuilder { 83 | private final String text; 84 | private final MessageType messageType; 85 | private String msisdn; 86 | private String source; 87 | private String messageId; 88 | private String validityPeriod; 89 | 90 | public MessageBuilder(String text, MessageType messageType) { 91 | this.text = text; 92 | this.messageType = messageType; 93 | } 94 | 95 | public MessageBuilder from(String from) { 96 | this.source = from; 97 | return this; 98 | } 99 | 100 | public MessageBuilder to(String to) { 101 | this.msisdn = to; 102 | return this; 103 | } 104 | 105 | public MessageBuilder messageId(String messageId) { 106 | this.messageId = messageId; 107 | return this; 108 | } 109 | 110 | public MessageBuilder validityPeriod(String validityPeriod){ 111 | this.validityPeriod = validityPeriod; 112 | return this; 113 | } 114 | 115 | public Message build() { 116 | return new Message(text, msisdn, source, messageId, 117 | messageType, validityPeriod); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/dto/MessageErrorInformation.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * @author Mike Safonov 7 | */ 8 | @Value 9 | public class MessageErrorInformation { 10 | private final int errorCode; 11 | private final String errorMessage; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/dto/MessageResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import lombok.Value; 4 | import org.springframework.lang.Nullable; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | 9 | /** 10 | * Response of message 11 | * 12 | * @author Mike Safonov 13 | */ 14 | @Value 15 | public class MessageResponse { 16 | /** 17 | * Original message 18 | */ 19 | @NotNull 20 | private Message original; 21 | /** 22 | * Id of smsc connection 23 | */ 24 | @NotBlank 25 | private String smscId; 26 | 27 | /** 28 | * Id of smsc message. May be null if {@link #sent} false or if {@link #original} is datagram message 29 | */ 30 | @Nullable 31 | private String smscMessageID; 32 | 33 | /** 34 | * Is message sent successfully 35 | */ 36 | private boolean sent; 37 | /** 38 | * Error code and message. May be null if response success 39 | */ 40 | @Nullable 41 | private MessageErrorInformation messageErrorInformation; 42 | 43 | 44 | public static MessageResponse success(@NotNull Message original, @NotBlank String smscId, String smscMessageID) { 45 | return new MessageResponse(original, smscId, smscMessageID, true, null); 46 | } 47 | 48 | public static MessageResponse error(@NotNull Message original, @NotBlank String smscId, @NotNull MessageErrorInformation messageErrorInformation) { 49 | return new MessageResponse(original, smscId, null, false, messageErrorInformation); 50 | } 51 | 52 | public boolean isSuccess(){ 53 | return messageErrorInformation == null; 54 | } 55 | 56 | public boolean isError(){ 57 | return messageErrorInformation != null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/dto/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | /** 4 | * @author Mike Safonov 5 | */ 6 | public enum MessageType { 7 | SIMPLE, 8 | DATAGRAM, 9 | SILENT, 10 | FLASH 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/exceptions/ClientNameSmppException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.exceptions; 2 | 3 | public class ClientNameSmppException extends RuntimeException { 4 | public ClientNameSmppException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/exceptions/IllegalAddressException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.exceptions; 2 | 3 | /** 4 | * @author MikeSafonov 5 | */ 6 | public class IllegalAddressException extends RuntimeException { 7 | 8 | public IllegalAddressException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/exceptions/ResponseClientBindException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.exceptions; 2 | 3 | /** 4 | * @author Mike Safonov 5 | */ 6 | public class ResponseClientBindException extends RuntimeException { 7 | 8 | public ResponseClientBindException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/exceptions/SenderClientBindException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.exceptions; 2 | 3 | /** 4 | * @author Mike Safonov 5 | */ 6 | public class SenderClientBindException extends RuntimeException { 7 | 8 | public SenderClientBindException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/exceptions/SmppException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.exceptions; 2 | 3 | /** 4 | * @author Mike Safonov 5 | */ 6 | public class SmppException extends RuntimeException { 7 | 8 | protected final int errorCode; 9 | protected final String errorMessage; 10 | 11 | public SmppException(int errorCode, String errorMessage){ 12 | 13 | super(errorMessage); 14 | 15 | this.errorCode = errorCode; 16 | this.errorMessage = errorMessage; 17 | } 18 | 19 | public int getErrorCode() { 20 | return errorCode; 21 | } 22 | 23 | public String getErrorMessage() { 24 | return errorMessage; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/exceptions/SmppMessageBuildingException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.exceptions; 2 | 3 | public class SmppMessageBuildingException extends SmppException { 4 | public SmppMessageBuildingException() { 5 | super(101, "Invalid param"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/exceptions/SmppSessionException.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.exceptions; 2 | 3 | 4 | 5 | /** 6 | * @author Mike Safonov 7 | */ 8 | public class SmppSessionException extends SmppException { 9 | 10 | private static final int SESSION_ERROR = 100; 11 | private static final String SESSION_ERROR_MESSAGE = "Unable to bind session"; 12 | 13 | 14 | public SmppSessionException(){ 15 | 16 | super(SESSION_ERROR, SESSION_ERROR_MESSAGE); 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/generators/AlwaysSuccessSmppResultGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.generators; 2 | 3 | 4 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 5 | import com.github.mikesafonov.smpp.core.dto.CancelMessageResponse; 6 | import com.github.mikesafonov.smpp.core.dto.Message; 7 | import com.github.mikesafonov.smpp.core.dto.MessageResponse; 8 | import lombok.EqualsAndHashCode; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.util.UUID; 12 | 13 | /** 14 | * Implementation of {@link SmppResultGenerator} which always generate success {@link MessageResponse}/ 15 | * {@link CancelMessageResponse} with random smsc message id (random UUID). 16 | * 17 | * @author Mike Safonov 18 | */ 19 | @EqualsAndHashCode 20 | public class AlwaysSuccessSmppResultGenerator implements SmppResultGenerator { 21 | 22 | @Override 23 | public MessageResponse generate(String smscId, Message message) { 24 | return MessageResponse.success(message, smscId, randomHexId()); 25 | } 26 | 27 | @Override 28 | public @NotNull CancelMessageResponse generate(@NotNull String smscId, @NotNull CancelMessage cancelMessage) { 29 | return CancelMessageResponse.success(cancelMessage, smscId); 30 | } 31 | 32 | 33 | private String randomHexId() { 34 | return UUID.randomUUID().toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/generators/SmppResultGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.generators; 2 | 3 | 4 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 5 | import com.github.mikesafonov.smpp.core.dto.CancelMessageResponse; 6 | import com.github.mikesafonov.smpp.core.dto.Message; 7 | import com.github.mikesafonov.smpp.core.dto.MessageResponse; 8 | import com.github.mikesafonov.smpp.core.sender.MockSenderClient; 9 | import com.github.mikesafonov.smpp.core.sender.TestSenderClient; 10 | 11 | import javax.validation.constraints.NotBlank; 12 | import javax.validation.constraints.NotNull; 13 | 14 | /** 15 | * Implementations of this interface is used by mock {@link MockSenderClient} or test {@link TestSenderClient} 16 | * clients to generate request answer {@link MessageResponse} and {@link CancelMessageResponse} 17 | * 18 | * @author Mike Safonov 19 | */ 20 | public interface SmppResultGenerator { 21 | 22 | @NotNull MessageResponse generate(@NotBlank String smscId, @NotNull Message message); 23 | 24 | @NotNull CancelMessageResponse generate(@NotNull String smscId, @NotNull CancelMessage cancelMessage); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/reciever/DeliveryReportConsumer.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | 4 | import com.github.mikesafonov.smpp.core.dto.DeliveryReport; 5 | 6 | import java.util.function.Consumer; 7 | 8 | /** 9 | * This class dedicated to handle {@link DeliveryReport} on client side. Starter`s client may build custom 10 | * logic on receiving delivery report by implementing this interface 11 | * 12 | * @author Mike Safonov 13 | */ 14 | public interface DeliveryReportConsumer extends Consumer { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/reciever/NullDeliveryReportConsumer.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | import com.github.mikesafonov.smpp.core.dto.DeliveryReport; 4 | 5 | /** 6 | * Implementation of {@link DeliveryReportConsumer} which simple ignore any delivery reports. 7 | * This is default implementation if any other implementations not presents in application context. 8 | * 9 | * @author Mike Safonov 10 | */ 11 | public class NullDeliveryReportConsumer implements DeliveryReportConsumer { 12 | @Override 13 | public void accept(DeliveryReport deliveryReport) { 14 | // ignore delivery report 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/reciever/ResponseClient.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import com.github.mikesafonov.smpp.core.exceptions.ResponseClientBindException; 5 | import org.springframework.lang.Nullable; 6 | 7 | import javax.validation.constraints.NotNull; 8 | 9 | /** 10 | * This abstraction represent connection via SMPP with RECEIVER type. Key purpose is listening 11 | * delivery reports. 12 | * 13 | * @author Mike Safonov 14 | */ 15 | public interface ResponseClient { 16 | 17 | /** 18 | * @return Identifier of response client 19 | */ 20 | @NotNull String getId(); 21 | 22 | /** 23 | * Create smpp connection and bind to it 24 | * 25 | * @throws ResponseClientBindException if connection not successfully 26 | */ 27 | void setup(); 28 | 29 | /** 30 | * Access to SMPP session 31 | * 32 | * @return smpp session, may be null 33 | */ 34 | @Nullable 35 | SmppSession getSession(); 36 | 37 | /** 38 | * Perform reconnection 39 | */ 40 | void reconnect(); 41 | 42 | /** 43 | * Close smpp session connection and destroy client 44 | */ 45 | void destroyClient(); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/reciever/ResponseSmppSessionHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler; 4 | import com.cloudhopper.smpp.pdu.DeliverSm; 5 | import com.cloudhopper.smpp.pdu.PduRequest; 6 | import com.cloudhopper.smpp.pdu.PduResponse; 7 | import com.cloudhopper.smpp.util.DeliveryReceipt; 8 | import com.cloudhopper.smpp.util.DeliveryReceiptException; 9 | import com.github.mikesafonov.smpp.core.dto.DeliveryReport; 10 | import lombok.EqualsAndHashCode; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.joda.time.DateTimeZone; 13 | 14 | import javax.validation.constraints.NotNull; 15 | import java.util.List; 16 | 17 | import static java.util.Objects.requireNonNull; 18 | 19 | /** 20 | * Handler for listening PDU events (delivery reports, etc) 21 | * 22 | * @author Mike Safonov 23 | */ 24 | @Slf4j 25 | @EqualsAndHashCode(callSuper = true) 26 | public class ResponseSmppSessionHandler extends DefaultSmppSessionHandler { 27 | 28 | private final String clientId; 29 | private final List deliveryReportConsumers; 30 | 31 | public ResponseSmppSessionHandler(String clientId, @NotNull List deliveryReportConsumers) { 32 | this.clientId = requireNonNull(clientId); 33 | this.deliveryReportConsumers = requireNonNull(deliveryReportConsumers); 34 | } 35 | 36 | @Override 37 | public PduResponse firePduRequestReceived(PduRequest pduRequest) { 38 | log.debug(pduRequest.toString()); 39 | if (isDelivery(pduRequest)) { 40 | processReport(pduRequest); 41 | } 42 | 43 | return pduRequest.createResponse(); 44 | } 45 | 46 | private boolean isDelivery(PduRequest pduRequest) { 47 | return pduRequest.isRequest() && pduRequest.getClass() == DeliverSm.class; 48 | } 49 | 50 | private void processReport(PduRequest pduRequest) { 51 | DeliverSm dlr = (DeliverSm) pduRequest; 52 | try { 53 | DeliveryReport report = toReport(dlr); 54 | for (DeliveryReportConsumer deliveryReportConsumer : deliveryReportConsumers) { 55 | deliveryReportConsumer.accept(report); 56 | } 57 | } catch (DeliveryReceiptException e) { 58 | log.error(e.getMessage(), e); 59 | } 60 | } 61 | 62 | private DeliveryReport toReport(DeliverSm deliverSm) throws DeliveryReceiptException { 63 | byte[] shortMessage = deliverSm.getShortMessage(); 64 | String sms = new String(shortMessage); 65 | DeliveryReceipt deliveryReceipt = DeliveryReceipt.parseShortMessage(sms, DateTimeZone.UTC); 66 | return DeliveryReport.of(deliveryReceipt, clientId); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/reciever/StandardResponseClient.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import javax.validation.constraints.NotNull; 8 | 9 | import static java.util.Objects.requireNonNull; 10 | 11 | /** 12 | * Default implementation of {@link ResponseClient}. 13 | * 14 | * @author Mike Safonov 15 | */ 16 | @Slf4j 17 | public class StandardResponseClient implements ResponseClient { 18 | private final ConnectionManager connectionManager; 19 | private boolean inited = false; 20 | 21 | /** 22 | * @param connectionManager smpp connection manager 23 | */ 24 | public StandardResponseClient(@NotNull ConnectionManager connectionManager) { 25 | this.connectionManager = requireNonNull(connectionManager); 26 | } 27 | 28 | @Override 29 | public @NotNull String getId() { 30 | return connectionManager.getConfiguration().getName(); 31 | } 32 | 33 | @Override 34 | public void setup() { 35 | if (!inited) { 36 | connectionManager.getSession(); 37 | inited = true; 38 | } 39 | } 40 | 41 | @Override 42 | public void reconnect() { 43 | connectionManager.closeSession(); 44 | connectionManager.getSession(); 45 | } 46 | 47 | @Override 48 | public void destroyClient() { 49 | connectionManager.destroy(); 50 | } 51 | 52 | @Override 53 | public SmppSession getSession() { 54 | return connectionManager.getSession(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/AddressBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.gsm.TypeOfAddress; 4 | import com.cloudhopper.smpp.type.Address; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | import static java.util.Objects.requireNonNull; 9 | 10 | 11 | /** 12 | * Class for building {@link Address} of source and destination 13 | * 14 | * @author Mike Safonov 15 | */ 16 | public class AddressBuilder { 17 | 18 | private final TypeOfAddressParser addressParser; 19 | 20 | public AddressBuilder(@NotNull TypeOfAddressParser addressParser) { 21 | this.addressParser = requireNonNull(addressParser); 22 | } 23 | 24 | /** 25 | * Detect TON and NPI parameters from {@code source} and convert to {@link Address} 26 | * 27 | * @param source message source 28 | * @return source address 29 | */ 30 | @NotNull 31 | public Address createSourceAddress(@NotNull String source) { 32 | return convertToAddress(source, addressParser.getSource(source)); 33 | } 34 | 35 | 36 | /** 37 | * Detect TON and NPI parameters from {@code msisdn} and convert to {@link Address} 38 | * 39 | * @param msisdn destination phone number 40 | * @return destination address 41 | */ 42 | @NotNull 43 | public Address createDestinationAddress(@NotNull String msisdn) { 44 | return convertToAddress(msisdn, addressParser.getDestination(msisdn)); 45 | } 46 | 47 | private Address convertToAddress(String value, TypeOfAddress typeOfAddress) { 48 | byte ton = (byte) typeOfAddress.getTon().toInt(); 49 | byte npi = (byte) typeOfAddress.getNpi().toInt(); 50 | return new Address(ton, npi, value); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/DataCoding.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * @author Mike Safonov 7 | */ 8 | @UtilityClass 9 | public class DataCoding { 10 | public static final byte FLASH_CODING = (byte) 0x18; 11 | public static final byte SILENT_CODING = (byte) 0xC0; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/DefaultTypeOfAddressParser.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.gsm.Npi; 4 | import com.cloudhopper.commons.gsm.Ton; 5 | import com.cloudhopper.commons.gsm.TypeOfAddress; 6 | import com.github.mikesafonov.smpp.core.exceptions.IllegalAddressException; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import javax.validation.constraints.NotNull; 10 | import java.util.regex.Pattern; 11 | 12 | import static java.lang.String.format; 13 | 14 | /** 15 | * Default implementation of {@link TypeOfAddressParser} interface. Supports international and alphanumeric 16 | * ton parameters, otherwise return {@link Ton#UNKNOWN} 17 | * 18 | * @author Mike Safonov 19 | */ 20 | @Slf4j 21 | public class DefaultTypeOfAddressParser implements TypeOfAddressParser { 22 | 23 | private static final Pattern ALPHABET_PATTERN = Pattern.compile("^[a-zA-Z0-9_ -]{3,11}$"); 24 | private static final Pattern INTERNATIONAL_PATTERN = Pattern.compile("^[0-9]{10,15}$"); 25 | 26 | 27 | /** 28 | * Create TON and NPI parameters for message source {@code source}. Supported two types of source: 29 | * international or alphanumeric, otherwise return {@link Ton#UNKNOWN} 30 | * 31 | * @param source message source 32 | * @return ton and npi for source 33 | * @throws IllegalAddressException if exception occurs 34 | */ 35 | @Override 36 | @NotNull 37 | public TypeOfAddress getSource(@NotNull String source){ 38 | try { 39 | if (checkPattern(source, INTERNATIONAL_PATTERN)) { 40 | return new TypeOfAddress(Ton.INTERNATIONAL, Npi.ISDN); 41 | } else if (checkPattern(source, ALPHABET_PATTERN)) { 42 | return new TypeOfAddress(Ton.ALPHANUMERIC, Npi.UNKNOWN); 43 | } 44 | return new TypeOfAddress(Ton.UNKNOWN, Npi.UNKNOWN); 45 | } catch (Exception ex) { 46 | log.error(ex.getMessage(), ex); 47 | throw new IllegalAddressException(format("Source %s not supported", source)); 48 | } 49 | } 50 | 51 | /** 52 | * Create TON and NPI parameters for message destination {@code destination}. If number in international number format 53 | * returns {@link Ton#INTERNATIONAL}, otherwise return {@link Ton#UNKNOWN} 54 | * 55 | * @param destination message destination 56 | * @return ton and npi for destination 57 | * @throws IllegalAddressException if exception occurs 58 | */ 59 | @Override 60 | @NotNull 61 | public TypeOfAddress getDestination(@NotNull String destination) { 62 | try { 63 | if (checkPattern(destination, INTERNATIONAL_PATTERN)) { 64 | return new TypeOfAddress(Ton.INTERNATIONAL, Npi.ISDN); 65 | } 66 | return new TypeOfAddress(Ton.UNKNOWN, Npi.UNKNOWN); 67 | } catch (Exception ex) { 68 | log.error(ex.getMessage(), ex); 69 | throw new IllegalAddressException(format("Destination %s not supported", destination)); 70 | } 71 | } 72 | 73 | 74 | /** 75 | * Check is source matching specific pattern 76 | * 77 | * @param source text. 78 | * @return true if source is matching pattern 79 | */ 80 | private boolean checkPattern(@NotNull String source, @NotNull Pattern pattern){ 81 | return pattern.matcher(source).matches(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/FlashSubmitSmEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.charset.CharsetUtil; 4 | import com.cloudhopper.smpp.SmppConstants; 5 | import com.cloudhopper.smpp.pdu.SubmitSm; 6 | import com.cloudhopper.smpp.tlv.Tlv; 7 | import com.github.mikesafonov.smpp.core.dto.Message; 8 | import com.github.mikesafonov.smpp.core.utils.CountWithEncoding; 9 | import com.github.mikesafonov.smpp.core.utils.MessageUtil; 10 | import lombok.SneakyThrows; 11 | 12 | /** 13 | * Encoder for flash messages 14 | * 15 | * @author Mike Safonov 16 | */ 17 | public class FlashSubmitSmEncoder implements SubmitSmEncoder { 18 | 19 | @Override 20 | @SneakyThrows 21 | public void encode(Message message, SubmitSm submitSm, boolean ucs2Only) { 22 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(message.getText(), true); 23 | submitSm.setDataCoding(DataCoding.FLASH_CODING); 24 | byte[] messageByte = CharsetUtil.encode(message.getText(), CharsetUtil.CHARSET_UCS_2); 25 | if (countWithEncoding.getCount() > 1) { 26 | submitSm.setShortMessage(new byte[0]); 27 | submitSm.addOptionalParameter(new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, messageByte)); 28 | } else { 29 | submitSm.setShortMessage(messageByte); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/MessageBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.smpp.SmppConstants; 4 | import com.cloudhopper.smpp.pdu.CancelSm; 5 | import com.cloudhopper.smpp.pdu.SubmitSm; 6 | import com.cloudhopper.smpp.type.Address; 7 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 8 | import com.github.mikesafonov.smpp.core.dto.Message; 9 | import com.github.mikesafonov.smpp.core.dto.MessageType; 10 | import com.github.mikesafonov.smpp.core.exceptions.IllegalAddressException; 11 | import com.github.mikesafonov.smpp.core.exceptions.SmppMessageBuildingException; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | import javax.validation.constraints.NotNull; 15 | 16 | 17 | /** 18 | * @author MikeSafonov 19 | */ 20 | @Slf4j 21 | public class MessageBuilder { 22 | 23 | private final AddressBuilder addressBuilder; 24 | private final SubmitSmEncoderFactory encoderFactory; 25 | 26 | public MessageBuilder(@NotNull TypeOfAddressParser typeOfAddressParser) { 27 | this(new AddressBuilder(typeOfAddressParser), new SubmitSmEncoderFactory()); 28 | } 29 | 30 | public MessageBuilder(@NotNull AddressBuilder addressBuilder, @NotNull SubmitSmEncoderFactory encoderFactory) { 31 | this.addressBuilder = addressBuilder; 32 | this.encoderFactory = encoderFactory; 33 | } 34 | 35 | /** 36 | * Builds {@link SubmitSm} for sending via smpp. 37 | * 38 | * @param message client message 39 | * @param ucs2Only use UCS2 encoding only or not 40 | * @return message {@link SubmitSm}. 41 | */ 42 | @NotNull 43 | public SubmitSm createSubmitSm(@NotNull Message message, boolean ucs2Only) { 44 | try { 45 | Address sourceAddress = addressBuilder.createSourceAddress(message.getSource()); 46 | Address destAddress = addressBuilder.createDestinationAddress(message.getMsisdn()); 47 | 48 | return createSubmitSm(message, sourceAddress, 49 | destAddress, ucs2Only); 50 | } catch (Exception e) { 51 | log.error(e.getMessage(), e); 52 | throw new SmppMessageBuildingException(); 53 | } 54 | } 55 | 56 | /** 57 | * Builds {@link CancelSm} for canceling sms message 58 | * 59 | * @param cancelMessage cancel message 60 | * @return request {@link CancelSm} 61 | * @throws IllegalAddressException if source/destination address not created 62 | */ 63 | public CancelSm createCancelSm(CancelMessage cancelMessage) { 64 | Address sourceAddress = addressBuilder.createSourceAddress(cancelMessage.getSource()); 65 | Address destAddress = addressBuilder.createDestinationAddress(cancelMessage.getMsisdn()); 66 | 67 | CancelSm cancelSm = new CancelSm(); 68 | cancelSm.setSourceAddress(sourceAddress); 69 | cancelSm.setDestAddress(destAddress); 70 | cancelSm.setMessageId(cancelMessage.getMessageId()); 71 | return cancelSm; 72 | } 73 | 74 | private byte getEsmClass(MessageType messageType) { 75 | if(messageType == MessageType.DATAGRAM){ 76 | return SmppConstants.ESM_CLASS_MM_DATAGRAM; 77 | } 78 | if(messageType == MessageType.FLASH){ 79 | return SmppConstants.ESM_CLASS_MM_DEFAULT; 80 | } 81 | return SmppConstants.ESM_CLASS_MM_STORE_FORWARD; 82 | } 83 | 84 | 85 | /** 86 | * Builds {@link SubmitSm} for sending via smpp. 87 | * 88 | * @param message client message 89 | * @param sourceAddress source address 90 | * @param destAddress destination address 91 | * @return message {@link SubmitSm}. 92 | */ 93 | @NotNull 94 | private SubmitSm createSubmitSm(@NotNull Message message, @NotNull Address sourceAddress, @NotNull Address destAddress, 95 | boolean ucs2Only) { 96 | 97 | byte esmClass = getEsmClass(message.getMessageType()); 98 | SubmitSm sm = new SubmitSm(); 99 | sm.setEsmClass(esmClass); 100 | sm.setSourceAddress(sourceAddress); 101 | sm.setDestAddress(destAddress); 102 | sm.setValidityPeriod(message.getValidityPeriod()); 103 | encoderFactory.get(message).encode(message, sm, ucs2Only); 104 | 105 | if (message.getMessageType() == MessageType.SIMPLE) { 106 | sm.setRegisteredDelivery(SmppConstants.REGISTERED_DELIVERY_SMSC_RECEIPT_REQUESTED); 107 | } 108 | return sm; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/MockSenderClient.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 4 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 5 | import com.github.mikesafonov.smpp.core.dto.CancelMessageResponse; 6 | import com.github.mikesafonov.smpp.core.dto.Message; 7 | import com.github.mikesafonov.smpp.core.dto.MessageResponse; 8 | import com.github.mikesafonov.smpp.core.generators.SmppResultGenerator; 9 | import lombok.EqualsAndHashCode; 10 | 11 | import javax.validation.constraints.NotNull; 12 | 13 | import java.util.Optional; 14 | 15 | import static java.util.Objects.requireNonNull; 16 | 17 | /** 18 | * Implementation of {@link SenderClient} which not perform any connection via smpp and only generate 19 | * {@link MessageResponse}/{@link CancelMessageResponse} by using {@link SmppResultGenerator} 20 | * 21 | * @author Mike Safonov 22 | * @author Mikhail Epatko 23 | */ 24 | @EqualsAndHashCode 25 | public class MockSenderClient implements SenderClient { 26 | 27 | private final SmppResultGenerator smppResultGenerator; 28 | private final String id; 29 | 30 | public MockSenderClient(@NotNull SmppResultGenerator smppResultGenerator, @NotNull String id) { 31 | this.smppResultGenerator = requireNonNull(smppResultGenerator); 32 | this.id = requireNonNull(id); 33 | } 34 | 35 | @Override 36 | public @NotNull String getId() { 37 | return id; 38 | } 39 | 40 | @Override 41 | public void setup() { 42 | // should be empty 43 | } 44 | 45 | @Override 46 | public @NotNull MessageResponse send(@NotNull Message message) { 47 | return smppResultGenerator.generate(id, message); 48 | } 49 | 50 | @Override 51 | public @NotNull CancelMessageResponse cancel(@NotNull CancelMessage cancelMessage) { 52 | return smppResultGenerator.generate(id, cancelMessage); 53 | } 54 | 55 | @Override 56 | public Optional getConnectionManager() { 57 | return Optional.empty(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/SenderClient.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 4 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 5 | import com.github.mikesafonov.smpp.core.dto.CancelMessageResponse; 6 | import com.github.mikesafonov.smpp.core.dto.Message; 7 | import com.github.mikesafonov.smpp.core.dto.MessageResponse; 8 | import com.github.mikesafonov.smpp.core.exceptions.SenderClientBindException; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.util.Optional; 12 | 13 | /** 14 | * This interface represents smpp protocol transmitter client 15 | * 16 | * @author Mike Safonov 17 | * @author Mikhail Epatko 18 | */ 19 | public interface SenderClient { 20 | 21 | /** 22 | * @return Identifier of response client 23 | */ 24 | @NotNull String getId(); 25 | 26 | /** 27 | * Connect to SMSC via SMPP protocol 28 | * 29 | * @throws SenderClientBindException if connection fails 30 | */ 31 | void setup(); 32 | 33 | /** 34 | * Send message via smpp protocol 35 | * 36 | * @param message sms 37 | * @return message response 38 | */ 39 | @NotNull MessageResponse send(@NotNull Message message); 40 | 41 | /** 42 | * Cancel sms message 43 | * 44 | * @param cancelMessage message to cancel 45 | * @return cancel response 46 | */ 47 | @NotNull CancelMessageResponse cancel(@NotNull CancelMessage cancelMessage); 48 | 49 | /** 50 | * Get Connection Manager 51 | * 52 | * @return {@link ConnectionManager} 53 | */ 54 | @NotNull Optional getConnectionManager(); 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/SilentSubmitSmEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.charset.CharsetUtil; 4 | import com.cloudhopper.smpp.SmppConstants; 5 | import com.cloudhopper.smpp.pdu.SubmitSm; 6 | import com.cloudhopper.smpp.tlv.Tlv; 7 | import com.github.mikesafonov.smpp.core.dto.Message; 8 | import com.github.mikesafonov.smpp.core.utils.CountWithEncoding; 9 | import com.github.mikesafonov.smpp.core.utils.MessageUtil; 10 | import lombok.SneakyThrows; 11 | 12 | /** 13 | * Encoder for silent message 14 | * 15 | * @author Mike Safonov 16 | */ 17 | public class SilentSubmitSmEncoder implements SubmitSmEncoder { 18 | 19 | @Override 20 | @SneakyThrows 21 | public void encode(Message message, SubmitSm submitSm, boolean ucs2Only) { 22 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(message.getText(), ucs2Only); 23 | submitSm.setDataCoding(DataCoding.SILENT_CODING); 24 | byte[] messageByte = CharsetUtil.encode(message.getText(), countWithEncoding.getCharset()); 25 | if (countWithEncoding.getCount() > 1) { 26 | submitSm.setShortMessage(new byte[0]); 27 | submitSm.addOptionalParameter(new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, messageByte)); 28 | } else { 29 | submitSm.setShortMessage(messageByte); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/SimpleSubmitSmEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.charset.Charset; 4 | import com.cloudhopper.commons.charset.CharsetUtil; 5 | import com.cloudhopper.smpp.SmppConstants; 6 | import com.cloudhopper.smpp.pdu.SubmitSm; 7 | import com.cloudhopper.smpp.tlv.Tlv; 8 | import com.github.mikesafonov.smpp.core.dto.Message; 9 | import com.github.mikesafonov.smpp.core.utils.CountWithEncoding; 10 | import com.github.mikesafonov.smpp.core.utils.MessageUtil; 11 | import lombok.SneakyThrows; 12 | 13 | import javax.validation.constraints.NotNull; 14 | 15 | /** 16 | * Encoder for simple/datagram messages 17 | * 18 | * @author Mike Safonov 19 | */ 20 | public class SimpleSubmitSmEncoder implements SubmitSmEncoder { 21 | 22 | @Override 23 | @SneakyThrows 24 | public void encode(Message message, SubmitSm submitSm, boolean ucs2Only) { 25 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(message.getText(), ucs2Only); 26 | byte coding = findCoding(countWithEncoding.getCharset()); 27 | submitSm.setDataCoding(coding); 28 | byte[] messageByte = CharsetUtil.encode(message.getText(), countWithEncoding.getCharset()); 29 | if (countWithEncoding.getCount() > 1) { 30 | submitSm.setShortMessage(new byte[0]); 31 | submitSm.addOptionalParameter(new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, messageByte)); 32 | } else { 33 | submitSm.setShortMessage(messageByte); 34 | } 35 | } 36 | 37 | private byte findCoding(@NotNull Charset charset) { 38 | return (charset == CharsetUtil.CHARSET_GSM) ? SmppConstants.DATA_CODING_DEFAULT : 39 | SmppConstants.DATA_CODING_UCS2; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/SubmitSmEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.smpp.pdu.SubmitSm; 4 | import com.github.mikesafonov.smpp.core.dto.Message; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | /** 9 | * This interface represents class for detecting message data coding and encoding message 10 | * text to necessary coding. 11 | * 12 | * @author Mike Safonov 13 | */ 14 | public interface SubmitSmEncoder { 15 | 16 | void encode(@NotNull Message message, @NotNull SubmitSm submitSm, boolean ucs2Only); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/SubmitSmEncoderFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.dto.Message; 4 | 5 | /** 6 | * Factory class for {@link SubmitSmEncoder} 7 | * 8 | * @author Mike Safonov 9 | */ 10 | public class SubmitSmEncoderFactory { 11 | 12 | public SubmitSmEncoder get(Message message) { 13 | switch (message.getMessageType()) { 14 | case SIMPLE: 15 | case DATAGRAM: 16 | return new SimpleSubmitSmEncoder(); 17 | case SILENT: 18 | return new SilentSubmitSmEncoder(); 19 | case FLASH: 20 | return new FlashSubmitSmEncoder(); 21 | default: 22 | throw new RuntimeException("Unable to find encoder for message type " + message.getMessageType()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/TestSenderClient.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 4 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 5 | import com.github.mikesafonov.smpp.core.dto.CancelMessageResponse; 6 | import com.github.mikesafonov.smpp.core.dto.Message; 7 | import com.github.mikesafonov.smpp.core.dto.MessageResponse; 8 | import com.github.mikesafonov.smpp.core.generators.SmppResultGenerator; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | import static java.util.Objects.requireNonNull; 16 | 17 | /** 18 | * Implementation of {@link SenderClient} which should be used for testing purpose. This client 19 | * may provide real smpp connection via incoming implementation of {@link SenderClient}. Every incoming request will 20 | * be redirected to real {@link #senderClient} only if list of allowed phone numbers {@link #allowedPhones} 21 | * contains message destination phone. Otherwise {@link MessageResponse}/{@link CancelMessageResponse} 22 | * will be generated via {@link SmppResultGenerator} 23 | * 24 | * @author Mike Safonov 25 | * @author Mikhail Epatko 26 | */ 27 | public class TestSenderClient implements SenderClient { 28 | 29 | /** 30 | * List of allowed phones to real smpp actions 31 | */ 32 | private final List allowedPhones; 33 | /** 34 | * Real smpp sender client 35 | */ 36 | private final SenderClient senderClient; 37 | /** 38 | * Generator for {@link MessageResponse} 39 | */ 40 | private final SmppResultGenerator smppResultGenerator; 41 | 42 | public TestSenderClient(@NotNull SenderClient senderClient, @NotNull List allowedPhones, 43 | @NotNull SmppResultGenerator smppResultGenerator) { 44 | this.senderClient = requireNonNull(senderClient); 45 | this.allowedPhones = new ArrayList<>(allowedPhones); 46 | this.smppResultGenerator = requireNonNull(smppResultGenerator); 47 | } 48 | 49 | @Override 50 | public @NotNull String getId() { 51 | return senderClient.getId(); 52 | } 53 | 54 | @Override 55 | public void setup() { 56 | senderClient.setup(); 57 | } 58 | 59 | @Override 60 | public MessageResponse send(Message message) { 61 | if (isAllowed(message.getMsisdn())) { 62 | return senderClient.send(message); 63 | } 64 | return smppResultGenerator.generate(senderClient.getId(), message); 65 | } 66 | 67 | @Override 68 | public @NotNull CancelMessageResponse cancel(@NotNull CancelMessage cancelMessage) { 69 | if (isAllowed(cancelMessage.getMsisdn())) { 70 | return senderClient.cancel(cancelMessage); 71 | } 72 | return smppResultGenerator.generate(senderClient.getId(), cancelMessage); 73 | } 74 | 75 | @Override 76 | public Optional getConnectionManager() { 77 | return senderClient.getConnectionManager(); 78 | } 79 | 80 | private boolean isAllowed(String phone) { 81 | return allowedPhones.contains(phone); 82 | } 83 | 84 | public List getAllowedPhones() { 85 | return allowedPhones; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/TypeOfAddressParser.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.gsm.TypeOfAddress; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | /** 8 | * Interface represents parser of TON and NPI parameters for source and destination address of message. 9 | * 10 | * @author Mike Safonov 11 | */ 12 | public interface TypeOfAddressParser { 13 | 14 | /** 15 | * Detect ton and npi parameters for message source address 16 | * 17 | * @param source message source 18 | * @return ton and npi for message source 19 | */ 20 | @NotNull TypeOfAddress getSource(@NotNull String source); 21 | 22 | /** 23 | * Detect ton and npi parameters for message destination address 24 | * 25 | * @param destination message destination 26 | * @return ton and npi for message destination 27 | */ 28 | @NotNull TypeOfAddress getDestination(@NotNull String destination); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/sender/UnknownTypeOfAddressParser.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.gsm.Npi; 4 | import com.cloudhopper.commons.gsm.Ton; 5 | import com.cloudhopper.commons.gsm.TypeOfAddress; 6 | 7 | import javax.validation.constraints.NotNull; 8 | 9 | /** 10 | * This implementation always returns {@link Ton#UNKNOWN} and {@link Npi#UNKNOWN} 11 | * 12 | * @author Mike Safonov 13 | */ 14 | public class UnknownTypeOfAddressParser implements TypeOfAddressParser { 15 | @Override 16 | public @NotNull TypeOfAddress getSource(@NotNull String source) { 17 | return new TypeOfAddress(Ton.UNKNOWN, Npi.UNKNOWN); 18 | } 19 | 20 | @Override 21 | public @NotNull TypeOfAddress getDestination(@NotNull String destination) { 22 | return new TypeOfAddress(Ton.UNKNOWN, Npi.UNKNOWN); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/utils/CountWithEncoding.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.utils; 2 | 3 | import com.cloudhopper.commons.charset.Charset; 4 | import lombok.Value; 5 | 6 | /** 7 | * @author Mike Safonov 8 | */ 9 | @Value 10 | public class CountWithEncoding { 11 | private final int count; 12 | private final Charset charset; 13 | 14 | public static CountWithEncoding empty(Charset charset){ 15 | return new CountWithEncoding(0, charset); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/utils/JodaJavaConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.utils; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.joda.time.DateTime; 5 | import org.springframework.lang.Nullable; 6 | 7 | import java.time.Instant; 8 | import java.time.ZoneId; 9 | import java.time.ZonedDateTime; 10 | 11 | /** 12 | * @author Mike Safonov 13 | */ 14 | @UtilityClass 15 | public class JodaJavaConverter { 16 | 17 | @Nullable 18 | public static ZonedDateTime convert(@Nullable DateTime dateTime) { 19 | if (dateTime == null) { 20 | return null; 21 | } 22 | 23 | Instant instant = Instant.ofEpochMilli(dateTime.toInstant().getMillis()); 24 | ZoneId zoneId = ZoneId.of(dateTime.getZone().getID()); 25 | return instant.atZone(zoneId); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/utils/MessageUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.utils; 2 | 3 | import com.cloudhopper.commons.charset.CharsetUtil; 4 | import lombok.experimental.UtilityClass; 5 | import org.springframework.lang.Nullable; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import java.util.regex.Pattern; 9 | 10 | /** 11 | * @author Mike Safonov 12 | */ 13 | @UtilityClass 14 | public class MessageUtil { 15 | 16 | private static final Pattern PATTERN = Pattern.compile("^[A-Za-z0-9 \\r\\n@£$¥èéùìòÇØøÅå\u0394_\u03A6\u0393\u039B\u03A9\u03A0\u03A8\u03A3\u0398\u039EÆæßÉ!\"#$%&'()*+,\\-./:;<=>?¡ÄÖÑܧ¿äöñüà^{}\\\\\\[~\\]|\u20AC]*$"); 17 | 18 | public static final int UCS_2_REGULAR_MESSAGE_LENGTH = 70; 19 | public static final int UCS_2_MULTIPART_MESSAGE_LENGTH = 67; 20 | public static final int GSM_7_REGULAR_MESSAGE_LENGTH = 160; 21 | public static final int GSM_7_MULTIPART_MESSAGE_LENGTH = 153; 22 | 23 | /** 24 | * Calculate count of sms message parts need to delivery with message text {@code message}. If {@code ucs2Only = true} 25 | * then message calculated in UCS2 encoding, otherwise encoding detecting according to message text 26 | * 27 | * @param message message text 28 | * @param ucs2Only using UCS2 encoding only 29 | * @return count sms parts 30 | */ 31 | public static CountWithEncoding calculateCountSMS(@Nullable String message, boolean ucs2Only) { 32 | if (isNullOrBlank(message)) { 33 | return CountWithEncoding.empty(CharsetUtil.CHARSET_UCS_2); 34 | } 35 | if (ucs2Only) { 36 | return new CountWithEncoding(countUcs2(message.length()), CharsetUtil.CHARSET_UCS_2); 37 | } else { 38 | return (isGsm(message)) ? new CountWithEncoding(countGsm(message.length()), CharsetUtil.CHARSET_GSM) : 39 | new CountWithEncoding(countUcs2(message.length()), CharsetUtil.CHARSET_UCS_2); 40 | } 41 | } 42 | 43 | /** 44 | * Calculate count of sms message parts need to delivery with message text {@code message}. 45 | * Encoding of sms detecting according to message text 46 | * 47 | * @param message message text 48 | * @return count sms parts 49 | */ 50 | public static CountWithEncoding calculateCountSMS(@Nullable String message) { 51 | return calculateCountSMS(message, false); 52 | } 53 | 54 | private static boolean isNullOrBlank(String value) { 55 | return value == null || value.trim().isEmpty(); 56 | } 57 | 58 | private static boolean isGsm(@NotNull String message) { 59 | return PATTERN.matcher(message).matches(); 60 | } 61 | 62 | private static int countUcs2(int length) { 63 | return countFragments(length, UCS_2_REGULAR_MESSAGE_LENGTH, UCS_2_MULTIPART_MESSAGE_LENGTH); 64 | } 65 | 66 | private static int countGsm(int length) { 67 | return countFragments(length, GSM_7_REGULAR_MESSAGE_LENGTH, GSM_7_MULTIPART_MESSAGE_LENGTH); 68 | } 69 | 70 | /** 71 | * Method calculate message parts count of incoming length. 72 | * 73 | * @param length length of incoming 74 | * @param regular size of regular message 75 | * @param multipart size of multipart message 76 | * @return message parts count 77 | */ 78 | private static int countFragments(int length, int regular, int multipart) { 79 | if (length > regular) { 80 | int fragmentsCount = length / multipart; 81 | if (length % multipart > 0) { 82 | fragmentsCount += 1; 83 | } 84 | return fragmentsCount; 85 | 86 | } else { 87 | return 1; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/github/mikesafonov/smpp/core/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.utils; 2 | 3 | import com.github.mikesafonov.smpp.core.exceptions.ClientNameSmppException; 4 | import lombok.experimental.UtilityClass; 5 | 6 | @UtilityClass 7 | public class Utils { 8 | /** 9 | * Check {@code name} not null and not blank 10 | * 11 | * @param name client name 12 | * @throws ClientNameSmppException if name null or blank 13 | */ 14 | public static void validateName(String name) { 15 | if (name == null || name.trim().isEmpty()) { 16 | throw new ClientNameSmppException("Name must not be empty!"); 17 | } 18 | } 19 | 20 | public static T getOrDefault(T obj, T defaultObj) { 21 | if (obj == null) { 22 | return defaultObj; 23 | } 24 | return obj; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 5 | "name": "smpp.defaults.ucs2Only", 6 | "type": "java.lang.Boolean", 7 | "defaultValue": false, 8 | "description": "Using ucs2 encoding only or not" 9 | }, 10 | { 11 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 12 | "name": "smpp.defaults.maxTry", 13 | "type": "java.lang.Integer", 14 | "defaultValue": 5, 15 | "description": "Number of attempts to reconnect if smpp session is closed" 16 | }, 17 | { 18 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 19 | "name": "smpp.defaults.connectionMode", 20 | "type": "com.github.mikesafonov.smpp.config.ConnectionMode", 21 | "defaultValue": "STANDARD", 22 | "description": "Client`s connection mode" 23 | }, 24 | { 25 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 26 | "name": "smpp.defaults.connectionType", 27 | "type": "com.github.mikesafonov.smpp.config.ConnectionType", 28 | "defaultValue": "TRANSMITTER_RECEIVER", 29 | "description": "Type of smpp connection" 30 | }, 31 | { 32 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 33 | "name": "smpp.defaults.windowSize", 34 | "type": "java.lang.Integer", 35 | "defaultValue": 90, 36 | "description": "Smpp connection window size" 37 | }, 38 | { 39 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 40 | "name": "smpp.defaults.loggingPdu", 41 | "type": "java.lang.Boolean", 42 | "defaultValue": false, 43 | "description": "Is logging smpp pdu" 44 | }, 45 | { 46 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 47 | "name": "smpp.defaults.loggingBytes", 48 | "type": "java.lang.Boolean", 49 | "defaultValue": false, 50 | "description": "Is logging smpp bytes" 51 | }, 52 | { 53 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 54 | "name": "smpp.defaults.rebindPeriod", 55 | "type": "java.time.Duration", 56 | "defaultValue": "90s", 57 | "description": "Connection rebind period" 58 | }, 59 | { 60 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 61 | "name": "smpp.defaults.requestTimeout", 62 | "type": "java.time.Duration", 63 | "defaultValue": "5s", 64 | "description": "Request timeout" 65 | }, 66 | { 67 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 68 | "name": "smpp.defaults.allowedPhones", 69 | "type": "java.lang.String[]", 70 | "defaultValue": [], 71 | "description": "Array of phones to send. Using only if connectionMode is TEST" 72 | }, 73 | { 74 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 75 | "name": "smpp.connections", 76 | "type": "java.util.Map", 77 | "description": "Map of SMSC connections" 78 | }, 79 | { 80 | "sourceType": "com.github.mikesafonov.smpp.config.SmppProperties", 81 | "name": "smpp.setupRightAway", 82 | "type": "java.lang.Boolean", 83 | "defaultValue": true, 84 | "description": "Should setup smpp clients after creation and fail fast if connection cant be established" 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.github.mikesafonov.smpp.config.SmppAutoConfiguration 2 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/api/RandomIndexDetectionStrategyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | class RandomIndexDetectionStrategyTest { 8 | @Test 9 | void shouldAlwaysReturnZero() { 10 | RandomIndexDetectionStrategy strategy = new RandomIndexDetectionStrategy(); 11 | assertEquals(0, strategy.next(1)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/api/RoundRobinIndexDetectionStrategyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | class RoundRobinIndexDetectionStrategyTest { 8 | 9 | @Test 10 | void shouldReturnAlwaysZero(){ 11 | RoundRobinIndexDetectionStrategy strategy = new RoundRobinIndexDetectionStrategy(); 12 | assertEquals(0, strategy.next(1)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/api/StrategySenderManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.api; 2 | 3 | 4 | import com.github.mikesafonov.smpp.config.SmscConnection; 5 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.List; 9 | 10 | import static com.github.mikesafonov.smpp.util.Randomizer.randomString; 11 | import static java.util.Arrays.asList; 12 | import static java.util.Collections.emptyList; 13 | import static java.util.stream.Collectors.toList; 14 | import static org.junit.jupiter.api.Assertions.*; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.verifyNoInteractions; 17 | 18 | /** 19 | * @author Mike Safonov 20 | */ 21 | class StrategySenderManagerTest { 22 | 23 | @Test 24 | void shouldThrowNPE() { 25 | assertThrows(NullPointerException.class, () -> new StrategySenderManager(null, mock(IndexDetectionStrategy.class))); 26 | assertThrows(NullPointerException.class, () -> new StrategySenderManager(emptyList(), null)); 27 | } 28 | 29 | @Test 30 | void shouldReturnEmpty() { 31 | IndexDetectionStrategy indexDetectionStrategy = mock(IndexDetectionStrategy.class); 32 | StrategySenderManager strategySenderManager = new StrategySenderManager(emptyList(), indexDetectionStrategy); 33 | 34 | assertThrows(NoSenderClientException.class, () -> strategySenderManager.getByName(randomString())); 35 | verifyNoInteractions(indexDetectionStrategy); 36 | } 37 | 38 | @Test 39 | void shouldReturnNonEmpty() { 40 | IndexDetectionStrategy indexDetectionStrategy = mock(IndexDetectionStrategy.class); 41 | String expectedName = randomString(); 42 | SmscConnection expectedConnection = new SmscConnection(expectedName, mock(SenderClient.class)); 43 | List smscConnections = asList( 44 | expectedConnection, 45 | new SmscConnection(randomString(), mock(SenderClient.class)) 46 | ); 47 | StrategySenderManager strategySenderManager = new StrategySenderManager(smscConnections, indexDetectionStrategy); 48 | 49 | assertThrows(NoSenderClientException.class, () -> strategySenderManager.getByName(randomString())); 50 | assertEquals(expectedConnection.getSenderClient(), strategySenderManager.getByName(expectedName)); 51 | verifyNoInteractions(indexDetectionStrategy); 52 | } 53 | 54 | @Test 55 | void shouldReturnEmptyBecauseNoConnections() { 56 | IndexDetectionStrategy indexDetectionStrategy = mock(IndexDetectionStrategy.class); 57 | StrategySenderManager strategySenderManager = new StrategySenderManager(emptyList(), mock(IndexDetectionStrategy.class)); 58 | 59 | assertThrows(NoSenderClientException.class, strategySenderManager::getClient); 60 | verifyNoInteractions(indexDetectionStrategy); 61 | } 62 | 63 | @Test 64 | void shouldReturnFirstBecauseOnlyOneConnection() { 65 | IndexDetectionStrategy indexDetectionStrategy = mock(IndexDetectionStrategy.class); 66 | SmscConnection expectedConnection = new SmscConnection(randomString(), mock(SenderClient.class)); 67 | List smscConnections = asList( 68 | expectedConnection 69 | ); 70 | StrategySenderManager strategySenderManager = new StrategySenderManager(smscConnections, mock(IndexDetectionStrategy.class)); 71 | 72 | assertEquals(expectedConnection.getSenderClient(), strategySenderManager.getClient()); 73 | verifyNoInteractions(indexDetectionStrategy); 74 | } 75 | 76 | @Test 77 | void shouldReturnNonEmptyClient() { 78 | RandomIndexDetectionStrategy strategy = new RandomIndexDetectionStrategy(); 79 | List smscConnections = asList( 80 | new SmscConnection(randomString(), mock(SenderClient.class)), 81 | new SmscConnection(randomString(), mock(SenderClient.class)) 82 | ); 83 | 84 | StrategySenderManager strategySenderManager = new StrategySenderManager(smscConnections, strategy); 85 | 86 | SenderClient client = strategySenderManager.getClient(); 87 | 88 | assertTrue(smscConnections.stream().map(SmscConnection::getSenderClient).collect(toList()).contains(client)); 89 | 90 | } 91 | 92 | @Test 93 | void shouldReturnNonEmptyClientRoundRobin() { 94 | RoundRobinIndexDetectionStrategy strategy = new RoundRobinIndexDetectionStrategy(); 95 | List smscConnections = asList( 96 | new SmscConnection(randomString(), mock(SenderClient.class)), 97 | new SmscConnection(randomString(), mock(SenderClient.class)) 98 | ); 99 | 100 | StrategySenderManager strategySenderManager = new StrategySenderManager(smscConnections, strategy); 101 | 102 | SenderClient client = strategySenderManager.getClient(); 103 | 104 | assertTrue(smscConnections.stream().map(SmscConnection::getSenderClient).collect(toList()).contains(client)); 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/connection/ReceiverConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppBindType; 4 | import com.github.mikesafonov.smpp.config.SmppProperties; 5 | import com.github.mikesafonov.smpp.util.Randomizer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | class ReceiverConfigurationTest { 12 | 13 | @Test 14 | void shouldThrowNullPointerException() { 15 | assertThrows(NullPointerException.class, () -> 16 | new ReceiverConfiguration(null, new SmppProperties.Credentials(), true, true)); 17 | assertThrows(NullPointerException.class, () -> 18 | new ReceiverConfiguration("asdasd", null, true, true)); 19 | } 20 | 21 | @Test 22 | void shouldCreateCorrectConfiguration() { 23 | String name = Randomizer.randomString(); 24 | SmppProperties.Credentials credentials = Randomizer.randomCredentials(); 25 | boolean loggingBytes = Randomizer.randomBoolean(); 26 | boolean loggingPdu = Randomizer.randomBoolean(); 27 | ReceiverConfiguration receiverConfiguration = new ReceiverConfiguration(name, credentials, loggingBytes, loggingPdu); 28 | 29 | assertEquals(name, receiverConfiguration.getName()); 30 | assertEquals(credentials.getHost(), receiverConfiguration.getHost()); 31 | assertEquals(credentials.getPassword(), receiverConfiguration.getPassword()); 32 | 33 | assertEquals(credentials.getPort(), receiverConfiguration.getPort()); 34 | assertEquals(credentials.getUsername(), receiverConfiguration.getSystemId()); 35 | assertEquals(SmppBindType.RECEIVER, receiverConfiguration.getType()); 36 | assertEquals(loggingBytes, receiverConfiguration.getLoggingOptions().isLogBytesEnabled()); 37 | assertEquals(loggingPdu, receiverConfiguration.getLoggingOptions().isLogPduEnabled()); 38 | assertEquals(String.format("%s host=%s port=%d username=%s", name, credentials.getHost(), 39 | credentials.getPort(), credentials.getUsername()), 40 | receiverConfiguration.configInformation()); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/connection/ReceiverConnectionManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 5 | import com.cloudhopper.smpp.type.SmppTimeoutException; 6 | import com.github.mikesafonov.smpp.core.exceptions.ResponseClientBindException; 7 | import com.github.mikesafonov.smpp.core.reciever.ResponseSmppSessionHandler; 8 | import lombok.SneakyThrows; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Nested; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import java.util.concurrent.ScheduledExecutorService; 14 | import java.util.concurrent.ScheduledFuture; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import static com.github.mikesafonov.smpp.util.Randomizer.randomPositive; 18 | import static com.github.mikesafonov.smpp.util.Randomizer.randomReceiverConfiguration; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertThrows; 21 | import static org.mockito.Mockito.*; 22 | 23 | class ReceiverConnectionManagerTest { 24 | private DefaultSmppClient smppClient; 25 | private ReceiverConfiguration configuration; 26 | private ResponseSmppSessionHandler responseSmppSessionHandler; 27 | private long rebindPeriod; 28 | private ScheduledExecutorService scheduledExecutorService; 29 | private ReceiverConnectionManager connectionManager; 30 | 31 | @BeforeEach 32 | void setUp() { 33 | smppClient = mock(DefaultSmppClient.class); 34 | configuration = randomReceiverConfiguration(); 35 | responseSmppSessionHandler = mock(ResponseSmppSessionHandler.class); 36 | rebindPeriod = randomPositive(10); 37 | scheduledExecutorService = mock(ScheduledExecutorService.class); 38 | connectionManager = new ReceiverConnectionManager(smppClient, configuration, 39 | responseSmppSessionHandler, rebindPeriod, scheduledExecutorService); 40 | } 41 | 42 | @Nested 43 | class GetSession { 44 | @Test 45 | @SneakyThrows 46 | void shouldCreateSession() { 47 | SmppSession session = mock(SmppSession.class); 48 | ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); 49 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 50 | when(scheduledExecutorService.scheduleAtFixedRate( 51 | any(ResponseClientRebindTask.class), eq(5L), eq(rebindPeriod), eq(TimeUnit.SECONDS))) 52 | .thenReturn(scheduledFuture); 53 | 54 | SmppSession smppSession = connectionManager.getSession(); 55 | assertEquals(session, smppSession); 56 | } 57 | 58 | @Test 59 | @SneakyThrows 60 | void shouldNotCreateNewSessionBecauseSessionNotNull() { 61 | SmppSession session = mock(SmppSession.class); 62 | ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); 63 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 64 | when(scheduledExecutorService.scheduleAtFixedRate( 65 | any(ResponseClientRebindTask.class), eq(5L), eq(rebindPeriod), eq(TimeUnit.SECONDS))) 66 | .thenReturn(scheduledFuture); 67 | 68 | connectionManager.getSession(); 69 | connectionManager.getSession(); 70 | 71 | verify(smppClient, times(1)).bind(configuration, responseSmppSessionHandler); 72 | } 73 | 74 | @Test 75 | @SneakyThrows 76 | void shouldThrowSmppSessionExceptionBecauseUnableToConnect() { 77 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenThrow(SmppTimeoutException.class); 78 | 79 | assertThrows(ResponseClientBindException.class, () -> connectionManager.getSession()); 80 | } 81 | 82 | } 83 | 84 | @Nested 85 | class CloseSession { 86 | @Test 87 | @SneakyThrows 88 | void shouldCloseSession() { 89 | SmppSession session = mock(SmppSession.class); 90 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 91 | 92 | connectionManager.getSession(); 93 | connectionManager.destroy(); 94 | verify(session).close(); 95 | verify(session).destroy(); 96 | } 97 | 98 | @Test 99 | @SneakyThrows 100 | void shouldDoNothingBecauseSessionIsNull() { 101 | SmppSession session = mock(SmppSession.class); 102 | when(smppClient.bind(configuration)).thenReturn(session); 103 | 104 | connectionManager.destroy(); 105 | 106 | verifyNoInteractions(session); 107 | } 108 | } 109 | 110 | @Nested 111 | class Destroy { 112 | @Test 113 | void shouldDestroyClient() { 114 | connectionManager.destroy(); 115 | 116 | verify(smppClient).destroy(); 117 | } 118 | 119 | @Test 120 | void shouldShutdownExecutor() { 121 | connectionManager.destroy(); 122 | 123 | verify(scheduledExecutorService).shutdown(); 124 | } 125 | 126 | @Test 127 | @SneakyThrows 128 | void shouldInterruptTaskIfNotNull() { 129 | SmppSession session = mock(SmppSession.class); 130 | ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); 131 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 132 | when(scheduledExecutorService.scheduleAtFixedRate( 133 | any(ResponseClientRebindTask.class), eq(5L), eq(rebindPeriod), eq(TimeUnit.SECONDS))) 134 | .thenReturn(scheduledFuture); 135 | 136 | connectionManager.getSession(); 137 | 138 | connectionManager.destroy(); 139 | 140 | verify(scheduledFuture).cancel(true); 141 | } 142 | 143 | @Test 144 | @SneakyThrows 145 | void shouldCloseSession() { 146 | SmppSession session = mock(SmppSession.class); 147 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 148 | 149 | connectionManager.getSession(); 150 | connectionManager.destroy(); 151 | verify(session).close(); 152 | verify(session).destroy(); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/connection/ResponseClientRebindTaskTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.mockito.Mockito.*; 8 | 9 | class ResponseClientRebindTaskTest { 10 | private SmppSession session; 11 | private SessionReconnector reconnector; 12 | private ResponseClientRebindTask rebindTask; 13 | 14 | @BeforeEach 15 | void setUp() { 16 | session = mock(SmppSession.class); 17 | reconnector = mock(SessionReconnector.class); 18 | rebindTask = new ResponseClientRebindTask(session, reconnector); 19 | } 20 | 21 | @Test 22 | void shouldReconnectWhenSessionIsNull() { 23 | rebindTask = new ResponseClientRebindTask(null, reconnector); 24 | 25 | rebindTask.run(); 26 | 27 | verify(reconnector).reconnect(); 28 | } 29 | 30 | @Test 31 | void shouldReconnectWhenSessionIsBound(){ 32 | when(session.isBound()).thenReturn(true); 33 | 34 | rebindTask.run(); 35 | 36 | verify(reconnector).reconnect(); 37 | } 38 | 39 | @Test 40 | void shouldReconnectWhenSessionIsNotBoundAndIsNotBinding(){ 41 | when(session.isBound()).thenReturn(false); 42 | when(session.isBinding()).thenReturn(false); 43 | 44 | rebindTask.run(); 45 | 46 | verify(reconnector).reconnect(); 47 | } 48 | 49 | @Test 50 | void shouldNotReconnectWhenSessionIsNotBinding(){ 51 | when(session.isBound()).thenReturn(false); 52 | when(session.isBinding()).thenReturn(true); 53 | 54 | rebindTask.run(); 55 | 56 | verifyNoInteractions(reconnector); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/connection/TransceiverConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppBindType; 4 | import com.github.mikesafonov.smpp.config.SmppProperties; 5 | import com.github.mikesafonov.smpp.util.Randomizer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | class TransceiverConfigurationTest { 12 | 13 | @Test 14 | void shouldThrowNullPointerException() { 15 | assertThrows(NullPointerException.class, () -> new TransceiverConfiguration(null, 16 | new SmppProperties.Credentials(), true, true, 1, null)); 17 | assertThrows(NullPointerException.class, () -> new TransceiverConfiguration("asdasd", 18 | null, true, true, 1, null)); 19 | } 20 | 21 | @Test 22 | void shouldCreateCorrectConfiguration() { 23 | String name = Randomizer.randomString(); 24 | SmppProperties.Credentials credentials = Randomizer.randomCredentials(); 25 | boolean loggingBytes = Randomizer.randomBoolean(); 26 | boolean loggingPdu = Randomizer.randomBoolean(); 27 | int windowsSize = Randomizer.randomInt(); 28 | TransceiverConfiguration configuration = 29 | new TransceiverConfiguration(name, credentials, loggingBytes, loggingPdu, windowsSize, null); 30 | 31 | assertEquals(name, configuration.getName()); 32 | assertEquals(credentials.getHost(), configuration.getHost()); 33 | assertEquals(credentials.getPassword(), configuration.getPassword()); 34 | 35 | assertEquals(credentials.getPort(), configuration.getPort()); 36 | assertEquals(credentials.getUsername(), configuration.getSystemId()); 37 | assertEquals(SmppBindType.TRANSCEIVER, configuration.getType()); 38 | assertEquals(windowsSize, configuration.getWindowSize()); 39 | assertEquals(loggingBytes, configuration.getLoggingOptions().isLogBytesEnabled()); 40 | assertEquals(loggingPdu, configuration.getLoggingOptions().isLogPduEnabled()); 41 | assertEquals(String.format("%s host=%s port=%d username=%s windowsSize=%d", name, credentials.getHost(), 42 | credentials.getPort(), credentials.getUsername(), windowsSize), 43 | configuration.configInformation()); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/connection/TransceiverConnectionManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 5 | import com.cloudhopper.smpp.pdu.EnquireLink; 6 | import com.cloudhopper.smpp.pdu.EnquireLinkResp; 7 | import com.github.mikesafonov.smpp.core.exceptions.SmppException; 8 | import com.github.mikesafonov.smpp.core.reciever.ResponseSmppSessionHandler; 9 | import lombok.SneakyThrows; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Nested; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import static com.github.mikesafonov.smpp.util.Randomizer.randomPositive; 15 | import static com.github.mikesafonov.smpp.util.Randomizer.randomTransceiverConfiguration; 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.junit.jupiter.api.Assertions.assertThrows; 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.ArgumentMatchers.anyLong; 20 | import static org.mockito.Mockito.*; 21 | 22 | class TransceiverConnectionManagerTest { 23 | private DefaultSmppClient smppClient; 24 | private TransceiverConfiguration configuration; 25 | private ResponseSmppSessionHandler responseSmppSessionHandler; 26 | private int maxTryCount; 27 | private TransceiverConnectionManager connectionManager; 28 | 29 | @BeforeEach 30 | void setUp() { 31 | smppClient = mock(DefaultSmppClient.class); 32 | configuration = randomTransceiverConfiguration(); 33 | maxTryCount = randomPositive(5); 34 | responseSmppSessionHandler = mock(ResponseSmppSessionHandler.class); 35 | connectionManager = new TransceiverConnectionManager(smppClient, configuration, 36 | responseSmppSessionHandler, maxTryCount); 37 | } 38 | 39 | @Nested 40 | class GetSession { 41 | @Test 42 | @SneakyThrows 43 | void shouldCreateSession() { 44 | SmppSession session = mock(SmppSession.class); 45 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 46 | 47 | SmppSession smppSession = connectionManager.getSession(); 48 | 49 | assertEquals(session, smppSession); 50 | } 51 | 52 | @Test 53 | @SneakyThrows 54 | void shouldReturnCurrentSessionWhenPingIsOk() { 55 | SmppSession session = mock(SmppSession.class); 56 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 57 | when(session.isBound()).thenReturn(true); 58 | when(session.enquireLink(any(EnquireLink.class), anyLong())).thenReturn(new EnquireLinkResp()); 59 | 60 | connectionManager.getSession(); 61 | SmppSession smppSession = connectionManager.getSession(); 62 | 63 | assertEquals(session, smppSession); 64 | } 65 | 66 | @Test 67 | @SneakyThrows 68 | void shouldReconnectWhenPingIsFails() { 69 | SmppSession session = mock(SmppSession.class); 70 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 71 | when(session.isBound()).thenReturn(true); 72 | when(session.enquireLink(any(EnquireLink.class), anyLong())).thenThrow(RuntimeException.class); 73 | 74 | connectionManager.getSession(); 75 | connectionManager.getSession(); 76 | 77 | verify(session).close(); 78 | verify(session).destroy(); 79 | } 80 | 81 | @Test 82 | @SneakyThrows 83 | void shouldReconnectWhenNotBoundAndNotBinding() { 84 | SmppSession session = mock(SmppSession.class); 85 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 86 | when(session.isBound()).thenReturn(false); 87 | when(session.isBinding()).thenReturn(false); 88 | 89 | connectionManager.getSession(); 90 | connectionManager.getSession(); 91 | 92 | verify(session).close(); 93 | verify(session).destroy(); 94 | } 95 | 96 | @Test 97 | @SneakyThrows 98 | void shouldThrowSmppSessionExceptionBecauseUnableToConnect() { 99 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenThrow(RuntimeException.class); 100 | 101 | assertThrows(SmppException.class, () -> connectionManager.getSession()); 102 | 103 | verify(smppClient, times(maxTryCount)).bind(configuration, responseSmppSessionHandler); 104 | } 105 | } 106 | 107 | @Nested 108 | class CloseSession { 109 | @Test 110 | @SneakyThrows 111 | void shouldCloseSession() { 112 | SmppSession session = mock(SmppSession.class); 113 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 114 | 115 | connectionManager.getSession(); 116 | connectionManager.destroy(); 117 | verify(session).close(); 118 | verify(session).destroy(); 119 | } 120 | 121 | @Test 122 | @SneakyThrows 123 | void shouldDoNothingBecauseSessionIsNull() { 124 | SmppSession session = mock(SmppSession.class); 125 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 126 | 127 | connectionManager.destroy(); 128 | 129 | verifyNoInteractions(session); 130 | } 131 | } 132 | 133 | @Nested 134 | class Destroy { 135 | @Test 136 | void shouldDestroyClient() { 137 | connectionManager.destroy(); 138 | 139 | verify(smppClient).destroy(); 140 | } 141 | 142 | @Test 143 | @SneakyThrows 144 | void shouldCloseSession() { 145 | SmppSession session = mock(SmppSession.class); 146 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 147 | 148 | connectionManager.getSession(); 149 | connectionManager.destroy(); 150 | verify(session).close(); 151 | verify(session).destroy(); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/connection/TransmitterConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppBindType; 4 | import com.github.mikesafonov.smpp.config.SmppProperties; 5 | import com.github.mikesafonov.smpp.util.Randomizer; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | class TransmitterConfigurationTest { 12 | 13 | @Test 14 | void shouldThrowNullPointerException() { 15 | assertThrows(NullPointerException.class, () -> new TransmitterConfiguration(null, new SmppProperties.Credentials(), true, true, 1, null)); 16 | assertThrows(NullPointerException.class, () -> new TransmitterConfiguration("asdasd", null, true, true, 1, null)); 17 | } 18 | 19 | @Test 20 | void shouldCreateCorrectConfiguration() { 21 | String name = Randomizer.randomString(); 22 | SmppProperties.Credentials credentials = Randomizer.randomCredentials(); 23 | boolean loggingBytes = Randomizer.randomBoolean(); 24 | boolean loggingPdu = Randomizer.randomBoolean(); 25 | int windowsSize = Randomizer.randomInt(); 26 | TransmitterConfiguration transmitterConfiguration = new TransmitterConfiguration(name, credentials, loggingBytes, loggingPdu, windowsSize, null); 27 | 28 | assertEquals(name, transmitterConfiguration.getName()); 29 | assertEquals(credentials.getHost(), transmitterConfiguration.getHost()); 30 | assertEquals(credentials.getPassword(), transmitterConfiguration.getPassword()); 31 | 32 | assertEquals(credentials.getPort(), transmitterConfiguration.getPort()); 33 | assertEquals(credentials.getUsername(), transmitterConfiguration.getSystemId()); 34 | assertEquals(SmppBindType.TRANSMITTER, transmitterConfiguration.getType()); 35 | assertEquals(windowsSize, transmitterConfiguration.getWindowSize()); 36 | assertEquals(loggingBytes, transmitterConfiguration.getLoggingOptions().isLogBytesEnabled()); 37 | assertEquals(loggingPdu, transmitterConfiguration.getLoggingOptions().isLogPduEnabled()); 38 | assertEquals(String.format("%s host=%s port=%d username=%s windowsSize=%d", name, credentials.getHost(), 39 | credentials.getPort(), credentials.getUsername(), windowsSize), 40 | transmitterConfiguration.configInformation()); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/connection/TransmitterConnectionManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.connection; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import com.cloudhopper.smpp.impl.DefaultSmppClient; 5 | import com.cloudhopper.smpp.pdu.EnquireLink; 6 | import com.cloudhopper.smpp.pdu.EnquireLinkResp; 7 | import com.github.mikesafonov.smpp.core.exceptions.SmppException; 8 | import com.github.mikesafonov.smpp.core.reciever.ResponseSmppSessionHandler; 9 | import lombok.SneakyThrows; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Nested; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import static com.github.mikesafonov.smpp.util.Randomizer.randomPositive; 15 | import static com.github.mikesafonov.smpp.util.Randomizer.randomTransmitterConfiguration; 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.junit.jupiter.api.Assertions.assertThrows; 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.ArgumentMatchers.anyLong; 20 | import static org.mockito.Mockito.*; 21 | 22 | class TransmitterConnectionManagerTest { 23 | private DefaultSmppClient smppClient; 24 | private TransmitterConfiguration configuration; 25 | private ResponseSmppSessionHandler responseSmppSessionHandler = null; 26 | private int maxTryCount; 27 | private TransmitterConnectionManager connectionManager; 28 | 29 | @BeforeEach 30 | void setUp() { 31 | smppClient = mock(DefaultSmppClient.class); 32 | configuration = randomTransmitterConfiguration(); 33 | maxTryCount = randomPositive(5); 34 | connectionManager = new TransmitterConnectionManager(smppClient, configuration, maxTryCount); 35 | } 36 | 37 | @Nested 38 | class GetSession { 39 | @Test 40 | @SneakyThrows 41 | void shouldCreateSession() { 42 | SmppSession session = mock(SmppSession.class); 43 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 44 | 45 | SmppSession smppSession = connectionManager.getSession(); 46 | 47 | assertEquals(session, smppSession); 48 | } 49 | 50 | @Test 51 | @SneakyThrows 52 | void shouldReturnCurrentSessionWhenPingIsOk() { 53 | SmppSession session = mock(SmppSession.class); 54 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 55 | when(session.isBound()).thenReturn(true); 56 | when(session.enquireLink(any(EnquireLink.class), anyLong())).thenReturn(new EnquireLinkResp()); 57 | 58 | connectionManager.getSession(); 59 | SmppSession smppSession = connectionManager.getSession(); 60 | 61 | assertEquals(session, smppSession); 62 | } 63 | 64 | @Test 65 | @SneakyThrows 66 | void shouldReconnectWhenPingIsFails() { 67 | SmppSession session = mock(SmppSession.class); 68 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 69 | when(session.isBound()).thenReturn(true); 70 | when(session.enquireLink(any(EnquireLink.class), anyLong())).thenThrow(RuntimeException.class); 71 | 72 | connectionManager.getSession(); 73 | connectionManager.getSession(); 74 | 75 | verify(session).close(); 76 | verify(session).destroy(); 77 | } 78 | 79 | @Test 80 | @SneakyThrows 81 | void shouldReconnectWhenNotBoundAndNotBinding() { 82 | SmppSession session = mock(SmppSession.class); 83 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 84 | when(session.isBound()).thenReturn(false); 85 | when(session.isBinding()).thenReturn(false); 86 | 87 | connectionManager.getSession(); 88 | connectionManager.getSession(); 89 | 90 | verify(session).close(); 91 | verify(session).destroy(); 92 | } 93 | 94 | @Test 95 | @SneakyThrows 96 | void shouldThrowSmppSessionExceptionBecauseUnableToConnect() { 97 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenThrow(RuntimeException.class); 98 | 99 | assertThrows(SmppException.class, () -> connectionManager.getSession()); 100 | 101 | verify(smppClient, times(maxTryCount)).bind(configuration, responseSmppSessionHandler); 102 | } 103 | } 104 | 105 | @Nested 106 | class CloseSession { 107 | @Test 108 | @SneakyThrows 109 | void shouldCloseSession() { 110 | SmppSession session = mock(SmppSession.class); 111 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 112 | 113 | connectionManager.getSession(); 114 | connectionManager.destroy(); 115 | verify(session).close(); 116 | verify(session).destroy(); 117 | } 118 | 119 | @Test 120 | @SneakyThrows 121 | void shouldDoNothingBecauseSessionIsNull() { 122 | SmppSession session = mock(SmppSession.class); 123 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 124 | 125 | connectionManager.destroy(); 126 | 127 | verifyNoInteractions(session); 128 | } 129 | } 130 | 131 | @Nested 132 | class Destroy { 133 | @Test 134 | void shouldDestroyClient() { 135 | connectionManager.destroy(); 136 | 137 | verify(smppClient).destroy(); 138 | } 139 | 140 | @Test 141 | @SneakyThrows 142 | void shouldCloseSession() { 143 | SmppSession session = mock(SmppSession.class); 144 | when(smppClient.bind(configuration, responseSmppSessionHandler)).thenReturn(session); 145 | 146 | connectionManager.getSession(); 147 | connectionManager.destroy(); 148 | verify(session).close(); 149 | verify(session).destroy(); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/dto/DeliveryReportTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import com.cloudhopper.smpp.SmppConstants; 4 | import com.cloudhopper.smpp.util.DeliveryReceipt; 5 | import com.cloudhopper.smpp.util.DeliveryReceiptException; 6 | import org.joda.time.DateTimeZone; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.time.LocalDateTime; 10 | import java.time.ZoneId; 11 | import java.time.ZonedDateTime; 12 | 13 | import static com.github.mikesafonov.smpp.util.Randomizer.randomString; 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | 16 | class DeliveryReportTest { 17 | @Test 18 | void shouldParseDeliveryReport() throws DeliveryReceiptException { 19 | String responseClientId = randomString(); 20 | String receipt0 = "id:0123456789 sub:002 dlvrd:001 submit date:1005232039 done date:1005242339 stat:DELIVRD err:012 text:This is a sample mes"; 21 | DeliveryReceipt dlr = DeliveryReceipt.parseShortMessage(receipt0, DateTimeZone.UTC); 22 | 23 | DeliveryReport deliveryReport = DeliveryReport.of(dlr, responseClientId); 24 | assertEquals(responseClientId, deliveryReport.getResponseClientId()); 25 | assertEquals("0123456789", deliveryReport.getMessageId()); 26 | assertEquals("This is a sample mes", deliveryReport.getText()); 27 | assertEquals(SmppConstants.STATE_DELIVERED, deliveryReport.getState()); 28 | assertEquals(12, deliveryReport.getErrorCode()); 29 | assertEquals(2, deliveryReport.getSubmitCount()); 30 | assertEquals(1, deliveryReport.getDeliveredCount()); 31 | assertEquals(ZonedDateTime.of(LocalDateTime.of(2010, 5, 23, 20, 39, 0), ZoneId.of(DateTimeZone.UTC.getID())), 32 | deliveryReport.getSubmitDate() 33 | ); 34 | assertEquals(ZonedDateTime.of(LocalDateTime.of(2010, 5, 24, 23, 39, 0), ZoneId.of(DateTimeZone.UTC.getID())), 35 | deliveryReport.getDeliveryDate() 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/dto/MessageBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.dto; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | class MessageBuilderTest { 9 | private String text = "text"; 10 | private String phone = "phone"; 11 | private String source = "source"; 12 | private String messageId = "message_id"; 13 | private String validityPeriod = "000007000000000R"; 14 | 15 | @Test 16 | void shouldBuildSilentMessage() { 17 | Message message = Message.silent(text) 18 | .from(source) 19 | .to(phone) 20 | .messageId(messageId) 21 | .validityPeriod(validityPeriod) 22 | .build(); 23 | 24 | assertTrue(message.isSilent()); 25 | assertEquals(MessageType.SILENT, message.getMessageType()); 26 | assertEquals(text, message.getText()); 27 | assertEquals(phone, message.getMsisdn()); 28 | assertEquals(source, message.getSource()); 29 | assertEquals(messageId, message.getMessageId()); 30 | assertEquals(validityPeriod, message.getValidityPeriod()); 31 | } 32 | 33 | @Test 34 | void shouldBuildDatagramMessage() { 35 | Message message = Message.datagram(text) 36 | .from(source) 37 | .to(phone) 38 | .messageId(messageId) 39 | .validityPeriod(validityPeriod) 40 | .build(); 41 | assertEquals(MessageType.DATAGRAM, message.getMessageType()); 42 | assertEquals(text, message.getText()); 43 | assertEquals(phone, message.getMsisdn()); 44 | assertEquals(source, message.getSource()); 45 | assertEquals(messageId, message.getMessageId()); 46 | assertEquals(validityPeriod, message.getValidityPeriod()); 47 | } 48 | 49 | @Test 50 | void shouldBuildSimpleMessage() { 51 | Message message = Message.simple(text) 52 | .from(source) 53 | .to(phone) 54 | .messageId(messageId) 55 | .validityPeriod(validityPeriod) 56 | .build(); 57 | assertEquals(MessageType.SIMPLE, message.getMessageType()); 58 | assertEquals(text, message.getText()); 59 | assertEquals(phone, message.getMsisdn()); 60 | assertEquals(source, message.getSource()); 61 | assertEquals(messageId, message.getMessageId()); 62 | assertEquals(validityPeriod, message.getValidityPeriod()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/generators/AlwaysSuccessSmppResultGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.generators; 2 | 3 | import com.github.mikesafonov.smpp.core.dto.*; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.github.mikesafonov.smpp.util.Randomizer.randomString; 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | /** 10 | * @author Mike Safonov 11 | */ 12 | class AlwaysSuccessSmppResultGeneratorTest { 13 | private AlwaysSuccessSmppResultGenerator generator = new AlwaysSuccessSmppResultGenerator(); 14 | 15 | @Test 16 | void shouldGenerateSuccessResponse() { 17 | 18 | Message message = new Message(randomString(), randomString(), randomString(), randomString(), MessageType.SIMPLE); 19 | String smscId = randomString(); 20 | MessageResponse messageResponse = generator.generate(smscId, message); 21 | 22 | assertEquals(message, messageResponse.getOriginal()); 23 | assertTrue(messageResponse.isSent()); 24 | assertEquals(smscId, messageResponse.getSmscId()); 25 | assertNotNull(messageResponse.getSmscMessageID()); 26 | assertNull(messageResponse.getMessageErrorInformation()); 27 | } 28 | 29 | @Test 30 | void shouldGenerateSuccessCancelResponse() { 31 | CancelMessage cancelMessage = new CancelMessage(randomString(), randomString(), randomString()); 32 | String smscId = randomString(); 33 | CancelMessageResponse messageResponse = generator.generate(smscId, cancelMessage); 34 | 35 | assertEquals(cancelMessage, messageResponse.getOriginal()); 36 | assertTrue(messageResponse.isSuccess()); 37 | assertEquals(smscId, messageResponse.getSmscConnectionId()); 38 | assertNull(messageResponse.getMessageErrorInformation()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/reciever/ResponseClientRebindTaskTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | /** 4 | * @author Mike Safonov 5 | */ 6 | class ResponseClientRebindTaskTest { 7 | 8 | // @Test 9 | // void shouldThrowNPE() { 10 | // assertThrows(NullPointerException.class, () -> new ResponseClientRebindTask(null)); 11 | // } 12 | // 13 | // @Test 14 | // void shouldReconnectIfSessionIsNull() { 15 | // ResponseClient responseClient = mock(ResponseClient.class); 16 | // ResponseClientRebindTask responseClientRebindTask = new ResponseClientRebindTask(responseClient); 17 | // 18 | // when(responseClient.getSession()).thenReturn(null); 19 | // 20 | // responseClientRebindTask.run(); 21 | // 22 | // verify(responseClient, times(1)).reconnect(); 23 | // } 24 | // 25 | // @Test 26 | // void shouldDoNothingIfSessionIsBinding() { 27 | // ResponseClient responseClient = mock(ResponseClient.class); 28 | // SmppSession smppSession = mock(SmppSession.class); 29 | // ResponseClientRebindTask responseClientRebindTask = new ResponseClientRebindTask(responseClient); 30 | // 31 | // when(responseClient.getSession()).thenReturn(smppSession); 32 | // when(smppSession.isBinding()).thenReturn(true); 33 | // when(smppSession.isBound()).thenReturn(false); 34 | // 35 | // responseClientRebindTask.run(); 36 | // 37 | // verify(responseClient, times(0)).reconnect(); 38 | // } 39 | // 40 | // @Test 41 | // void shouldDoNothingIfSessionIsBoundAndClientInProcess() { 42 | // ResponseClient responseClient = mock(ResponseClient.class); 43 | // SmppSession smppSession = mock(SmppSession.class); 44 | // ResponseClientRebindTask responseClientRebindTask = new ResponseClientRebindTask(responseClient); 45 | // 46 | // when(responseClient.getSession()).thenReturn(smppSession); 47 | // when(responseClient.isInProcess()).thenReturn(true); 48 | // when(smppSession.isBinding()).thenReturn(false); 49 | // when(smppSession.isBound()).thenReturn(true); 50 | // 51 | // responseClientRebindTask.run(); 52 | // 53 | // verify(responseClient, times(0)).reconnect(); 54 | // } 55 | // 56 | // @Test 57 | // void shouldReconnectIfSessionIsBoundAndClientNotInProcess() { 58 | // ResponseClient responseClient = mock(ResponseClient.class); 59 | // SmppSession smppSession = mock(SmppSession.class); 60 | // ResponseClientRebindTask responseClientRebindTask = new ResponseClientRebindTask(responseClient); 61 | // 62 | // when(responseClient.getSession()).thenReturn(smppSession); 63 | // when(responseClient.isInProcess()).thenReturn(false); 64 | // when(smppSession.isBinding()).thenReturn(false); 65 | // when(smppSession.isBound()).thenReturn(true); 66 | // 67 | // responseClientRebindTask.run(); 68 | // 69 | // verify(responseClient, times(1)).reconnect(); 70 | // } 71 | // 72 | // @Test 73 | // void shouldReconnectIfSessionNotBoundAndNotBinding() { 74 | // ResponseClient responseClient = mock(ResponseClient.class); 75 | // SmppSession smppSession = mock(SmppSession.class); 76 | // ResponseClientRebindTask responseClientRebindTask = new ResponseClientRebindTask(responseClient); 77 | // 78 | // when(responseClient.getSession()).thenReturn(smppSession); 79 | // when(smppSession.isBinding()).thenReturn(false); 80 | // when(smppSession.isBound()).thenReturn(false); 81 | // 82 | // responseClientRebindTask.run(); 83 | // 84 | // verify(responseClient, times(1)).reconnect(); 85 | // } 86 | // 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/reciever/ResponseSmppSessionHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | import com.cloudhopper.smpp.pdu.DeliverSm; 4 | import com.cloudhopper.smpp.pdu.EnquireLink; 5 | import com.cloudhopper.smpp.pdu.PduResponse; 6 | import com.cloudhopper.smpp.type.SmppInvalidArgumentException; 7 | import com.cloudhopper.smpp.util.DeliveryReceipt; 8 | import com.cloudhopper.smpp.util.DeliveryReceiptException; 9 | import com.github.mikesafonov.smpp.core.dto.DeliveryReport; 10 | import com.github.mikesafonov.smpp.util.Randomizer; 11 | import org.joda.time.DateTimeZone; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.util.Arrays; 16 | import java.util.List; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.mockito.Mockito.*; 21 | 22 | /** 23 | * @author Mike Safonov 24 | */ 25 | class ResponseSmppSessionHandlerTest { 26 | private String clientId; 27 | private DeliveryReportConsumer deliveryReportConsumer; 28 | private ResponseSmppSessionHandler responseSmppSessionHandler; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | clientId = Randomizer.randomString(); 33 | deliveryReportConsumer = mock(DeliveryReportConsumer.class); 34 | responseSmppSessionHandler = new ResponseSmppSessionHandler(clientId, Arrays.asList(deliveryReportConsumer)); 35 | } 36 | 37 | @Test 38 | void shouldThrowNPE() { 39 | assertThrows(NullPointerException.class, () -> new ResponseSmppSessionHandler(null, mock(List.class))); 40 | assertThrows(NullPointerException.class, () -> new ResponseSmppSessionHandler(clientId, null)); 41 | } 42 | 43 | 44 | @Test 45 | void shouldDoNothingBecauseRequestIsNotDeliveryReceipt() { 46 | EnquireLink enquireLink = new EnquireLink(); 47 | responseSmppSessionHandler.firePduRequestReceived(enquireLink); 48 | 49 | verifyNoInteractions(deliveryReportConsumer); 50 | } 51 | 52 | @Test 53 | void shouldHandleRequest() throws SmppInvalidArgumentException, DeliveryReceiptException { 54 | String dlSms = "id:261BD3E2 sub:001 dlvrd:001 submit date:190305131326 done date:190305131326 stat:DELIVRD err:0 Text:report"; 55 | DeliverSm deliverSm = new DeliverSm(); 56 | deliverSm.setShortMessage(dlSms.getBytes()); 57 | DeliveryReport deliveryReport = DeliveryReport.of(DeliveryReceipt.parseShortMessage(dlSms, DateTimeZone.UTC), clientId); 58 | PduResponse pduResponse = responseSmppSessionHandler.firePduRequestReceived(deliverSm); 59 | 60 | verify(deliveryReportConsumer, times(1)).accept(deliveryReport); 61 | 62 | assertThat(pduResponse.toString()).isEqualTo(deliverSm.createResponse().toString()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/reciever/StandardResponseClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.reciever; 2 | 3 | import com.cloudhopper.smpp.SmppSession; 4 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 5 | import com.github.mikesafonov.smpp.core.connection.ReceiverConfiguration; 6 | import com.github.mikesafonov.smpp.core.exceptions.ResponseClientBindException; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static com.github.mikesafonov.smpp.util.Randomizer.randomReceiverConfiguration; 11 | import static java.lang.String.format; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | import static org.mockito.Mockito.*; 15 | 16 | /** 17 | * @author Mike Safonov 18 | */ 19 | class StandardResponseClientTest { 20 | private ConnectionManager connectionManager; 21 | private StandardResponseClient responseClient; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | connectionManager = mock(ConnectionManager.class); 26 | responseClient = new StandardResponseClient(connectionManager); 27 | } 28 | 29 | @Test 30 | void shouldThrowNPE() { 31 | assertThrows(NullPointerException.class, () -> 32 | new StandardResponseClient(null)); 33 | } 34 | 35 | @Test 36 | void shouldContainExpectedId() { 37 | 38 | ReceiverConfiguration receiverConfiguration = randomReceiverConfiguration(); 39 | 40 | when(connectionManager.getConfiguration()).thenReturn(receiverConfiguration); 41 | 42 | assertEquals(receiverConfiguration.getName(), responseClient.getId()); 43 | } 44 | 45 | @Test 46 | void shouldFailSetup() { 47 | ReceiverConfiguration receiverConfiguration = randomReceiverConfiguration(); 48 | 49 | when(connectionManager.getSession()).thenThrow(new ResponseClientBindException(format("Unable to bind with configuration: %s ", 50 | receiverConfiguration.configInformation()))); 51 | 52 | ResponseClientBindException exception = assertThrows(ResponseClientBindException.class, responseClient::setup); 53 | assertEquals(format("Unable to bind with configuration: %s ", receiverConfiguration.configInformation()), 54 | exception.getMessage()); 55 | } 56 | 57 | @Test 58 | void shouldSuccessSetup() { 59 | SmppSession session = mock(SmppSession.class); 60 | 61 | when(connectionManager.getSession()).thenReturn(session); 62 | 63 | responseClient.setup(); 64 | 65 | assertEquals(session, responseClient.getSession()); 66 | } 67 | 68 | @Test 69 | void shouldSuccessReconnect() { 70 | responseClient.reconnect(); 71 | 72 | verify(connectionManager).closeSession(); 73 | verify(connectionManager).getSession(); 74 | } 75 | 76 | @Test 77 | void shouldDestroy() { 78 | responseClient.destroyClient(); 79 | 80 | verify(connectionManager).destroy(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/AddressBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.gsm.Npi; 4 | import com.cloudhopper.commons.gsm.Ton; 5 | import com.cloudhopper.commons.gsm.TypeOfAddress; 6 | import com.cloudhopper.smpp.type.Address; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | import static org.mockito.ArgumentMatchers.anyString; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.when; 15 | 16 | /** 17 | * @author Mike Safonov 18 | */ 19 | class AddressBuilderTest { 20 | 21 | private AddressBuilder addressBuilder; 22 | private TypeOfAddressParser addressParser; 23 | 24 | @BeforeEach 25 | void setUp() { 26 | addressParser = mock(TypeOfAddressParser.class); 27 | addressBuilder = new AddressBuilder(addressParser); 28 | 29 | } 30 | 31 | @Test 32 | void shouldThrowNullPointerException() { 33 | assertThrows(NullPointerException.class, () -> new AddressBuilder(null)); 34 | } 35 | 36 | @Test 37 | void shouldThrow() { 38 | when(addressParser.getSource(anyString())).thenThrow(RuntimeException.class); 39 | when(addressParser.getDestination(anyString())).thenThrow(RuntimeException.class); 40 | 41 | assertThrows(RuntimeException.class, () -> addressBuilder.createSourceAddress("string")); 42 | assertThrows(RuntimeException.class, () -> addressBuilder.createDestinationAddress("string")); 43 | } 44 | 45 | @Test 46 | void shouldReturnExceptedSourceAddress(){ 47 | TypeOfAddress typeOfAddress = new TypeOfAddress(Ton.INTERNATIONAL, Npi.ISDN); 48 | when(addressParser.getSource(anyString())).thenReturn(typeOfAddress); 49 | 50 | String value = "asdads"; 51 | Address sourceAddress = addressBuilder.createSourceAddress(value); 52 | 53 | assertEquals(value, sourceAddress.getAddress()); 54 | assertEquals((byte)typeOfAddress.getTon().toInt(), sourceAddress.getTon()); 55 | assertEquals((byte)typeOfAddress.getNpi().toInt(), sourceAddress.getNpi()); 56 | } 57 | 58 | @Test 59 | void shouldReturnExceptedDestinationAddress(){ 60 | TypeOfAddress typeOfAddress = new TypeOfAddress(Ton.INTERNATIONAL, Npi.ISDN); 61 | when(addressParser.getDestination(anyString())).thenReturn(typeOfAddress); 62 | 63 | String value = "asdads"; 64 | Address destinationAddress = addressBuilder.createDestinationAddress(value); 65 | 66 | assertEquals(value, destinationAddress.getAddress()); 67 | assertEquals((byte)typeOfAddress.getTon().toInt(), destinationAddress.getTon()); 68 | assertEquals((byte)typeOfAddress.getNpi().toInt(), destinationAddress.getNpi()); 69 | } 70 | 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/BaseStandardSenderClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | import static com.github.mikesafonov.smpp.util.Randomizer.randomBoolean; 7 | import static com.github.mikesafonov.smpp.util.Randomizer.randomInt; 8 | import static org.mockito.Mockito.mock; 9 | 10 | /** 11 | * @author Mike Safonov 12 | */ 13 | abstract class BaseStandardSenderClientTest { 14 | protected StandardSenderClient senderClient; 15 | protected MessageBuilder messageBuilder; 16 | protected ConnectionManager connectionManager; 17 | 18 | @BeforeEach 19 | void setUp() { 20 | connectionManager = mock(ConnectionManager.class); 21 | messageBuilder = mock(MessageBuilder.class); 22 | senderClient = new StandardSenderClient(connectionManager, randomBoolean(), randomInt(), messageBuilder); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/DefaultTypeOfAddressParserTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.gsm.Npi; 4 | import com.cloudhopper.commons.gsm.Ton; 5 | import com.cloudhopper.commons.gsm.TypeOfAddress; 6 | import com.github.mikesafonov.smpp.core.exceptions.IllegalAddressException; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | 12 | import java.util.List; 13 | 14 | import static java.util.Arrays.asList; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertThrows; 17 | 18 | /** 19 | * @author Mike Safonov 20 | */ 21 | class DefaultTypeOfAddressParserTest { 22 | 23 | private static final TypeOfAddress unknown = new TypeOfAddress(Ton.UNKNOWN, Npi.UNKNOWN); 24 | 25 | private static List invalidSourceDestinationProvider() { 26 | return asList("3805030922353333", "", "$$$$$$", "@fasd", "Прив"); 27 | } 28 | 29 | private static List numericSourceDestinationProvider() { 30 | return asList("380223092235", "780223092235"); 31 | } 32 | 33 | private static List textSourceDestinationProvider() { 34 | return asList("TEST-A", "Test A", "Test_a", "Hello"); 35 | } 36 | 37 | private DefaultTypeOfAddressParser defaultTypeOfAddressParser; 38 | 39 | @BeforeEach 40 | void setUp() { 41 | defaultTypeOfAddressParser = new DefaultTypeOfAddressParser(); 42 | } 43 | 44 | @ParameterizedTest 45 | @MethodSource("invalidSourceDestinationProvider") 46 | void shouldReturnUnknownBecauseSourceInvalid(String invalidSource) { 47 | TypeOfAddress typeOfAddress = defaultTypeOfAddressParser.getSource(invalidSource); 48 | 49 | assertEquals(unknown.getTon(), typeOfAddress.getTon()); 50 | assertEquals(unknown.getNpi(), typeOfAddress.getNpi()); 51 | } 52 | 53 | @ParameterizedTest 54 | @MethodSource("numericSourceDestinationProvider") 55 | void shouldReturnNumericSource(String numericSource) { 56 | TypeOfAddress typeOfAddress = defaultTypeOfAddressParser.getSource(numericSource); 57 | 58 | assertEquals(Ton.INTERNATIONAL, typeOfAddress.getTon()); 59 | assertEquals(Npi.ISDN, typeOfAddress.getNpi()); 60 | } 61 | 62 | @ParameterizedTest 63 | @MethodSource("textSourceDestinationProvider") 64 | void shouldReturnTextSource(String numericSource) { 65 | TypeOfAddress typeOfAddress = defaultTypeOfAddressParser.getSource(numericSource); 66 | 67 | assertEquals(Ton.ALPHANUMERIC, typeOfAddress.getTon()); 68 | assertEquals(Npi.UNKNOWN, typeOfAddress.getNpi()); 69 | } 70 | 71 | @Test 72 | void shouldThrowIllegalSourceAddressException(){ 73 | assertThrows(IllegalAddressException.class, () -> defaultTypeOfAddressParser.getSource(null)); 74 | } 75 | 76 | @ParameterizedTest 77 | @MethodSource("numericSourceDestinationProvider") 78 | void shouldReturnNumericDestination(String numericDestination) { 79 | TypeOfAddress typeOfAddress = defaultTypeOfAddressParser.getDestination(numericDestination); 80 | 81 | assertEquals(Ton.INTERNATIONAL, typeOfAddress.getTon()); 82 | assertEquals(Npi.ISDN, typeOfAddress.getNpi()); 83 | } 84 | 85 | @ParameterizedTest 86 | @MethodSource("textSourceDestinationProvider") 87 | void shouldReturnUnknownDestination(String destination) { 88 | TypeOfAddress typeOfAddress = defaultTypeOfAddressParser.getDestination(destination); 89 | 90 | assertEquals(Ton.UNKNOWN, typeOfAddress.getTon()); 91 | assertEquals(Npi.UNKNOWN, typeOfAddress.getNpi()); 92 | } 93 | 94 | @Test 95 | void shouldThrowIllegalDestinationAddressException(){ 96 | assertThrows(IllegalAddressException.class, () -> defaultTypeOfAddressParser.getDestination(null)); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/MockSenderClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 4 | import com.github.mikesafonov.smpp.core.dto.Message; 5 | import com.github.mikesafonov.smpp.core.dto.MessageType; 6 | import com.github.mikesafonov.smpp.core.generators.AlwaysSuccessSmppResultGenerator; 7 | import com.github.mikesafonov.smpp.core.generators.SmppResultGenerator; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static com.github.mikesafonov.smpp.util.Randomizer.randomString; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | import static org.mockito.Mockito.*; 14 | 15 | /** 16 | * @author Mike Safonov 17 | */ 18 | class MockSenderClientTest { 19 | 20 | @Test 21 | void shouldThrowNPE() { 22 | 23 | assertThrows(NullPointerException.class, () -> new MockSenderClient(null, randomString())); 24 | assertThrows(NullPointerException.class, () -> new MockSenderClient(new AlwaysSuccessSmppResultGenerator(), null)); 25 | } 26 | 27 | @Test 28 | void shouldContainExpectedId() { 29 | String id = randomString(); 30 | MockSenderClient senderClient = new MockSenderClient(new AlwaysSuccessSmppResultGenerator(), id); 31 | 32 | assertEquals(id, senderClient.getId()); 33 | } 34 | 35 | @Test 36 | void shouldCallGenerator() { 37 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 38 | String id = randomString(); 39 | MockSenderClient senderClient = new MockSenderClient(smppResultGenerator, id); 40 | 41 | Message message = new Message(randomString(), randomString(), randomString(), randomString(), MessageType.SIMPLE); 42 | senderClient.send(message); 43 | 44 | verify(smppResultGenerator, times(1)).generate(id, message); 45 | } 46 | 47 | @Test 48 | void shouldCallGeneratorForCancel() { 49 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 50 | String id = randomString(); 51 | MockSenderClient senderClient = new MockSenderClient(smppResultGenerator, id); 52 | 53 | CancelMessage message = new CancelMessage(randomString(), randomString(), randomString()); 54 | senderClient.cancel(message); 55 | 56 | verify(smppResultGenerator, times(1)).generate(id, message); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/StandardSenderClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.connection.TransmitterConfiguration; 4 | import com.github.mikesafonov.smpp.core.exceptions.SenderClientBindException; 5 | import com.github.mikesafonov.smpp.core.exceptions.SmppSessionException; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static com.github.mikesafonov.smpp.util.Randomizer.*; 9 | import static java.lang.String.format; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | import static org.mockito.Mockito.verify; 13 | import static org.mockito.Mockito.when; 14 | 15 | /** 16 | * @author Mike Safonov 17 | */ 18 | class StandardSenderClientTest extends BaseStandardSenderClientTest { 19 | 20 | @Test 21 | void shouldThrowNPE() { 22 | assertThrows(NullPointerException.class, 23 | () -> new StandardSenderClient(connectionManager, randomBoolean(), randomInt(), null)); 24 | assertThrows(NullPointerException.class, 25 | () -> new StandardSenderClient(null, randomBoolean(), randomInt(), new MessageBuilder(new DefaultTypeOfAddressParser()))); 26 | } 27 | 28 | @Test 29 | void shouldContainExpectedId() { 30 | TransmitterConfiguration transmitterConfiguration = randomTransmitterConfiguration(); 31 | when(connectionManager.getConfiguration()).thenReturn(transmitterConfiguration); 32 | 33 | assertEquals(transmitterConfiguration.getName(), senderClient.getId()); 34 | } 35 | 36 | @Test 37 | void shouldThrowSenderClientBindExceptionWhenSetupFailed() { 38 | TransmitterConfiguration transmitterConfiguration = randomTransmitterConfiguration(); 39 | 40 | when(connectionManager.getConfiguration()).thenReturn(transmitterConfiguration); 41 | when(connectionManager.getSession()).thenThrow(SmppSessionException.class); 42 | 43 | String message = assertThrows(SenderClientBindException.class, () -> senderClient.setup()).getMessage(); 44 | assertEquals(format("Unable to bind with configuration: %s ", transmitterConfiguration.configInformation()), message); 45 | } 46 | 47 | @Test 48 | void shouldSuccessSetup() { 49 | senderClient.setup(); 50 | 51 | verify(connectionManager).getSession(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/SubmitSmEncoderFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.dto.Message; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class SubmitSmEncoderFactoryTest { 10 | 11 | private SubmitSmEncoderFactory factory; 12 | 13 | @BeforeEach 14 | void setUp(){ 15 | factory = new SubmitSmEncoderFactory(); 16 | } 17 | 18 | @Test 19 | void shouldReturnSimpleWhenMessageSimple(){ 20 | Message message = Message.simple("").build(); 21 | 22 | assertThat(factory.get(message)).isInstanceOf(SimpleSubmitSmEncoder.class); 23 | } 24 | 25 | 26 | @Test 27 | void shouldReturnSimpleWhenMessageDatagram(){ 28 | Message message = Message.datagram("").build(); 29 | 30 | assertThat(factory.get(message)).isInstanceOf(SimpleSubmitSmEncoder.class); 31 | } 32 | 33 | 34 | @Test 35 | void shouldReturnSilentWhenMessageSilent(){ 36 | Message message = Message.silent("").build(); 37 | 38 | assertThat(factory.get(message)).isInstanceOf(SilentSubmitSmEncoder.class); 39 | } 40 | 41 | 42 | @Test 43 | void shouldReturnFlashWhenMessageFlash(){ 44 | Message message = Message.flash("").build(); 45 | 46 | assertThat(factory.get(message)).isInstanceOf(FlashSubmitSmEncoder.class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/TestSenderClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 4 | import com.github.mikesafonov.smpp.core.dto.Message; 5 | import com.github.mikesafonov.smpp.core.dto.MessageType; 6 | import com.github.mikesafonov.smpp.core.generators.AlwaysSuccessSmppResultGenerator; 7 | import com.github.mikesafonov.smpp.core.generators.SmppResultGenerator; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.List; 11 | 12 | import static com.github.mikesafonov.smpp.util.Randomizer.randomString; 13 | import static java.util.Arrays.asList; 14 | import static java.util.Collections.emptyList; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertThrows; 17 | import static org.mockito.Mockito.*; 18 | 19 | /** 20 | * @author Mike Safonov 21 | */ 22 | class TestSenderClientTest { 23 | 24 | @Test 25 | void shouldThrowNPE() { 26 | assertThrows(NullPointerException.class, () -> new TestSenderClient(null, emptyList(), new AlwaysSuccessSmppResultGenerator())); 27 | assertThrows(NullPointerException.class, () -> new TestSenderClient(mock(SenderClient.class), emptyList(), null)); 28 | } 29 | 30 | @Test 31 | void shouldContainExpectedId() { 32 | SenderClient senderClient = mock(SenderClient.class); 33 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 34 | TestSenderClient testSenderClient = new TestSenderClient(senderClient, emptyList(), smppResultGenerator); 35 | 36 | String id = randomString(); 37 | when(senderClient.getId()).thenReturn(id); 38 | 39 | assertEquals(id, testSenderClient.getId()); 40 | } 41 | 42 | @Test 43 | void shouldCallSenderClientSetup(){ 44 | SenderClient senderClient = mock(SenderClient.class); 45 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 46 | TestSenderClient testSenderClient = new TestSenderClient(senderClient, emptyList(), smppResultGenerator); 47 | testSenderClient.setup(); 48 | 49 | verify(senderClient, times(1)).setup(); 50 | } 51 | 52 | @Test 53 | void shouldGenerateResponse() { 54 | SenderClient senderClient = mock(SenderClient.class); 55 | List allowedPhones = asList(randomString()); 56 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 57 | TestSenderClient testSenderClient = new TestSenderClient(senderClient, allowedPhones, smppResultGenerator); 58 | 59 | String id = randomString(); 60 | when(senderClient.getId()).thenReturn(id); 61 | 62 | Message message = new Message(randomString(), randomString(), randomString(), randomString(), MessageType.SIMPLE); 63 | testSenderClient.send(message); 64 | 65 | verify(smppResultGenerator, times(1)).generate(id, message); 66 | verify(senderClient, times(0)).send(message); 67 | } 68 | 69 | @Test 70 | void shouldCallSenderClient() { 71 | SenderClient senderClient = mock(SenderClient.class); 72 | String destinationPhone = randomString(); 73 | List allowedPhones = asList(destinationPhone); 74 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 75 | TestSenderClient testSenderClient = new TestSenderClient(senderClient, allowedPhones, smppResultGenerator); 76 | 77 | String id = randomString(); 78 | when(senderClient.getId()).thenReturn(id); 79 | 80 | Message message = new Message(randomString(), destinationPhone, randomString(), randomString(), MessageType.SIMPLE); 81 | testSenderClient.send(message); 82 | 83 | verify(smppResultGenerator, times(0)).generate(id, message); 84 | verify(senderClient, times(1)).send(message); 85 | } 86 | 87 | @Test 88 | void shouldGenerateResponseForCancel() { 89 | SenderClient senderClient = mock(SenderClient.class); 90 | List allowedPhones = asList(randomString()); 91 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 92 | TestSenderClient testSenderClient = new TestSenderClient(senderClient, allowedPhones, smppResultGenerator); 93 | 94 | String id = randomString(); 95 | when(senderClient.getId()).thenReturn(id); 96 | 97 | CancelMessage message = new CancelMessage(randomString(), randomString(), randomString()); 98 | testSenderClient.cancel(message); 99 | 100 | verify(smppResultGenerator, times(1)).generate(id, message); 101 | verify(senderClient, times(0)).cancel(message); 102 | } 103 | 104 | @Test 105 | void shouldCallSenderClientForCancel() { 106 | SenderClient senderClient = mock(SenderClient.class); 107 | String destinationPhone = randomString(); 108 | List allowedPhones = asList(destinationPhone); 109 | SmppResultGenerator smppResultGenerator = mock(SmppResultGenerator.class); 110 | TestSenderClient testSenderClient = new TestSenderClient(senderClient, allowedPhones, smppResultGenerator); 111 | 112 | String id = randomString(); 113 | when(senderClient.getId()).thenReturn(id); 114 | 115 | CancelMessage message = new CancelMessage(randomString(), randomString(), destinationPhone); 116 | testSenderClient.cancel(message); 117 | 118 | verify(smppResultGenerator, times(0)).generate(id, message); 119 | verify(senderClient, times(1)).cancel(message); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/sender/UnknownTypeOfAddressParserTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.sender; 2 | 3 | import com.cloudhopper.commons.gsm.Npi; 4 | import com.cloudhopper.commons.gsm.Ton; 5 | import com.cloudhopper.commons.gsm.TypeOfAddress; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | 9 | import java.util.List; 10 | 11 | import static java.util.Arrays.asList; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | /** 15 | * @author Mike Safonov 16 | */ 17 | class UnknownTypeOfAddressParserTest { 18 | 19 | private static final TypeOfAddress unknown = new TypeOfAddress(Ton.UNKNOWN, Npi.UNKNOWN); 20 | 21 | private UnknownTypeOfAddressParser addressParser = new UnknownTypeOfAddressParser(); 22 | 23 | private static List sources() { 24 | return asList("TEST-A", "Test A", "Test_a", "Hello", "380223092235", "780223092235"); 25 | } 26 | 27 | @ParameterizedTest 28 | @MethodSource("sources") 29 | void shouldReturnUnknownSourceAddress(String source) { 30 | TypeOfAddress typeOfAddress = addressParser.getSource(source); 31 | 32 | assertEquals(unknown.getTon(), typeOfAddress.getTon()); 33 | assertEquals(unknown.getNpi(), typeOfAddress.getNpi()); 34 | } 35 | 36 | @ParameterizedTest 37 | @MethodSource("sources") 38 | void shouldReturnUnknownDestinationAddress(String source) { 39 | TypeOfAddress typeOfAddress = addressParser.getDestination(source); 40 | 41 | assertEquals(unknown.getTon(), typeOfAddress.getTon()); 42 | assertEquals(unknown.getNpi(), typeOfAddress.getNpi()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/utils/JodaJavaConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.utils; 2 | 3 | import org.joda.time.DateTime; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.ZonedDateTime; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertNull; 10 | 11 | /** 12 | * @author Mike Safonov 13 | */ 14 | class JodaJavaConverterTest { 15 | 16 | @Test 17 | void shouldConvert(){ 18 | 19 | DateTime dateTime = new DateTime(2001, 2, 2, 2, 2, 2, 2); 20 | 21 | ZonedDateTime localDateTime = JodaJavaConverter.convert(dateTime); 22 | 23 | assertEquals(dateTime.getYear(), localDateTime.getYear()); 24 | assertEquals(dateTime.getMonthOfYear(), localDateTime.getMonthValue()); 25 | assertEquals(dateTime.getDayOfMonth(), localDateTime.getDayOfMonth()); 26 | assertEquals(dateTime.getHourOfDay(), localDateTime.getHour()); 27 | assertEquals(dateTime.getMinuteOfHour(), localDateTime.getMinute()); 28 | assertEquals(dateTime.getSecondOfMinute(), localDateTime.getSecond()); 29 | assertEquals(dateTime.getMillisOfSecond(), localDateTime.getNano() / 1000000); 30 | } 31 | 32 | @Test 33 | void shouldReturnNull(){ 34 | assertNull(JodaJavaConverter.convert(null)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/core/utils/MessageUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.core.utils; 2 | 3 | import com.cloudhopper.commons.charset.CharsetUtil; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | 8 | import java.util.stream.Stream; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | /** 13 | * @author Mike Safonov 14 | */ 15 | class MessageUtilTest { 16 | 17 | 18 | private static Stream emptyTextProvider() { 19 | return Stream.of("", null); 20 | } 21 | 22 | @Test 23 | void latinRegular() { 24 | StringBuilder builder = new StringBuilder(); 25 | for (int i = 0; i < MessageUtil.GSM_7_REGULAR_MESSAGE_LENGTH; i++) { 26 | builder.append('W'); 27 | } 28 | 29 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(builder.toString()); 30 | assertEquals(1, countWithEncoding.getCount()); 31 | assertEquals(CharsetUtil.CHARSET_GSM, countWithEncoding.getCharset()); 32 | } 33 | 34 | @Test 35 | void latinMultipart() { 36 | StringBuilder builder = new StringBuilder(); 37 | for (int i = 0; i < 2 * MessageUtil.GSM_7_MULTIPART_MESSAGE_LENGTH; i++) { 38 | builder.append('W'); 39 | } 40 | 41 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(builder.toString()); 42 | assertEquals(2, countWithEncoding.getCount()); 43 | assertEquals(CharsetUtil.CHARSET_GSM, countWithEncoding.getCharset()); 44 | } 45 | 46 | @Test 47 | void testLatinOnlyUcs2() { 48 | StringBuilder builder = new StringBuilder(); 49 | for (int i = 0; i < MessageUtil.UCS_2_REGULAR_MESSAGE_LENGTH + 1; i++) { 50 | builder.append('W'); 51 | } 52 | 53 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(builder.toString(), true); 54 | assertEquals(2, countWithEncoding.getCount()); 55 | assertEquals(CharsetUtil.CHARSET_UCS_2, countWithEncoding.getCharset()); 56 | } 57 | 58 | @Test 59 | void latinAndCyrillicSymbol() { 60 | StringBuilder builder = new StringBuilder(); 61 | for (int i = 0; i < MessageUtil.UCS_2_REGULAR_MESSAGE_LENGTH - 1; i++) { 62 | builder.append('W'); 63 | } 64 | 65 | builder.append('А'); 66 | 67 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(builder.toString()); 68 | assertEquals(1, countWithEncoding.getCount()); 69 | assertEquals(CharsetUtil.CHARSET_UCS_2, countWithEncoding.getCharset()); 70 | 71 | } 72 | 73 | @Test 74 | void cyrillicRegular() { 75 | StringBuilder builder = new StringBuilder(); 76 | for (int i = 0; i < MessageUtil.UCS_2_REGULAR_MESSAGE_LENGTH; i++) { 77 | builder.append('А'); 78 | } 79 | 80 | 81 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(builder.toString()); 82 | assertEquals(1, countWithEncoding.getCount()); 83 | assertEquals(CharsetUtil.CHARSET_UCS_2, countWithEncoding.getCharset()); 84 | } 85 | 86 | @Test 87 | void cyrillicMultipart() { 88 | StringBuilder builder = new StringBuilder(); 89 | for (int i = 0; i < 2 * MessageUtil.UCS_2_MULTIPART_MESSAGE_LENGTH + 1; i++) { 90 | builder.append('А'); 91 | } 92 | 93 | 94 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(builder.toString()); 95 | assertEquals(3, countWithEncoding.getCount()); 96 | assertEquals(CharsetUtil.CHARSET_UCS_2, countWithEncoding.getCharset()); 97 | } 98 | 99 | @ParameterizedTest 100 | @MethodSource("emptyTextProvider") 101 | void shouldReturnEmpty(String value) { 102 | CountWithEncoding countWithEncoding = MessageUtil.calculateCountSMS(value); 103 | 104 | assertEquals(0, countWithEncoding.getCount()); 105 | assertEquals(CharsetUtil.CHARSET_UCS_2, countWithEncoding.getCharset()); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com/github/mikesafonov/smpp/util/Randomizer.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.util; 2 | 3 | import com.cloudhopper.commons.util.windowing.WindowFuture; 4 | import com.cloudhopper.smpp.pdu.PduRequest; 5 | import com.cloudhopper.smpp.pdu.PduResponse; 6 | import com.devskiller.jfairy.Fairy; 7 | import com.github.mikesafonov.smpp.config.SmppProperties; 8 | import com.github.mikesafonov.smpp.core.connection.ReceiverConfiguration; 9 | import com.github.mikesafonov.smpp.core.connection.TransceiverConfiguration; 10 | import com.github.mikesafonov.smpp.core.connection.TransmitterConfiguration; 11 | import lombok.experimental.UtilityClass; 12 | 13 | import java.time.Duration; 14 | 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.when; 17 | 18 | /** 19 | * @author Mike Safonov 20 | */ 21 | @UtilityClass 22 | public class Randomizer { 23 | private static final Fairy FAIRY = Fairy.create(); 24 | 25 | public static int randomPort() { 26 | return FAIRY.baseProducer().randomInt(9999); 27 | } 28 | 29 | public static int randomInt() { 30 | return FAIRY.baseProducer().randomInt(9999); 31 | } 32 | 33 | public static int randomPositive(int max) { 34 | return FAIRY.baseProducer().randomInt(max) + 1; 35 | } 36 | 37 | public static long randomLong() { 38 | return FAIRY.baseProducer().randomBetween(0, 9999); 39 | } 40 | 41 | public static boolean randomBoolean() { 42 | return FAIRY.baseProducer().trueOrFalse(); 43 | } 44 | 45 | public static String randomString() { 46 | return FAIRY.textProducer().latinWord(10); 47 | } 48 | 49 | public static String randomIp() { 50 | return FAIRY.networkProducer().ipAddress(); 51 | } 52 | 53 | public static Duration randomDuration() { 54 | return Duration.ofSeconds(randomLong()); 55 | } 56 | 57 | public static WindowFuture successWindowsFuture() throws InterruptedException { 58 | WindowFuture futureResponse = mock(WindowFuture.class); 59 | when(futureResponse.await()).thenReturn(true); 60 | when(futureResponse.isDone()).thenReturn(true); 61 | when(futureResponse.isSuccess()).thenReturn(true); 62 | return futureResponse; 63 | } 64 | 65 | public static WindowFuture failWindowsFuture(boolean await, boolean done, boolean success) { 66 | try { 67 | WindowFuture futureResponse = mock(WindowFuture.class); 68 | when(futureResponse.await()).thenReturn(await); 69 | when(futureResponse.isDone()).thenReturn(done); 70 | when(futureResponse.isSuccess()).thenReturn(success); 71 | return futureResponse; 72 | } catch (InterruptedException e) { 73 | return null; 74 | } 75 | } 76 | 77 | public static SmppProperties.Credentials randomCredentials() { 78 | SmppProperties.Credentials credentials = new SmppProperties.Credentials(); 79 | credentials.setHost(randomIp()); 80 | credentials.setPort(randomPort()); 81 | credentials.setUsername(randomString()); 82 | credentials.setPassword(randomString()); 83 | return credentials; 84 | } 85 | 86 | public static TransmitterConfiguration randomTransmitterConfiguration() { 87 | SmppProperties.Credentials credentials = randomCredentials(); 88 | return new TransmitterConfiguration(randomString(), credentials, randomBoolean(), randomBoolean(), randomInt(), 89 | randomString()); 90 | } 91 | 92 | public static TransceiverConfiguration randomTransceiverConfiguration() { 93 | SmppProperties.Credentials credentials = randomCredentials(); 94 | return new TransceiverConfiguration(randomString(), credentials, randomBoolean(), randomBoolean(), randomInt(), 95 | randomString()); 96 | } 97 | 98 | public static ReceiverConfiguration randomReceiverConfiguration() { 99 | SmppProperties.Credentials credentials = randomCredentials(); 100 | return new ReceiverConfiguration(randomString(), credentials, randomBoolean(), randomBoolean()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/SingleClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp; 2 | 3 | import com.cloudhopper.smpp.SmppConstants; 4 | import com.github.mikesafonov.smpp.config.SmppProperties; 5 | import com.github.mikesafonov.smpp.core.dto.CancelMessage; 6 | import com.github.mikesafonov.smpp.core.dto.Message; 7 | import com.github.mikesafonov.smpp.core.sender.DataCoding; 8 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 9 | import com.github.mikesafonov.smpp.junit.MockSmppExtension; 10 | import com.github.mikesafonov.smpp.junit.SmppServer; 11 | import com.github.mikesafonov.smpp.server.MockSmppServer; 12 | import lombok.extern.log4j.Log4j2; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | 17 | import static com.github.mikesafonov.smpp.TestUtils.createDefaultSenderClient; 18 | import static com.github.mikesafonov.smpp.assertj.SmppAssertions.assertThat; 19 | 20 | @Log4j2 21 | @ExtendWith(MockSmppExtension.class) 22 | public class SingleClientTest { 23 | @SmppServer 24 | private MockSmppServer mockSmppServer; 25 | private SenderClient client; 26 | 27 | @BeforeEach 28 | void createClient() { 29 | SmppProperties.Credentials credentials = new SmppProperties.Credentials(); 30 | credentials.setPort(mockSmppServer.getPort()); 31 | credentials.setHost(mockSmppServer.getHost()); 32 | credentials.setUsername(mockSmppServer.getSystemId()); 33 | credentials.setPassword(mockSmppServer.getPassword()); 34 | 35 | client = createDefaultSenderClient("test", credentials); 36 | } 37 | 38 | @Test 39 | void shouldSendSingleSimpleMessage() { 40 | Message message = Message.simple("asdasd") 41 | .from("123123123") 42 | .to("12312312") 43 | .build(); 44 | client.send(message); 45 | 46 | assertThat(mockSmppServer).hasSingleMessage() 47 | .hasEsmClass(SmppConstants.ESM_CLASS_MM_STORE_FORWARD) 48 | .hasDest("12312312") 49 | .hasSource("123123123") 50 | .hasText("asdasd") 51 | .hasDeliveryReport(); 52 | } 53 | 54 | @Test 55 | void shouldSendSingleDatagramMessage() { 56 | Message message = Message.datagram("asdasd") 57 | .from("123123123") 58 | .to("12312312") 59 | .build(); 60 | client.send(message); 61 | 62 | assertThat(mockSmppServer).hasSingleMessage() 63 | .hasEsmClass(SmppConstants.ESM_CLASS_MM_DATAGRAM) 64 | .hasDest("12312312") 65 | .hasSource("123123123") 66 | .hasText("asdasd") 67 | .doesNotHaveDeliveryReport(); 68 | } 69 | 70 | @Test 71 | void shouldSendSingleSilentMessage() { 72 | Message message = Message.silent("asdasd") 73 | .from("123123123") 74 | .to("12312312") 75 | .build(); 76 | client.send(message); 77 | 78 | assertThat(mockSmppServer).hasSingleMessage() 79 | .hasEsmClass(SmppConstants.ESM_CLASS_MM_STORE_FORWARD) 80 | .hasDest("12312312") 81 | .hasSource("123123123") 82 | .hasText("asdasd") 83 | .doesNotHaveDeliveryReport() 84 | .satisfies(submitSm -> assertThat(submitSm.getDataCoding()).isEqualTo(DataCoding.SILENT_CODING)); 85 | } 86 | 87 | @Test 88 | void shouldSendSingleCancelMessage() { 89 | CancelMessage cancelMessage = new CancelMessage("123", "123123123", "12312312"); 90 | client.cancel(cancelMessage); 91 | 92 | assertThat(mockSmppServer).hasSingleCancelMessage() 93 | .hasDest("12312312") 94 | .hasSource("123123123") 95 | .hasId("123"); 96 | } 97 | 98 | @Test 99 | void shouldSendSingleDatagramAndCancelMessages() { 100 | Message message = Message.datagram("asdasd") 101 | .from("123123123") 102 | .to("12312312") 103 | .build(); 104 | client.send(message); 105 | 106 | CancelMessage cancelMessage = new CancelMessage("123", "123123123", "12312312"); 107 | client.cancel(cancelMessage); 108 | 109 | assertThat(mockSmppServer).hasSingleMessage() 110 | .hasEsmClass(SmppConstants.ESM_CLASS_MM_DATAGRAM) 111 | .hasDest("12312312") 112 | .hasSource("123123123") 113 | .hasText("asdasd") 114 | .doesNotHaveDeliveryReport(); 115 | 116 | assertThat(mockSmppServer).hasSingleCancelMessage() 117 | .hasDest("12312312") 118 | .hasSource("123123123") 119 | .hasId("123"); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/TestUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp; 2 | 3 | import com.cloudhopper.smpp.SmppServerSession; 4 | import com.github.mikesafonov.smpp.config.SmppProperties; 5 | import com.github.mikesafonov.smpp.core.ClientFactory; 6 | import com.github.mikesafonov.smpp.core.connection.ConnectionManager; 7 | import com.github.mikesafonov.smpp.core.connection.ConnectionManagerFactory; 8 | import com.github.mikesafonov.smpp.core.sender.DefaultTypeOfAddressParser; 9 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 10 | import lombok.experimental.UtilityClass; 11 | import org.assertj.core.api.Assertions; 12 | import org.assertj.core.api.InstanceOfAssertFactory; 13 | import org.assertj.core.api.IterableAssert; 14 | 15 | import java.util.Set; 16 | 17 | @UtilityClass 18 | public class TestUtils { 19 | static SenderClient createDefaultSenderClient(String name, SmppProperties.Credentials credentials) { 20 | SmppProperties.SMSC smsc = new SmppProperties.SMSC(); 21 | smsc.setCredentials(credentials); 22 | smsc.setMaxTry(5); 23 | SmppProperties.Defaults defaults = new SmppProperties.Defaults(); 24 | ConnectionManager manager = new ConnectionManagerFactory().transmitter(name, defaults, smsc); 25 | return new ClientFactory().standardSender(name, defaults, smsc, new DefaultTypeOfAddressParser(), manager); 26 | } 27 | 28 | public static InstanceOfAssertFactory> sessionSet() { 29 | return new InstanceOfAssertFactory<>(Set.class, Assertions::assertThat); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/handler/CustomHandlerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.handler; 2 | 3 | import com.cloudhopper.smpp.SmppSessionListener; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * @author Mike Safonov 9 | */ 10 | @Configuration 11 | public class CustomHandlerConfiguration { 12 | 13 | @Bean 14 | public SmppSessionListener listener() { 15 | return new SmppSessionListenerImpl(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/handler/CustomHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.handler; 2 | 3 | import com.cloudhopper.smpp.pdu.DeliverSm; 4 | import com.cloudhopper.smpp.pdu.Pdu; 5 | import com.cloudhopper.smpp.util.DeliveryReceipt; 6 | import com.github.mikesafonov.smpp.api.SenderManager; 7 | import com.github.mikesafonov.smpp.config.SmppAutoConfiguration; 8 | import com.github.mikesafonov.smpp.core.dto.Message; 9 | import com.github.mikesafonov.smpp.core.dto.MessageResponse; 10 | import com.github.mikesafonov.smpp.server.MockSmppServerHolder; 11 | import lombok.SneakyThrows; 12 | import org.joda.time.DateTimeZone; 13 | import org.junit.jupiter.api.AfterAll; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.TestInstance; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.boot.test.context.SpringBootTest; 19 | import org.springframework.test.context.ActiveProfiles; 20 | import org.springframework.test.context.TestPropertySource; 21 | 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import static org.awaitility.Awaitility.await; 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | import static org.junit.jupiter.api.Assertions.assertFalse; 27 | 28 | /** 29 | * @author Mike Safonov 30 | */ 31 | @ActiveProfiles("handler") 32 | @SpringBootTest(classes = {CustomHandlerConfiguration.class, SmppAutoConfiguration.class}) 33 | @TestPropertySource( 34 | locations = "classpath:application-handler.properties") 35 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 36 | public class CustomHandlerTest { 37 | 38 | @Autowired 39 | private SmppSessionListenerImpl smppSessionListener; 40 | 41 | @Autowired 42 | private SenderManager senderManager; 43 | 44 | @Autowired 45 | private MockSmppServerHolder smppServerHolder; 46 | private Message message; 47 | 48 | @BeforeEach 49 | void clearAll() { 50 | smppServerHolder.clearAll(); 51 | message = Message.simple("my message") 52 | .from("3322") 53 | .to("2233") 54 | .build(); 55 | } 56 | 57 | @AfterAll 58 | void stopAll() { 59 | smppServerHolder.stopAll(); 60 | } 61 | 62 | @Test 63 | @SneakyThrows 64 | void shouldReceiveDeliveryReport() { 65 | MessageResponse response = senderManager.getByName("one").send(message); 66 | 67 | await().atMost(1, TimeUnit.SECONDS) 68 | .untilAsserted(() -> assertFalse(smppSessionListener.getPduList().isEmpty())); 69 | 70 | for (Pdu pdu : smppSessionListener.getPduList()) { 71 | if (pdu.isRequest() && pdu.getClass() == DeliverSm.class) { 72 | DeliveryReceipt deliveryReceipt = parseDeliveryReceipt((DeliverSm) pdu); 73 | assertEquals(response.getSmscMessageID(), deliveryReceipt.getMessageId()); 74 | } 75 | } 76 | } 77 | 78 | @SneakyThrows 79 | private DeliveryReceipt parseDeliveryReceipt(DeliverSm deliverSm) { 80 | byte[] shortMessage = deliverSm.getShortMessage(); 81 | String sms = new String(shortMessage); 82 | return DeliveryReceipt.parseShortMessage(sms, DateTimeZone.UTC); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/handler/SmppSessionListenerImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.handler; 2 | 3 | import com.cloudhopper.smpp.PduAsyncResponse; 4 | import com.cloudhopper.smpp.SmppSessionListener; 5 | import com.cloudhopper.smpp.pdu.Pdu; 6 | import com.cloudhopper.smpp.pdu.PduRequest; 7 | import com.cloudhopper.smpp.pdu.PduResponse; 8 | import com.cloudhopper.smpp.type.RecoverablePduException; 9 | import com.cloudhopper.smpp.type.UnrecoverablePduException; 10 | import lombok.Getter; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * @author Mike Safonov 17 | */ 18 | public class SmppSessionListenerImpl implements SmppSessionListener { 19 | @Getter 20 | private final List pduList = new ArrayList<>(); 21 | 22 | @Override 23 | public boolean firePduReceived(Pdu pdu) { 24 | return pduList.add(pdu); 25 | } 26 | 27 | @Override 28 | public boolean firePduDispatch(Pdu pdu) { 29 | return true; 30 | } 31 | 32 | @Override 33 | public void fireChannelUnexpectedlyClosed() { 34 | 35 | } 36 | 37 | @Override 38 | public PduResponse firePduRequestReceived(PduRequest pduRequest) { 39 | return null; 40 | } 41 | 42 | @Override 43 | public void firePduRequestExpired(PduRequest pduRequest) { 44 | 45 | } 46 | 47 | @Override 48 | public void fireExpectedPduResponseReceived(PduAsyncResponse pduAsyncResponse) { 49 | 50 | } 51 | 52 | @Override 53 | public void fireUnexpectedPduResponseReceived(PduResponse pduResponse) { 54 | 55 | } 56 | 57 | @Override 58 | public void fireUnrecoverablePduException(UnrecoverablePduException e) { 59 | 60 | } 61 | 62 | @Override 63 | public void fireRecoverablePduException(RecoverablePduException e) { 64 | 65 | } 66 | 67 | @Override 68 | public void fireUnknownThrowable(Throwable t) { 69 | 70 | } 71 | 72 | @Override 73 | public String lookupResultMessage(int commandStatus) { 74 | return null; 75 | } 76 | 77 | @Override 78 | public String lookupTlvTagName(short tag) { 79 | return null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/roundrobin/RoundRobinApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.roundrobin; 2 | 3 | import com.github.mikesafonov.smpp.api.SenderManager; 4 | import com.github.mikesafonov.smpp.core.reciever.DeliveryReportConsumer; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | @SpringBootApplication 9 | public class RoundRobinApplicationConfiguration { 10 | @Bean 11 | public RoundRobinApplicationService roundRobinApplicationService(SenderManager senderManager) { 12 | return new RoundRobinApplicationService(senderManager); 13 | } 14 | 15 | @Bean 16 | public DeliveryReportConsumer deliveryReportConsumer(){ 17 | return deliveryReport -> {}; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/roundrobin/RoundRobinApplicationService.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.roundrobin; 2 | 3 | import com.github.mikesafonov.smpp.api.SenderManager; 4 | import com.github.mikesafonov.smpp.core.dto.Message; 5 | import com.github.mikesafonov.smpp.core.sender.SenderClient; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | public class RoundRobinApplicationService { 10 | private final SenderManager senderManager; 11 | 12 | public void sendMessage(String from, String to, String text) { 13 | Message message = Message.simple(text) 14 | .from(from) 15 | .to(to) 16 | .build(); 17 | SenderClient client = senderManager.getClient(); 18 | client.send(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/roundrobin/RoundRobinSmppTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.roundrobin; 2 | 3 | import com.github.mikesafonov.smpp.config.SmppAutoConfiguration; 4 | import com.github.mikesafonov.smpp.server.MockSmppServerHolder; 5 | import org.junit.jupiter.api.AfterAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.TestInstance; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.ActiveProfiles; 12 | import org.springframework.test.context.TestPropertySource; 13 | 14 | import static com.github.mikesafonov.smpp.TestUtils.sessionSet; 15 | import static com.github.mikesafonov.smpp.assertj.SmppAssertions.assertThat; 16 | 17 | 18 | @ActiveProfiles("robin") 19 | @SpringBootTest(classes = {RoundRobinApplicationConfiguration.class, SmppAutoConfiguration.class}) 20 | @TestPropertySource( 21 | locations = "classpath:application-robin.properties") 22 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 23 | public class RoundRobinSmppTest { 24 | @Autowired 25 | private RoundRobinApplicationService roundRobinApplicationService; 26 | 27 | @Autowired 28 | private MockSmppServerHolder smppServerHolder; 29 | 30 | @BeforeEach 31 | void clearAll() { 32 | smppServerHolder.clearAll(); 33 | } 34 | 35 | @AfterAll 36 | void stopAll() { 37 | smppServerHolder.stopAll(); 38 | } 39 | 40 | @Test 41 | void shouldOpenTwoConnection(){ 42 | org.assertj.core.api.Assertions.assertThat(smppServerHolder.getByName("one").get()) 43 | .extracting("handler.sessions").asInstanceOf(sessionSet()).hasSize(2); 44 | org.assertj.core.api.Assertions.assertThat(smppServerHolder.getByName("two").get()) 45 | .extracting("handler.sessions").asInstanceOf(sessionSet()).hasSize(2); 46 | } 47 | 48 | @Test 49 | void shouldSendTwoMessages() { 50 | roundRobinApplicationService.sendMessage("one", "two", "one message"); 51 | roundRobinApplicationService.sendMessage("two", "one", "two message"); 52 | roundRobinApplicationService.sendMessage("one", "three", "three message"); 53 | 54 | assertThat(smppServerHolder).serverByName("one").messages() 55 | .hasSize(2) 56 | .containsDest("two") 57 | .containsDest("three") 58 | .containsSource("one") 59 | .containsText("one message") 60 | .containsText("three message"); 61 | 62 | assertThat(smppServerHolder).serverByName("two").hasSingleMessage() 63 | .hasDeliveryReport() 64 | .hasDest("one") 65 | .hasSource("two") 66 | .hasText("two message"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/transceiver/TransceiverConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.transceiver; 2 | 3 | import com.github.mikesafonov.smpp.core.reciever.DeliveryReportConsumer; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import static org.mockito.Mockito.mock; 8 | 9 | /** 10 | * @author Mike Safonov 11 | */ 12 | @Configuration 13 | public class TransceiverConfiguration { 14 | 15 | @Bean 16 | public DeliveryReportConsumer deliveryReportConsumer() { 17 | return mock(DeliveryReportConsumer.class); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/testIntegration/java/com/github/mikesafonov/smpp/transceiver/TransceiverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mikesafonov.smpp.transceiver; 2 | 3 | import com.github.mikesafonov.smpp.api.SenderManager; 4 | import com.github.mikesafonov.smpp.assertj.SmppAssertions; 5 | import com.github.mikesafonov.smpp.config.SmppAutoConfiguration; 6 | import com.github.mikesafonov.smpp.core.dto.DeliveryReport; 7 | import com.github.mikesafonov.smpp.core.dto.Message; 8 | import com.github.mikesafonov.smpp.core.dto.MessageResponse; 9 | import com.github.mikesafonov.smpp.core.reciever.DeliveryReportConsumer; 10 | import com.github.mikesafonov.smpp.server.MockSmppServer; 11 | import com.github.mikesafonov.smpp.server.MockSmppServerHolder; 12 | import org.junit.jupiter.api.AfterAll; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.TestInstance; 16 | import org.mockito.ArgumentCaptor; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.boot.test.context.SpringBootTest; 19 | import org.springframework.test.context.ActiveProfiles; 20 | import org.springframework.test.context.TestPropertySource; 21 | 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import static com.github.mikesafonov.smpp.TestUtils.sessionSet; 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | import static org.awaitility.Awaitility.await; 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | import static org.mockito.Mockito.verify; 29 | 30 | /** 31 | * @author Mike Safonov 32 | */ 33 | @ActiveProfiles("transceiver") 34 | @SpringBootTest(classes = {TransceiverConfiguration.class, SmppAutoConfiguration.class}) 35 | @TestPropertySource( 36 | locations = "classpath:application-transceiver.properties") 37 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 38 | public class TransceiverTest { 39 | @Autowired 40 | private SenderManager senderManager; 41 | 42 | @Autowired 43 | private DeliveryReportConsumer deliveryReportConsumer; 44 | 45 | @Autowired 46 | private MockSmppServerHolder smppServerHolder; 47 | private Message message; 48 | 49 | @BeforeEach 50 | void clearAll() { 51 | smppServerHolder.clearAll(); 52 | message = Message.simple("my message") 53 | .from("3322") 54 | .to("2233") 55 | .build(); 56 | } 57 | 58 | @AfterAll 59 | void stopAll() { 60 | smppServerHolder.stopAll(); 61 | } 62 | 63 | @Test 64 | void shouldOpenOneConnection() { 65 | MockSmppServer smppServer = smppServerHolder.getByName("one").get(); 66 | assertThat(smppServer).extracting("handler.sessions") 67 | .asInstanceOf(sessionSet()) 68 | .hasSize(1); 69 | } 70 | 71 | @Test 72 | void shouldSendMessage() { 73 | senderManager.getByName("one").send(message); 74 | SmppAssertions.assertThat(smppServerHolder).serverByName("one").hasSingleMessage() 75 | .hasDest("2233") 76 | .hasSource("3322") 77 | .hasText("my message") 78 | .hasDeliveryReport(); 79 | } 80 | 81 | @Test 82 | void shouldReceiveDeliveryReport() { 83 | MessageResponse response = senderManager.getByName("one").send(message); 84 | ArgumentCaptor captor = ArgumentCaptor.forClass(DeliveryReport.class); 85 | 86 | await().atMost(1, TimeUnit.SECONDS) 87 | .untilAsserted(() -> verify(deliveryReportConsumer).accept(captor.capture())); 88 | 89 | DeliveryReport report = captor.getValue(); 90 | 91 | assertEquals(response.getSmscMessageID(), report.getMessageId()); 92 | assertEquals("one", report.getResponseClientId()); 93 | } 94 | 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/testIntegration/resources/application-handler.properties: -------------------------------------------------------------------------------- 1 | smpp.defaults.connectionType=transceiver 2 | smpp.connections.one.credentials.host=${smpp.mocks.one.host} 3 | smpp.connections.one.credentials.username=${smpp.mocks.one.system-id} 4 | smpp.connections.one.credentials.password=${smpp.mocks.one.password} 5 | smpp.connections.one.credentials.port=${smpp.mocks.one.port} 6 | -------------------------------------------------------------------------------- /src/testIntegration/resources/application-robin.properties: -------------------------------------------------------------------------------- 1 | smpp.connections.one.credentials.host=${smpp.mocks.one.host} 2 | smpp.connections.one.credentials.username=${smpp.mocks.one.system-id} 3 | smpp.connections.one.credentials.password=${smpp.mocks.one.password} 4 | smpp.connections.one.credentials.port=${smpp.mocks.one.port} 5 | smpp.connections.two.credentials.host=${smpp.mocks.two.host} 6 | smpp.connections.two.credentials.username=${smpp.mocks.two.system-id} 7 | smpp.connections.two.credentials.password=${smpp.mocks.two.password} 8 | smpp.connections.two.credentials.port=${smpp.mocks.two.port} 9 | -------------------------------------------------------------------------------- /src/testIntegration/resources/application-transceiver.properties: -------------------------------------------------------------------------------- 1 | smpp.defaults.connectionType=transceiver 2 | smpp.connections.one.credentials.host=${smpp.mocks.one.host} 3 | smpp.connections.one.credentials.username=${smpp.mocks.one.system-id} 4 | smpp.connections.one.credentials.password=${smpp.mocks.one.password} 5 | smpp.connections.one.credentials.port=${smpp.mocks.one.port} 6 | -------------------------------------------------------------------------------- /src/testIntegration/resources/bootstrap-handler.properties: -------------------------------------------------------------------------------- 1 | smpp.mocks.one.password=pass 2 | smpp.mocks.one.system-id=user 3 | -------------------------------------------------------------------------------- /src/testIntegration/resources/bootstrap-robin.properties: -------------------------------------------------------------------------------- 1 | smpp.mocks.one.password=pass 2 | smpp.mocks.one.system-id=user 3 | smpp.mocks.two.password=pass2 4 | smpp.mocks.two.system-id=user2 -------------------------------------------------------------------------------- /src/testIntegration/resources/bootstrap-transceiver.properties: -------------------------------------------------------------------------------- 1 | smpp.mocks.one.password=pass 2 | smpp.mocks.one.system-id=user 3 | --------------------------------------------------------------------------------