├── Jdempotent-core ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ ├── spring.factories │ │ │ │ └── spring │ │ │ │ └── app-context.xml │ │ └── java │ │ │ └── com │ │ │ └── trendyol │ │ │ └── jdempotent │ │ │ └── core │ │ │ ├── config │ │ │ └── ConfigUtility.java │ │ │ ├── constant │ │ │ ├── EnvironmentVariableUtils.java │ │ │ ├── CryptographyAlgorithm.java │ │ │ └── RepositoryType.java │ │ │ ├── annotation │ │ │ ├── JdempotentIgnore.java │ │ │ ├── JdempotentId.java │ │ │ ├── JdempotentRequestPayload.java │ │ │ ├── JdempotentProperty.java │ │ │ └── JdempotentResource.java │ │ │ ├── chain │ │ │ ├── AnnotationChain.java │ │ │ ├── JdempotentDefaultChain.java │ │ │ ├── JdempotentNoAnnotationChain.java │ │ │ ├── JdempotentIgnoreAnnotationChain.java │ │ │ └── JdempotentPropertyAnnotationChain.java │ │ │ ├── callback │ │ │ └── ErrorConditionalCallback.java │ │ │ ├── model │ │ │ ├── KeyValuePair.java │ │ │ ├── ChainData.java │ │ │ ├── IdempotentRequestWrapper.java │ │ │ ├── IdempotentResponseWrapper.java │ │ │ ├── IdempotentIgnorableWrapper.java │ │ │ ├── IdempotentRequestResponseWrapper.java │ │ │ └── IdempotencyKey.java │ │ │ ├── generator │ │ │ ├── KeyGenerator.java │ │ │ └── DefaultKeyGenerator.java │ │ │ └── datasource │ │ │ ├── InMemoryIdempotentRepository.java │ │ │ ├── IdempotentRepository.java │ │ │ └── AbstractIdempotentRepository.java │ └── test │ │ └── java │ │ └── aspect │ │ ├── core │ │ ├── TestException.java │ │ ├── IdempotentTestPayload.java │ │ └── TestIdempotentResource.java │ │ ├── errorcallback │ │ ├── TestCustomErrorCallback.java │ │ ├── TestAopWithErrorCallbackContext.java │ │ └── IdempotentAspectWithErrorCallbackIT.java │ │ ├── withaspect │ │ ├── TestAopContext.java │ │ └── IdempotentAspectIT.java │ │ ├── withoutaspect │ │ └── IdempotentWithoutAspectIT.java │ │ └── chain │ │ ├── JdempotentDefaultChainTest.java │ │ ├── JdempotentPropertyAnnotationChainTest.java │ │ ├── JdempotentNoAnnotationChainTest.java │ │ └── JdempotentIgnoreAnnotationChainTest.java ├── javadoc │ └── overview.html └── settings.xml ├── examples ├── logo.jpg ├── cpu-profiling.png ├── jdempotent-redis-example │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ ├── maven-wrapper.properties │ │ │ └── MavenWrapperDownloader.java │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── jdempotent │ │ │ │ │ └── example │ │ │ │ │ └── demo │ │ │ │ │ ├── exception │ │ │ │ │ ├── InvalidEmailAddressException.java │ │ │ │ │ ├── RetryIdempotentRequestException.java │ │ │ │ │ └── CustomExceptionHandler.java │ │ │ │ │ ├── DemoApplication.java │ │ │ │ │ ├── model │ │ │ │ │ ├── SendEmailResponse.java │ │ │ │ │ ├── SendEmailRequest.java │ │ │ │ │ └── ErrorResponse.java │ │ │ │ │ ├── listener │ │ │ │ │ └── WelcomingListener.java │ │ │ │ │ ├── service │ │ │ │ │ └── MailSenderService.java │ │ │ │ │ └── controller │ │ │ │ │ └── MailController.java │ │ │ └── resources │ │ │ │ ├── logback-spring.xml │ │ │ │ └── application.yml │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── jdempotent │ │ │ └── example │ │ │ └── demo │ │ │ └── DemoApplicationTests.java │ ├── .gitignore │ ├── pom.xml │ ├── mvnw.cmd │ └── mvnw └── jdempotent-couchbase-example │ ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ ├── maven-wrapper.properties │ │ └── MavenWrapperDownloader.java │ ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── jdempotent │ │ │ │ └── example │ │ │ │ └── demo │ │ │ │ ├── exception │ │ │ │ ├── InvalidEmailAddressException.java │ │ │ │ ├── RetryIdempotentRequestException.java │ │ │ │ └── CustomExceptionHandler.java │ │ │ │ ├── DemoApplication.java │ │ │ │ ├── model │ │ │ │ ├── SendEmailResponse.java │ │ │ │ ├── SendEmailRequest.java │ │ │ │ └── ErrorResponse.java │ │ │ │ ├── listener │ │ │ │ └── WelcomingListener.java │ │ │ │ ├── service │ │ │ │ └── MailSenderService.java │ │ │ │ └── controller │ │ │ │ └── MailController.java │ │ └── resources │ │ │ ├── logback-spring.xml │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── com │ │ └── jdempotent │ │ └── example │ │ └── demo │ │ └── DemoApplicationTests.java │ ├── .gitignore │ ├── pom.xml │ ├── mvnw.cmd │ └── mvnw ├── Jdempotent-spring-boot-couchbase-starter ├── target │ ├── maven-status │ │ └── maven-compiler-plugin │ │ │ ├── testCompile │ │ │ └── default-testCompile │ │ │ │ └── inputFiles.lst │ │ │ └── compile │ │ │ └── default-compile │ │ │ ├── createdFiles.lst │ │ │ └── inputFiles.lst │ ├── maven-archiver │ │ └── pom.properties │ ├── Jdempotent-spring-boot-couchbase-starter-1.0.5.jar │ ├── test-classes │ │ └── CouchbaseIdempotentRepositoryTest.class │ └── classes │ │ ├── com │ │ └── trendyol │ │ │ └── jdempotent │ │ │ └── couchbase │ │ │ ├── CouchbaseConfig.class │ │ │ ├── ApplicationConfig.class │ │ │ ├── CouchbaseBeanConfig.class │ │ │ └── CouchbaseIdempotentRepository.class │ │ └── META-INF │ │ └── spring.factories ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring.factories │ │ └── java │ │ │ └── com │ │ │ └── trendyol │ │ │ └── jdempotent │ │ │ └── couchbase │ │ │ ├── ApplicationConfig.java │ │ │ ├── CouchbaseConfig.java │ │ │ ├── CouchbaseBeanConfig.java │ │ │ └── CouchbaseIdempotentRepository.java │ └── test │ │ └── java │ │ └── com │ │ └── trendyol │ │ └── jdempotent │ │ └── couchbase │ │ └── CouchbaseIdempotentRepositoryTest.java └── settings.xml ├── Jdempotent-spring-boot-redis-starter ├── javadoc │ └── overview.html ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring.factories │ │ └── java │ │ │ └── com │ │ │ └── trendyol │ │ │ └── jdempotent │ │ │ └── redis │ │ │ ├── ApplicationConfig.java │ │ │ ├── RedisEnvironmentPostProcessor.java │ │ │ ├── RedisSentinelConfiguration.java │ │ │ ├── RedisConfigProperties.java │ │ │ └── RedisIdempotentRepository.java │ └── test │ │ └── java │ │ ├── RedisTemplateMocker.java │ │ └── RedisIdempotentRepositoryTest.java └── settings.xml ├── Dockerfile ├── .gitignore ├── .github └── workflows │ ├── jdempotent-core.yml │ ├── jdempotent-spring-boot-redis-starter.yml │ └── codeql-analysis.yml ├── LICENSE └── README.md /Jdempotent-core/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/examples/logo.jpg -------------------------------------------------------------------------------- /examples/cpu-profiling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/examples/cpu-profiling.png -------------------------------------------------------------------------------- /Jdempotent-core/javadoc/overview.html: -------------------------------------------------------------------------------- 1 | 2 | Jdempotent-core 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/javadoc/overview.html: -------------------------------------------------------------------------------- 1 | 2 | Jdempotent-spring-boot-redis-starter 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/examples/jdempotent-redis-example/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/maven-archiver/pom.properties: -------------------------------------------------------------------------------- 1 | #Created by Apache Maven 3.8.2 2 | groupId=com.trendyol 3 | artifactId=Jdempotent-spring-boot-couchbase-starter 4 | version=1.1.0 5 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/Jdempotent-spring-boot-couchbase-starter-1.0.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/Jdempotent-spring-boot-couchbase-starter/target/Jdempotent-spring-boot-couchbase-starter-1.0.5.jar -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/test-classes/CouchbaseIdempotentRepositoryTest.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/Jdempotent-spring-boot-couchbase-starter/target/test-classes/CouchbaseIdempotentRepositoryTest.class -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/InvalidEmailAddressException.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.exception; 2 | 3 | public class InvalidEmailAddressException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/exception/InvalidEmailAddressException.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.exception; 2 | 3 | public class InvalidEmailAddressException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseConfig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseConfig.class -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/ApplicationConfig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/ApplicationConfig.class -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trendyol/Jdempotent/HEAD/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/core/TestException.java: -------------------------------------------------------------------------------- 1 | package aspect.core; 2 | 3 | public class TestException extends RuntimeException { 4 | public TestException(){ 5 | super(); 6 | } 7 | 8 | public TestException(String message){ 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/classes/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.trendyol.jdempotent.couchbase.CouchbaseConfig,\ 3 | com.trendyol.jdempotent.couchbase.CouchbaseBeanConfig,\ 4 | com.trendyol.jdempotent.couchbase.ApplicationConfig -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.trendyol.jdempotent.couchbase.CouchbaseConfig,\ 3 | com.trendyol.jdempotent.couchbase.CouchbaseBeanConfig,\ 4 | com.trendyol.jdempotent.couchbase.ApplicationConfig -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst: -------------------------------------------------------------------------------- 1 | com/trendyol/jdempotent/couchbase/ApplicationConfig.class 2 | com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class 3 | com/trendyol/jdempotent/couchbase/CouchbaseConfig.class 4 | com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class 5 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/exception/RetryIdempotentRequestException.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.exception; 2 | 3 | import javax.mail.MessagingException; 4 | 5 | public class RetryIdempotentRequestException extends RuntimeException { 6 | public RetryIdempotentRequestException(MessagingException e) { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/RetryIdempotentRequestException.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.exception; 2 | 3 | import javax.mail.MessagingException; 4 | 5 | public class RetryIdempotentRequestException extends RuntimeException { 6 | public RetryIdempotentRequestException(MessagingException e) { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/test/java/com/jdempotent/example/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/test/java/com/jdempotent/example/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/config/ConfigUtility.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | public class ConfigUtility { 8 | @Value("${jdempotent.cryptography.algorithm}") 9 | private String algorithm; 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This file is a template, and might need editing before it works on your project. 2 | FROM maven:3.5-jdk-11 as BUILD 3 | 4 | COPY . /usr/src/app 5 | RUN mvn --batch-mode -f /usr/src/app/pom.xml clean package 6 | 7 | FROM openjdk:11-jdk 8 | ENV PORT 4567 9 | EXPOSE 4567 10 | COPY --from=BUILD /usr/src/app/target /opt/target 11 | WORKDIR /opt/target 12 | 13 | CMD ["/bin/bash", "-c", "find -type f -name '*-with-dependencies.jar' | xargs java -jar"] -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/constant/EnvironmentVariableUtils.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.constant; 2 | 3 | /** 4 | * 5 | * Supported environment variables constants 6 | * 7 | 8 | */ 9 | public class EnvironmentVariableUtils { 10 | 11 | /** 12 | * Application name environment variables value that uses to generate idempotency key 13 | * 14 | */ 15 | public static String APP_NAME = "APP_NAME"; 16 | } 17 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DemoApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DemoApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/model/SendEmailResponse.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | 10 | @Getter 11 | @Setter 12 | @ToString 13 | @AllArgsConstructor 14 | public class SendEmailResponse implements Serializable { 15 | private String message; 16 | } 17 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailResponse.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | 10 | @Getter 11 | @Setter 12 | @ToString 13 | @AllArgsConstructor 14 | public class SendEmailResponse implements Serializable { 15 | private String message; 16 | } 17 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.trendyol.jdempotent.redis.ApplicationConfig,\ 3 | com.trendyol.jdempotent.redis.RedisConfigProperties,\ 4 | com.trendyol.jdempotent.redis.RedisSentinelConfiguration,\ 5 | org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration 6 | org.springframework.boot.env.EnvironmentPostProcessor=\ 7 | com.trendyol.jdempotent.redis.RedisEnvironmentPostProcessor -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailRequest.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.model; 2 | 3 | import lombok.*; 4 | import org.springframework.lang.NonNull; 5 | 6 | import java.io.Serializable; 7 | 8 | @Getter 9 | @Setter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | @Builder 13 | @ToString 14 | public class SendEmailRequest implements Serializable { 15 | private String email; 16 | private String subject; 17 | private String message; 18 | } -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/model/SendEmailRequest.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.model; 2 | 3 | import lombok.*; 4 | import org.springframework.lang.NonNull; 5 | 6 | import java.io.Serializable; 7 | 8 | @Getter 9 | @Setter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | @Builder 13 | @ToString 14 | public class SendEmailRequest implements Serializable { 15 | private String email; 16 | private String subject; 17 | private String message; 18 | } 19 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/annotation/JdempotentIgnore.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 10 | * That annotation needs to ignore annotation field 11 | * 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.FIELD) 15 | public @interface JdempotentIgnore { 16 | } 17 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/annotation/JdempotentId.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 10 | * Places the generated idempotency identifier into annotated field. 11 | * 12 | */ 13 | @Target(ElementType.FIELD) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface JdempotentId { 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | /Jdempotent-core/target/ 3 | /Jdempotent-spring-boot-redis-starter/target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/chain/AnnotationChain.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.chain; 2 | 3 | import com.trendyol.jdempotent.core.model.ChainData; 4 | import com.trendyol.jdempotent.core.model.KeyValuePair; 5 | 6 | public abstract class AnnotationChain { 7 | protected AnnotationChain nextChain; 8 | 9 | public abstract KeyValuePair process(ChainData chainData) throws IllegalAccessException; 10 | 11 | public void next(AnnotationChain nextChain) { 12 | this.nextChain = nextChain; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/annotation/JdempotentRequestPayload.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Add to the methods arguments that represents the idempotent request payload. 10 | * 11 | */ 12 | @Target(ElementType.PARAMETER) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface JdempotentRequestPayload { 15 | } -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/resources/META-INF/spring/app-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/annotation/JdempotentProperty.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 10 | * That annotation needs to get the field name differently and generate the hash 11 | * 12 | */ 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.FIELD) 15 | public @interface JdempotentProperty { 16 | String value() default ""; 17 | } 18 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/model/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | @AllArgsConstructor 12 | @Getter 13 | @Setter 14 | @ToString 15 | public class ErrorResponse implements Serializable { 16 | private String message; 17 | private List details; 18 | 19 | public ErrorResponse(String message){ 20 | this.message = message; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | @AllArgsConstructor 12 | @Getter 13 | @Setter 14 | @ToString 15 | public class ErrorResponse implements Serializable { 16 | private String message; 17 | private List details; 18 | 19 | public ErrorResponse(String message){ 20 | this.message = message; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jdempotent-core/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ossrh 5 | ${env.SONATYPE_USERNAME} 6 | ${env.SONATYPE_PASSWORD} 7 | 8 | 9 | 10 | 11 | 12 | ossrh 13 | 14 | true 15 | 16 | 17 | ${env.PASSPHRASE} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/callback/ErrorConditionalCallback.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.callback; 2 | 3 | /** 4 | * 5 | * A callback interface that need to clear cache for custom error condition 6 | * 7 | */ 8 | public interface ErrorConditionalCallback { 9 | 10 | /** 11 | * a error state flag 12 | * 13 | * @param response 14 | * @return 15 | */ 16 | boolean onErrorCondition(Object response); 17 | 18 | /** 19 | * 20 | * exception to throw when custom error occurs 21 | * 22 | * @return 23 | */ 24 | RuntimeException onErrorCustomException(); 25 | 26 | } -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ossrh 5 | ${env.SONATYPE_USERNAME} 6 | ${env.SONATYPE_PASSWORD} 7 | 8 | 9 | 10 | 11 | 12 | ossrh 13 | 14 | true 15 | 16 | 17 | ${env.PASSPHRASE} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ossrh 5 | ${env.SONATYPE_USERNAME} 6 | ${env.SONATYPE_PASSWORD} 7 | 8 | 9 | 10 | 11 | 12 | ossrh 13 | 14 | true 15 | 16 | 17 | ${env.PASSPHRASE} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/chain/JdempotentDefaultChain.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.chain; 2 | 3 | import com.trendyol.jdempotent.core.model.ChainData; 4 | import com.trendyol.jdempotent.core.model.KeyValuePair; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | public class JdempotentDefaultChain extends AnnotationChain { 9 | 10 | @Override 11 | public KeyValuePair process(ChainData chainData) throws IllegalAccessException { 12 | Field declaredField = chainData.getDeclaredField(); 13 | declaredField.setAccessible(true); 14 | return new KeyValuePair(declaredField.getName(),declaredField.get(chainData.getArgs())); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/KeyValuePair.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.model; 2 | 3 | public class KeyValuePair { 4 | private String key; 5 | private Object value; 6 | 7 | public KeyValuePair(){} 8 | 9 | public KeyValuePair(String key, Object value) { 10 | this.key = key; 11 | this.value = value; 12 | } 13 | 14 | public String getKey() { 15 | return key; 16 | } 17 | 18 | public void setKey(String key) { 19 | this.key = key; 20 | } 21 | 22 | public Object getValue() { 23 | return value; 24 | } 25 | 26 | public void setValue(String value) { 27 | this.value = value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/generator/KeyGenerator.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.generator; 2 | 3 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 4 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 5 | 6 | import java.security.MessageDigest; 7 | 8 | /** 9 | * 10 | */ 11 | public interface KeyGenerator { 12 | 13 | /** 14 | * 15 | * @param requestObject 16 | * @param listenerName 17 | * @param builder 18 | * @param messageDigest 19 | * @return 20 | */ 21 | IdempotencyKey generateIdempotentKey(IdempotentRequestWrapper requestObject, String listenerName, StringBuilder builder, MessageDigest messageDigest); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst: -------------------------------------------------------------------------------- 1 | /Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java 2 | /Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java 3 | /Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java 4 | /Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java 5 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/constant/CryptographyAlgorithm.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.constant; 2 | 3 | /** 4 | * 5 | * Supported hash algorithms to generate idempotency key 6 | * 7 | */ 8 | public enum CryptographyAlgorithm { 9 | 10 | /** 11 | * use md5 hash algorithm 12 | */ 13 | MD5("MD5"), 14 | 15 | /** 16 | * use SHA-256 hash algorithm 17 | */ 18 | SHA256("SHA-256"), 19 | 20 | /** 21 | * use SHA-1 hash algorithm 22 | */ 23 | SHA1("SHA-1"); 24 | 25 | private String algorithm; 26 | 27 | CryptographyAlgorithm(String algorithm) { 28 | this.algorithm = algorithm; 29 | } 30 | 31 | public String value(){ 32 | return algorithm; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/jdempotent-core.yml: -------------------------------------------------------------------------------- 1 | name: Release Jdempotent-core 2 | 3 | # Run workflow on manually 4 | on: [workflow_dispatch] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-18.04 8 | steps: 9 | - name: Check out Git repository 10 | uses: actions/checkout@v2 11 | 12 | - name: Install Java and Maven 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 11 16 | 17 | - name: Release Maven package 18 | uses: samuelmeuli/action-maven-publish@v1.4.0 19 | with: 20 | gpg_private_key: ${{ secrets.gpg_private_key }} 21 | gpg_passphrase: ${{ secrets.gpg_passphrase }} 22 | nexus_username: ${{ secrets.nexus_username }} 23 | nexus_password: ${{ secrets.nexus_password }} 24 | directory: Jdempotent-core -------------------------------------------------------------------------------- /.github/workflows/jdempotent-spring-boot-redis-starter.yml: -------------------------------------------------------------------------------- 1 | name: Release Jdempotent 2 | 3 | # Run workflow on manually 4 | on: [workflow_dispatch] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-18.04 8 | steps: 9 | - name: Check out Git repository 10 | uses: actions/checkout@v2 11 | 12 | - name: Install Java and Maven 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 11 16 | 17 | - name: Release Maven package 18 | uses: samuelmeuli/action-maven-publish@v1.4.0 19 | with: 20 | gpg_private_key: ${{ secrets.gpg_private_key }} 21 | gpg_passphrase: ${{ secrets.gpg_passphrase }} 22 | nexus_username: ${{ secrets.nexus_username }} 23 | nexus_password: ${{ secrets.nexus_password }} 24 | directory: . 25 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/ChainData.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.model; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | public class ChainData { 6 | private Field declaredField; 7 | private Object args; 8 | 9 | public ChainData(){ 10 | } 11 | 12 | public ChainData(Field declaredField, Object args) { 13 | this.declaredField = declaredField; 14 | this.args = args; 15 | } 16 | 17 | public Field getDeclaredField() { 18 | return declaredField; 19 | } 20 | 21 | public void setDeclaredField(Field declaredField) { 22 | this.declaredField = declaredField; 23 | } 24 | 25 | public Object getArgs() { 26 | return args; 27 | } 28 | 29 | public void setArgs(Object args) { 30 | this.args = args; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/annotation/JdempotentResource.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.annotation; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * Add to the methods that need to be idempotent. 9 | * 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface JdempotentResource { 13 | 14 | /** 15 | * Prefix value to make hash value more collider 16 | * 17 | * @return 18 | */ 19 | String cachePrefix() default ""; 20 | 21 | /** 22 | * To add custom ttl 23 | * 24 | * @return 25 | */ 26 | long ttl() default 0L; 27 | 28 | /** 29 | * To add custom time unit 30 | * 31 | * @return 32 | */ 33 | TimeUnit ttlTimeUnit() default TimeUnit.HOURS; 34 | } -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/chain/JdempotentNoAnnotationChain.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.chain; 2 | 3 | import com.trendyol.jdempotent.core.model.ChainData; 4 | import com.trendyol.jdempotent.core.model.KeyValuePair; 5 | 6 | import java.lang.reflect.Field; 7 | 8 | public class JdempotentNoAnnotationChain extends AnnotationChain { 9 | 10 | @Override 11 | public KeyValuePair process(ChainData chainData) throws IllegalAccessException { 12 | if (chainData.getDeclaredField().getDeclaredAnnotations().length == 0) { 13 | Field declaredField = chainData.getDeclaredField(); 14 | declaredField.setAccessible(true); 15 | return new KeyValuePair(declaredField.getName(),declaredField.get(chainData.getArgs())); 16 | } 17 | return super.nextChain.process(chainData); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/chain/JdempotentIgnoreAnnotationChain.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.chain; 2 | 3 | import com.trendyol.jdempotent.core.annotation.JdempotentIgnore; 4 | import com.trendyol.jdempotent.core.model.ChainData; 5 | import com.trendyol.jdempotent.core.model.KeyValuePair; 6 | 7 | import java.lang.reflect.Field; 8 | 9 | public class JdempotentIgnoreAnnotationChain extends AnnotationChain { 10 | @Override 11 | public KeyValuePair process(ChainData chainData) throws IllegalAccessException { 12 | Field declaredField = chainData.getDeclaredField(); 13 | declaredField.setAccessible(true); 14 | JdempotentIgnore annotation = declaredField.getAnnotation(JdempotentIgnore.class); 15 | if(annotation != null){ 16 | return new KeyValuePair(); 17 | } 18 | return super.nextChain.process(chainData); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/errorcallback/TestCustomErrorCallback.java: -------------------------------------------------------------------------------- 1 | package aspect.errorcallback; 2 | 3 | import aspect.core.IdempotentTestPayload; 4 | import aspect.core.TestException; 5 | import com.trendyol.jdempotent.core.callback.ErrorConditionalCallback; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.util.ObjectUtils; 8 | 9 | @Component 10 | public class TestCustomErrorCallback implements ErrorConditionalCallback { 11 | @Override 12 | public boolean onErrorCondition(Object response) { 13 | if(ObjectUtils.isEmpty(((IdempotentTestPayload) response).getName())){ 14 | return false; 15 | } 16 | return ((IdempotentTestPayload) response).getName().equalsIgnoreCase("test"); 17 | } 18 | 19 | @Override 20 | public RuntimeException onErrorCustomException() { 21 | return new TestException("Name will not be test"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/core/IdempotentTestPayload.java: -------------------------------------------------------------------------------- 1 | package aspect.core; 2 | 3 | import com.trendyol.jdempotent.core.annotation.JdempotentIgnore; 4 | import com.trendyol.jdempotent.core.annotation.JdempotentProperty; 5 | 6 | public class IdempotentTestPayload { 7 | private String name; 8 | @JdempotentIgnore 9 | private Long age; 10 | 11 | @JdempotentProperty("transactionId") 12 | private Long eventId; 13 | 14 | public IdempotentTestPayload() { 15 | } 16 | 17 | public IdempotentTestPayload(String name) { 18 | this.name = name; 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public void setName(String name) { 26 | this.name = name; 27 | } 28 | 29 | public void setEventId(Long eventId) { 30 | this.eventId = eventId; 31 | } 32 | 33 | public void setAge(Long age) { 34 | this.age = age; 35 | } 36 | } -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/datasource/InMemoryIdempotentRepository.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.datasource; 2 | 3 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 4 | import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | /** 10 | * An implementation of the idempotent AbstractIdempotentRepository 11 | * that uses as a default map 12 | */ 13 | public class InMemoryIdempotentRepository extends AbstractIdempotentRepository { 14 | 15 | private final ConcurrentHashMap map; 16 | 17 | public InMemoryIdempotentRepository() { 18 | this.map = new ConcurrentHashMap<>(); 19 | } 20 | 21 | @Override 22 | protected Map getMap() { 23 | return map; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/chain/JdempotentPropertyAnnotationChain.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.chain; 2 | 3 | import com.trendyol.jdempotent.core.annotation.JdempotentProperty; 4 | import com.trendyol.jdempotent.core.model.ChainData; 5 | import com.trendyol.jdempotent.core.model.KeyValuePair; 6 | 7 | import java.lang.reflect.Field; 8 | 9 | public class JdempotentPropertyAnnotationChain extends AnnotationChain { 10 | 11 | @Override 12 | public KeyValuePair process(ChainData chainData) throws IllegalAccessException { 13 | Field declaredField = chainData.getDeclaredField(); 14 | declaredField.setAccessible(true); 15 | JdempotentProperty annotation = declaredField.getAnnotation(JdempotentProperty.class); 16 | if(annotation != null){ 17 | return new KeyValuePair(annotation.value(),declaredField.get(chainData.getArgs())); 18 | } 19 | return super.nextChain.process(chainData); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestWrapper.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.model; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * 7 | * Wraps the incoming event value 8 | * 9 | */ 10 | @SuppressWarnings("serial") 11 | public class IdempotentRequestWrapper implements Serializable { 12 | private Object request; 13 | 14 | public IdempotentRequestWrapper(){ 15 | } 16 | 17 | public IdempotentRequestWrapper(Object request) { 18 | this.request = request; 19 | } 20 | 21 | public Object getRequest() { 22 | return request; 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return request == null ? 0 : request.hashCode(); 28 | } 29 | 30 | @Override 31 | public boolean equals(Object obj) { 32 | return request == null ? false : request.equals(obj); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return String.format("IdempotentRequestWrapper [request=%s]", request); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentResponseWrapper.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.model; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Wraps the incoming event response 7 | * 8 | */ 9 | @SuppressWarnings("serial") 10 | public class IdempotentResponseWrapper implements Serializable { 11 | 12 | private Object response; 13 | 14 | public IdempotentResponseWrapper(){} 15 | 16 | public IdempotentResponseWrapper(Object response) { 17 | this.response = response; 18 | } 19 | 20 | public Object getResponse() { 21 | return response; 22 | } 23 | 24 | @Override 25 | public int hashCode() { 26 | return response == null ? 0 : response.hashCode(); 27 | } 28 | 29 | @Override 30 | public boolean equals(Object obj) { 31 | return response == null ? false : response.equals(obj); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return String.format("IdempotentResponseWrapper [response=%s]", response); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/constant/RepositoryType.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.constant; 2 | 3 | import java.util.Arrays; 4 | 5 | /** 6 | * 7 | * Supported datasource types 8 | * 9 | */ 10 | public enum RepositoryType { 11 | 12 | /** 13 | * Redis config value 14 | */ 15 | REDIS("redis"), 16 | 17 | /** 18 | * Hazelcast config value 19 | */ 20 | HAZELCAST("hazelcast"), 21 | 22 | /** 23 | * Default config 24 | */ 25 | INMEMORY("default"); 26 | 27 | private String value; 28 | 29 | RepositoryType(String value) { 30 | this.value = value; 31 | } 32 | 33 | public String value() { 34 | return value; 35 | } 36 | 37 | /** 38 | * return 39 | * 40 | * @param repositoryName 41 | * @return 42 | */ 43 | public static RepositoryType getRepositoryTypeByValue(String repositoryName){ 44 | return Arrays.stream(values()).filter(repositoryType -> repositoryName.equalsIgnoreCase(repositoryType.value)).findAny().orElse(RepositoryType.INMEMORY); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Customer Service / CS / Libs 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 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/withaspect/TestAopContext.java: -------------------------------------------------------------------------------- 1 | package aspect.withaspect; 2 | 3 | import com.trendyol.jdempotent.core.aspect.IdempotentAspect; 4 | import com.trendyol.jdempotent.core.datasource.InMemoryIdempotentRepository; 5 | import com.trendyol.jdempotent.core.generator.DefaultKeyGenerator; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 10 | 11 | @Configuration 12 | @EnableAspectJAutoProxy 13 | @ComponentScan(basePackages = {"com.trendyol.jdempotent.core"}) 14 | public class TestAopContext { 15 | 16 | @Bean 17 | public IdempotentAspect idempotentAspect(InMemoryIdempotentRepository inMemoryIdempotentRepository, DefaultKeyGenerator defaultKeyGenerator) { 18 | return new IdempotentAspect(inMemoryIdempotentRepository, defaultKeyGenerator); 19 | } 20 | 21 | @Bean 22 | public InMemoryIdempotentRepository inMemoryIdempotentRepository() { 23 | return new InMemoryIdempotentRepository(); 24 | } 25 | 26 | @Bean 27 | public DefaultKeyGenerator defaultKeyGenerator() { 28 | return new DefaultKeyGenerator(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentIgnorableWrapper.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Objects; 7 | 8 | public class IdempotentIgnorableWrapper implements Serializable { 9 | private Map nonIgnoredFields; 10 | 11 | public IdempotentIgnorableWrapper() { 12 | nonIgnoredFields = new HashMap<>(); 13 | } 14 | 15 | public Map getNonIgnoredFields() { 16 | return nonIgnoredFields; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | IdempotentIgnorableWrapper wrapper = (IdempotentIgnorableWrapper) o; 25 | 26 | return Objects.equals(nonIgnoredFields, wrapper.nonIgnoredFields); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return nonIgnoredFields != null ? nonIgnoredFields.hashCode() : 0; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "IdempotentIgnorableWrapper{" + 37 | "nonIgnoredFields=" + nonIgnoredFields + 38 | '}'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | jdempotent: 2 | enable: true 3 | cryptography: 4 | algorithm: MD5 5 | cache: 6 | persistReqRes: false 7 | couchbase: 8 | connection-string: XXXXXXXX 9 | password: XXXXXXXX 10 | username: XXXXXXXX 11 | bucket-name: XXXXXXXX 12 | connect-timeout: 100000 13 | query-timeout: 20000 14 | kv-timeout: 3000 15 | 16 | email: 17 | from: 18 | address: XXXXXXX 19 | 20 | spring: 21 | mail: 22 | host: smtp.gmail.com 23 | port: 587 24 | username: XXXXXXX 25 | password: XXXXXXX 26 | properties.mail.smtp: 27 | auth: true 28 | starttls.enable: true 29 | 30 | template: 31 | welcoming: 32 | subject: "Welcoming" 33 | message: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum" -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | jdempotent: 2 | enable: true 3 | cryptography: 4 | algorithm: MD5 5 | cache: 6 | redis: 7 | database: 9 8 | password: "pass" 9 | sentinelHostList: localhost 10 | sentinelPort: 26379 11 | sentinelMasterName: "master" 12 | expirationTimeHour: 2 13 | dialTimeoutSecond: 3 14 | readTimeoutSecond: 3 15 | writeTimeoutSecond: 3 16 | maxRetryCount: 3 17 | expireTimeoutHour: 84 18 | 19 | email: 20 | from: 21 | address: XXXXXXX 22 | 23 | spring: 24 | mail: 25 | host: smtp.gmail.com 26 | port: 587 27 | username: XXXXXXX 28 | password: XXXXXXX 29 | properties.mail.smtp: 30 | auth: true 31 | starttls.enable: true 32 | 33 | template: 34 | welcoming: 35 | subject: "Welcoming" 36 | message: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum" -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestResponseWrapper.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.model; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * 7 | * That is a container for idempotent requests and responses 8 | * 9 | */ 10 | @SuppressWarnings("serial") 11 | public class IdempotentRequestResponseWrapper implements Serializable { 12 | private IdempotentRequestWrapper request; 13 | private IdempotentResponseWrapper response = null; 14 | 15 | public IdempotentRequestResponseWrapper(){} 16 | 17 | public IdempotentRequestResponseWrapper(IdempotentRequestWrapper request) { 18 | this.request = request; 19 | } 20 | 21 | public IdempotentRequestResponseWrapper(IdempotentRequestWrapper request, IdempotentResponseWrapper response) { 22 | this.request = request; 23 | this.response = response; 24 | } 25 | 26 | public IdempotentResponseWrapper getResponse() { 27 | return response; 28 | } 29 | 30 | public void setResponse(IdempotentResponseWrapper response) { 31 | synchronized (this) { 32 | this.response = response; 33 | } 34 | } 35 | 36 | public IdempotentRequestWrapper getRequest() { 37 | return request; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return String.format("IdempotentRequestResponseWrapper [request=%s, response=%s]", request, response); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/errorcallback/TestAopWithErrorCallbackContext.java: -------------------------------------------------------------------------------- 1 | package aspect.errorcallback; 2 | 3 | import com.trendyol.jdempotent.core.aspect.IdempotentAspect; 4 | import com.trendyol.jdempotent.core.datasource.InMemoryIdempotentRepository; 5 | import com.trendyol.jdempotent.core.generator.DefaultKeyGenerator; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 10 | 11 | @Configuration 12 | @EnableAspectJAutoProxy 13 | @ComponentScan(basePackages = { "com.trendyol.jdempotent.core" }) 14 | public class TestAopWithErrorCallbackContext { 15 | 16 | @Bean 17 | public IdempotentAspect idempotentAspect (InMemoryIdempotentRepository inMemoryIdempotentRepository, DefaultKeyGenerator defaultKeyGenerator, TestCustomErrorCallback testCustomErrorCallback) { 18 | return new IdempotentAspect(inMemoryIdempotentRepository,testCustomErrorCallback, defaultKeyGenerator); 19 | } 20 | 21 | @Bean 22 | public InMemoryIdempotentRepository inMemoryIdempotentRepository(){ 23 | return new InMemoryIdempotentRepository(); 24 | } 25 | 26 | @Bean 27 | public DefaultKeyGenerator defaultKeyGenerator(){ 28 | return new DefaultKeyGenerator(); 29 | } 30 | 31 | @Bean 32 | public TestCustomErrorCallback testCustomErrorCallback(){ 33 | return new TestCustomErrorCallback(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/withoutaspect/IdempotentWithoutAspectIT.java: -------------------------------------------------------------------------------- 1 | package aspect.withoutaspect; 2 | 3 | import aspect.core.TestIdempotentResource; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.aop.framework.AopProxyUtils; 7 | import org.springframework.aop.support.AopUtils; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | import org.springframework.test.util.AopTestUtils; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertFalse; 15 | 16 | @RunWith(SpringRunner.class) 17 | @ContextConfiguration(classes = {TestIdempotentResource.class}) 18 | public class IdempotentWithoutAspectIT { 19 | 20 | @Autowired 21 | private TestIdempotentResource testIdempotentResource; 22 | 23 | @Test 24 | public void given_context_then_run_with_non_aop_context() { 25 | assertEquals(testIdempotentResource.getClass(), TestIdempotentResource.class); 26 | assertFalse(AopUtils.isAopProxy(testIdempotentResource)); 27 | assertFalse(AopUtils.isCglibProxy(testIdempotentResource)); 28 | 29 | assertEquals(AopProxyUtils.ultimateTargetClass(testIdempotentResource), TestIdempotentResource.class); 30 | assertEquals(AopTestUtils.getTargetObject(testIdempotentResource).getClass(), TestIdempotentResource.class); 31 | assertEquals(AopTestUtils.getUltimateTargetObject(testIdempotentResource).getClass(), TestIdempotentResource.class); 32 | } 33 | } -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/exception/CustomExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.exception; 2 | 3 | import com.jdempotent.example.demo.model.ErrorResponse; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.context.request.WebRequest; 9 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @ControllerAdvice 15 | public class CustomExceptionHandler extends ResponseEntityExceptionHandler { 16 | @ExceptionHandler(Exception.class) 17 | public final ResponseEntity handleAllExceptions(Exception ex, WebRequest request) { 18 | List details = new ArrayList<>(); 19 | details.add(ex.getLocalizedMessage()); 20 | ErrorResponse error = new ErrorResponse("Server Error", details); 21 | return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR); 22 | } 23 | 24 | @ExceptionHandler(InvalidEmailAddressException.class) 25 | public final ResponseEntity handleRecordNotFoundException(InvalidEmailAddressException ex, WebRequest request) { 26 | List details = new ArrayList<>(); 27 | details.add(ex.getLocalizedMessage()); 28 | ErrorResponse error = new ErrorResponse("Invalid email address", details); 29 | return new ResponseEntity(error, HttpStatus.NOT_FOUND); 30 | } 31 | } -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/CustomExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.exception; 2 | 3 | import com.jdempotent.example.demo.model.ErrorResponse; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.context.request.WebRequest; 9 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @ControllerAdvice 15 | public class CustomExceptionHandler extends ResponseEntityExceptionHandler { 16 | @ExceptionHandler(Exception.class) 17 | public final ResponseEntity handleAllExceptions(Exception ex, WebRequest request) { 18 | List details = new ArrayList<>(); 19 | details.add(ex.getLocalizedMessage()); 20 | ErrorResponse error = new ErrorResponse("Server Error", details); 21 | return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR); 22 | } 23 | 24 | @ExceptionHandler(InvalidEmailAddressException.class) 25 | public final ResponseEntity handleRecordNotFoundException(InvalidEmailAddressException ex, WebRequest request) { 26 | List details = new ArrayList<>(); 27 | details.add(ex.getLocalizedMessage()); 28 | ErrorResponse error = new ErrorResponse("Invalid email address", details); 29 | return new ResponseEntity(error, HttpStatus.NOT_FOUND); 30 | } 31 | } -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotencyKey.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.model; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * 7 | * Wraps the combine of application name, listener name and incoming event value hash 8 | * 9 | */ 10 | public class IdempotencyKey implements Serializable { 11 | 12 | private String keyValue; 13 | 14 | public IdempotencyKey() { 15 | } 16 | 17 | public IdempotencyKey(String keyValue) { 18 | this.keyValue = keyValue; 19 | } 20 | 21 | public String getKeyValue() { 22 | return keyValue; 23 | } 24 | 25 | public void setKeyValue(String keyValue) { 26 | this.keyValue = keyValue; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | final int prime = 31; 32 | int result = 1; 33 | result = prime + keyValue.hashCode(); 34 | return result; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object obj) { 39 | if (this == obj) { 40 | return true; 41 | } 42 | if (obj == null) { 43 | return false; 44 | } 45 | if (!(obj instanceof IdempotencyKey)) { 46 | return false; 47 | } 48 | IdempotencyKey other = (IdempotencyKey) obj; 49 | if (keyValue == null) { 50 | if (other.keyValue != null) { 51 | return false; 52 | } 53 | } else if (!keyValue.equals(other.keyValue)) { 54 | return false; 55 | } 56 | return true; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return String.format("IdempotencyKey [keyValue=%s]", keyValue); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.couchbase; 2 | 3 | import com.couchbase.client.java.Collection; 4 | import com.trendyol.jdempotent.core.aspect.IdempotentAspect; 5 | import com.trendyol.jdempotent.core.callback.ErrorConditionalCallback; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | @ConditionalOnProperty( 13 | prefix="jdempotent", name = "enable", 14 | havingValue = "true", 15 | matchIfMissing = true) 16 | public class ApplicationConfig { 17 | 18 | private final CouchbaseConfig couchbaseConfig; 19 | 20 | public ApplicationConfig(CouchbaseConfig couchbaseConfig) { 21 | this.couchbaseConfig = couchbaseConfig; 22 | } 23 | 24 | @Bean 25 | @ConditionalOnProperty( 26 | prefix="jdempotent", name = "enable", 27 | havingValue = "true", 28 | matchIfMissing = true) 29 | @ConditionalOnClass(ErrorConditionalCallback.class) 30 | public IdempotentAspect getIdempotentAspect(Collection collection, ErrorConditionalCallback errorConditionalCallback) { 31 | return new IdempotentAspect(new CouchbaseIdempotentRepository(couchbaseConfig, collection), errorConditionalCallback); 32 | } 33 | 34 | @Bean 35 | public IdempotentAspect getIdempotentAspect(Collection collection) { 36 | return new IdempotentAspect(new CouchbaseIdempotentRepository(couchbaseConfig, collection)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.redis; 2 | 3 | 4 | import com.trendyol.jdempotent.core.aspect.IdempotentAspect; 5 | import com.trendyol.jdempotent.core.callback.ErrorConditionalCallback; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.data.redis.core.RedisTemplate; 11 | 12 | /** 13 | * 14 | */ 15 | @Configuration 16 | @ConditionalOnProperty( 17 | prefix="jdempotent", name = "enable", 18 | havingValue = "true", 19 | matchIfMissing = true) 20 | public class ApplicationConfig { 21 | 22 | private final RedisConfigProperties redisProperties; 23 | 24 | public ApplicationConfig(RedisConfigProperties redisProperties) { 25 | this.redisProperties = redisProperties; 26 | } 27 | 28 | @Bean 29 | @ConditionalOnProperty( 30 | prefix="jdempotent", name = "enable", 31 | havingValue = "true", 32 | matchIfMissing = true) 33 | @ConditionalOnClass(ErrorConditionalCallback.class) 34 | public IdempotentAspect getIdempotentAspect(RedisTemplate redisTemplate, ErrorConditionalCallback errorConditionalCallback) { 35 | return new IdempotentAspect(new RedisIdempotentRepository(redisTemplate, redisProperties), errorConditionalCallback); 36 | } 37 | 38 | @Bean 39 | public IdempotentAspect getIdempotentAspect(RedisTemplate redisTemplate) { 40 | return new IdempotentAspect(new RedisIdempotentRepository(redisTemplate, redisProperties)); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/datasource/IdempotentRepository.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.datasource; 2 | 3 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 4 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 5 | import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | /** 10 | * an interface that the functionality required of a request store for idempotent method invocations. 11 | */ 12 | public interface IdempotentRepository { 13 | /** 14 | * @param key 15 | * @return 16 | */ 17 | boolean contains(IdempotencyKey key); 18 | 19 | /** 20 | * Checks the cache for an existing call for this request 21 | * 22 | * @param key 23 | * @return 24 | */ 25 | IdempotentResponseWrapper getResponse(IdempotencyKey key); 26 | 27 | /** 28 | * @param key 29 | * @param requestObject 30 | */ 31 | void store(IdempotencyKey key, IdempotentRequestWrapper requestObject); 32 | 33 | 34 | /** 35 | * 36 | * @param key 37 | * @param requestObject 38 | * @param ttl 39 | * @param timeUnit 40 | */ 41 | void store(IdempotencyKey key, IdempotentRequestWrapper requestObject,Long ttl, TimeUnit timeUnit); 42 | 43 | 44 | /** 45 | * @param key 46 | */ 47 | void remove(IdempotencyKey key); 48 | 49 | /** 50 | * @param request 51 | * @param idempotentResponse 52 | */ 53 | void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse); 54 | 55 | /** 56 | * @param request 57 | * @param idempotentResponse 58 | */ 59 | void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse, Long ttl, TimeUnit timeUnit); 60 | } -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/generator/DefaultKeyGenerator.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.generator; 2 | 3 | import com.trendyol.jdempotent.core.constant.EnvironmentVariableUtils; 4 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 5 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.util.StringUtils; 9 | 10 | import java.security.MessageDigest; 11 | 12 | /** 13 | * 14 | * 15 | */ 16 | public class DefaultKeyGenerator implements KeyGenerator { 17 | 18 | private static final Logger logger = LoggerFactory.getLogger(DefaultKeyGenerator.class); 19 | private final String appName; 20 | 21 | public DefaultKeyGenerator() { 22 | appName = System.getenv(EnvironmentVariableUtils.APP_NAME); 23 | } 24 | 25 | /** 26 | * 27 | * Generates a idempotent key for incoming event 28 | * 29 | * @param requestObject 30 | * @param listenerName 31 | * @param builder 32 | * @param messageDigest 33 | * @return 34 | */ 35 | public IdempotencyKey generateIdempotentKey(IdempotentRequestWrapper requestObject, String listenerName, StringBuilder builder, MessageDigest messageDigest) { 36 | messageDigest.update(requestObject.toString().getBytes()); 37 | byte[] digest = messageDigest.digest(); 38 | 39 | if (!StringUtils.isEmpty(appName)) { 40 | builder.append(appName); 41 | builder.append("-"); 42 | } 43 | 44 | if (!StringUtils.isEmpty(listenerName)) { 45 | builder.append(listenerName); 46 | builder.append("-"); 47 | } 48 | 49 | for (byte b : digest) { 50 | builder.append(Integer.toHexString(0xFF & b)); 51 | } 52 | 53 | return new IdempotencyKey(builder.toString()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/core/TestIdempotentResource.java: -------------------------------------------------------------------------------- 1 | package aspect.core; 2 | 3 | import com.trendyol.jdempotent.core.annotation.JdempotentRequestPayload; 4 | import com.trendyol.jdempotent.core.annotation.JdempotentResource; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class TestIdempotentResource { 9 | @JdempotentResource 10 | public void idempotentMethod(IdempotentTestPayload testObject) { 11 | } 12 | 13 | @JdempotentResource(cachePrefix = "TestIdempotentResource") 14 | public void idempotentMethodThrowingARuntimeException(IdempotentTestPayload testObject) { 15 | throw new TestException(); 16 | } 17 | 18 | @JdempotentResource(cachePrefix = "TestIdempotentResource") 19 | public void idempotentMethodWithThreeParameter(@JdempotentRequestPayload IdempotentTestPayload testObject, IdempotentTestPayload anotherObject, IdempotentTestPayload anotherObject2) { 20 | } 21 | 22 | @JdempotentResource(cachePrefix = "TestIdempotentResource") 23 | public void idempotentMethodWithThreeParamaterAndMultipleJdempotentRequestPayloadAnnotation(IdempotentTestPayload testObject, @JdempotentRequestPayload IdempotentTestPayload anotherObject, @JdempotentRequestPayload Object anotherObject2) { 24 | } 25 | 26 | @JdempotentResource(cachePrefix = "TestIdempotentResource") 27 | public void idempotentMethodWithZeroParamater() { 28 | } 29 | 30 | @JdempotentResource(cachePrefix = "TestIdempotentResource") 31 | public void methodWithTwoParamater(IdempotentTestPayload testObject, IdempotentTestPayload anotherObject) { 32 | } 33 | 34 | @JdempotentResource 35 | public IdempotentTestPayload idempotentMethodReturnArg(IdempotentTestPayload testObject) { 36 | return testObject; 37 | } 38 | 39 | @JdempotentResource 40 | public String idempotencyKeyAsString(@JdempotentRequestPayload String idempotencyKey) { 41 | return idempotencyKey; 42 | } 43 | } -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/listener/WelcomingListener.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.listener; 2 | 3 | import com.trendyol.jdempotent.core.annotation.IdempotentResource; 4 | import com.jdempotent.example.demo.exception.RetryIdempotentRequestException; 5 | import com.jdempotent.example.demo.model.SendEmailRequest; 6 | import com.jdempotent.example.demo.service.MailSenderService; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.kafka.annotation.KafkaListener; 12 | import org.springframework.stereotype.Service; 13 | 14 | import javax.mail.MessagingException; 15 | 16 | @Service 17 | public class WelcomingListener { 18 | 19 | @Autowired 20 | private MailSenderService mailSenderService; 21 | private static final Logger logger = LoggerFactory.getLogger(WelcomingListener.class); 22 | 23 | @Value("${template.welcoming.message}") 24 | private String message; 25 | 26 | @Value("${template.welcoming.subject}") 27 | private String subject; 28 | 29 | @KafkaListener(topics = "trendyol.mail.welcome", groupId = "group_id") 30 | @IdempotentResource 31 | public void consumeMessage(String emailAdress) { 32 | SendEmailRequest request = SendEmailRequest.builder() 33 | .email(message) 34 | .subject(subject) 35 | .build(); 36 | 37 | try { 38 | mailSenderService.sendMail(request); 39 | } catch (MessagingException e) { 40 | logger.error("MailSenderService.sendEmail() throw exception {} event: {} ", e, emailAdress); 41 | 42 | // Throwing any exception is enough to delete from redis. When successful, it will not be deleted from redis and will be idempotent. 43 | throw new RetryIdempotentRequestException(e); 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/listener/WelcomingListener.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.listener; 2 | 3 | import com.trendyol.jdempotent.core.annotation.IdempotentResource; 4 | import com.jdempotent.example.demo.exception.RetryIdempotentRequestException; 5 | import com.jdempotent.example.demo.model.SendEmailRequest; 6 | import com.jdempotent.example.demo.service.MailSenderService; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.kafka.annotation.KafkaListener; 12 | import org.springframework.stereotype.Service; 13 | 14 | import javax.mail.MessagingException; 15 | 16 | @Service 17 | public class WelcomingListener { 18 | 19 | @Autowired 20 | private MailSenderService mailSenderService; 21 | private static final Logger logger = LoggerFactory.getLogger(WelcomingListener.class); 22 | 23 | @Value("${template.welcoming.message}") 24 | private String message; 25 | 26 | @Value("${template.welcoming.subject}") 27 | private String subject; 28 | 29 | @KafkaListener(topics = "trendyol.mail.welcome", groupId = "group_id") 30 | @IdempotentResource 31 | public void consumeMessage(String emailAdress) { 32 | SendEmailRequest request = SendEmailRequest.builder() 33 | .email(message) 34 | .subject(subject) 35 | .build(); 36 | 37 | try { 38 | mailSenderService.sendMail(request); 39 | } catch (MessagingException e) { 40 | logger.error("MailSenderService.sendEmail() throw exception {} event: {} ", e, emailAdress); 41 | 42 | // Throwing any exception is enough to delete from redis. When successful, it will not be deleted from redis and will be idempotent. 43 | throw new RetryIdempotentRequestException(e); 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/service/MailSenderService.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.service; 2 | 3 | import com.jdempotent.example.demo.model.SendEmailRequest; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.mail.javamail.JavaMailSender; 7 | import org.springframework.mail.javamail.MimeMessageHelper; 8 | import org.springframework.stereotype.Service; 9 | 10 | import javax.mail.MessagingException; 11 | import javax.mail.internet.MimeMessage; 12 | import java.io.File; 13 | 14 | @Service 15 | public class MailSenderService { 16 | 17 | private JavaMailSender javaMailSender; 18 | 19 | @Autowired 20 | public MailSenderService(JavaMailSender javaMailSender) { 21 | this.javaMailSender = javaMailSender; 22 | } 23 | 24 | @Value("${email.from.address}") 25 | private String fromAddress; 26 | 27 | public void sendMail(SendEmailRequest emailRequest) throws MessagingException { 28 | sendMailMultipart(emailRequest.getEmail(), emailRequest.getSubject(), emailRequest.getMessage(), null); 29 | } 30 | 31 | public void sendMail(SendEmailRequest emailRequest, File file) throws MessagingException { 32 | sendMailMultipart(emailRequest.getEmail(), emailRequest.getSubject(), emailRequest.getMessage(), file); 33 | } 34 | 35 | private void sendMailMultipart(String toEmail, String subject, String message, File file) throws MessagingException { 36 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 37 | 38 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); 39 | helper.setFrom(fromAddress); 40 | helper.setTo(toEmail); 41 | helper.setSubject(subject); 42 | helper.setText(message); 43 | 44 | if (file != null) { 45 | helper.addAttachment(file.getName(), file); 46 | } 47 | javaMailSender.send(mimeMessage); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/service/MailSenderService.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.service; 2 | 3 | import com.jdempotent.example.demo.model.SendEmailRequest; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.mail.javamail.JavaMailSender; 7 | import org.springframework.mail.javamail.MimeMessageHelper; 8 | import org.springframework.stereotype.Service; 9 | 10 | import javax.mail.MessagingException; 11 | import javax.mail.internet.MimeMessage; 12 | import java.io.File; 13 | 14 | @Service 15 | public class MailSenderService { 16 | 17 | private JavaMailSender javaMailSender; 18 | 19 | @Autowired 20 | public MailSenderService(JavaMailSender javaMailSender) { 21 | this.javaMailSender = javaMailSender; 22 | } 23 | 24 | @Value("${email.from.address}") 25 | private String fromAddress; 26 | 27 | public void sendMail(SendEmailRequest emailRequest) throws MessagingException { 28 | sendMailMultipart(emailRequest.getEmail(), emailRequest.getSubject(), emailRequest.getMessage(), null); 29 | } 30 | 31 | public void sendMail(SendEmailRequest emailRequest, File file) throws MessagingException { 32 | sendMailMultipart(emailRequest.getEmail(), emailRequest.getSubject(), emailRequest.getMessage(), file); 33 | } 34 | 35 | private void sendMailMultipart(String toEmail, String subject, String message, File file) throws MessagingException { 36 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 37 | 38 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); 39 | helper.setFrom(fromAddress); 40 | helper.setTo(toEmail); 41 | helper.setSubject(subject); 42 | helper.setText(message); 43 | 44 | if (file != null) { 45 | helper.addAttachment(file.getName(), file); 46 | } 47 | javaMailSender.send(mimeMessage); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/chain/JdempotentDefaultChainTest.java: -------------------------------------------------------------------------------- 1 | package aspect.chain; 2 | 3 | import aspect.core.IdempotentTestPayload; 4 | import com.trendyol.jdempotent.core.chain.JdempotentDefaultChain; 5 | import com.trendyol.jdempotent.core.model.ChainData; 6 | import com.trendyol.jdempotent.core.model.KeyValuePair; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.mockito.InjectMocks; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | 14 | @RunWith(SpringRunner.class) 15 | public class JdempotentDefaultChainTest { 16 | 17 | @InjectMocks 18 | private JdempotentDefaultChain jdempotentDefaultChain; 19 | 20 | @Test 21 | public void should_process_with_no_annotation() throws IllegalAccessException, NoSuchFieldException { 22 | //Given 23 | IdempotentTestPayload idempotentTestPayload = new IdempotentTestPayload(); 24 | idempotentTestPayload.setName("value"); 25 | ChainData chainData = new ChainData(); 26 | chainData.setArgs(idempotentTestPayload); 27 | chainData.setDeclaredField(idempotentTestPayload.getClass().getDeclaredField("name")); 28 | 29 | //When 30 | KeyValuePair process = jdempotentDefaultChain.process(chainData); 31 | 32 | //Then 33 | assertEquals("name", process.getKey()); 34 | assertEquals("value", process.getValue()); 35 | } 36 | 37 | @Test 38 | public void should_process_with_another_annotated_property() throws IllegalAccessException, NoSuchFieldException { 39 | //Given 40 | IdempotentTestPayload idempotentTestPayload = new IdempotentTestPayload(); 41 | idempotentTestPayload.setEventId(1l); 42 | ChainData chainData = new ChainData(); 43 | chainData.setArgs(idempotentTestPayload); 44 | chainData.setDeclaredField(idempotentTestPayload.getClass().getDeclaredField("eventId")); 45 | 46 | //When 47 | KeyValuePair process = jdempotentDefaultChain.process(chainData); 48 | 49 | //Then 50 | assertEquals("eventId", process.getKey()); 51 | assertEquals(1l, process.getValue()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisEnvironmentPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.redis; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.env.EnvironmentPostProcessor; 9 | import org.springframework.core.env.ConfigurableEnvironment; 10 | import org.springframework.core.env.MapPropertySource; 11 | import org.springframework.core.env.MutablePropertySources; 12 | import org.springframework.core.env.PropertySource; 13 | 14 | @ConditionalOnProperty( 15 | prefix="jdempotent", name = "enable", 16 | havingValue = "true", 17 | matchIfMissing = true) 18 | public class RedisEnvironmentPostProcessor implements EnvironmentPostProcessor { 19 | private static final String PROPERTY_SOURCE_NAME = "defaultProperties"; 20 | 21 | @Override 22 | public void postProcessEnvironment(ConfigurableEnvironment environment, 23 | SpringApplication application) { 24 | Map map = new HashMap(); 25 | map.put("spring.data.redis.repositories.enabled", "false"); 26 | addOrReplace(environment.getPropertySources(), map); 27 | } 28 | 29 | private void addOrReplace(MutablePropertySources propertySources, 30 | Map map) { 31 | MapPropertySource target = null; 32 | if (propertySources.contains(PROPERTY_SOURCE_NAME)) { 33 | PropertySource source = propertySources.get(PROPERTY_SOURCE_NAME); 34 | if (source instanceof MapPropertySource) { 35 | target = (MapPropertySource) source; 36 | for (String key : map.keySet()) { 37 | if (!target.containsProperty(key)) { 38 | target.getSource().put(key, map.get(key)); 39 | } 40 | } 41 | } 42 | } 43 | if (target == null) { 44 | target = new MapPropertySource(PROPERTY_SOURCE_NAME, map); 45 | } 46 | if (!propertySources.contains(PROPERTY_SOURCE_NAME)) { 47 | propertySources.addLast(target); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/chain/JdempotentPropertyAnnotationChainTest.java: -------------------------------------------------------------------------------- 1 | package aspect.chain; 2 | 3 | import aspect.core.IdempotentTestPayload; 4 | import com.trendyol.jdempotent.core.chain.JdempotentDefaultChain; 5 | import com.trendyol.jdempotent.core.chain.JdempotentPropertyAnnotationChain; 6 | import com.trendyol.jdempotent.core.model.ChainData; 7 | import com.trendyol.jdempotent.core.model.KeyValuePair; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.InjectMocks; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | @RunWith(SpringRunner.class) 16 | public class JdempotentPropertyAnnotationChainTest { 17 | 18 | @InjectMocks 19 | private JdempotentPropertyAnnotationChain jdempotentPropertyAnnotationChain; 20 | 21 | @Test 22 | public void should_process_with_no_annotation() throws IllegalAccessException, NoSuchFieldException { 23 | //Given 24 | IdempotentTestPayload idempotentTestPayload = new IdempotentTestPayload(); 25 | idempotentTestPayload.setEventId(1l); 26 | ChainData chainData = new ChainData(); 27 | chainData.setArgs(idempotentTestPayload); 28 | chainData.setDeclaredField(idempotentTestPayload.getClass().getDeclaredField("eventId")); 29 | 30 | //When 31 | KeyValuePair process = jdempotentPropertyAnnotationChain.process(chainData); 32 | 33 | //Then 34 | assertEquals("transactionId", process.getKey()); 35 | assertEquals(1l, process.getValue()); 36 | } 37 | 38 | @Test 39 | public void should_process_with_another_annotated_property() throws IllegalAccessException, NoSuchFieldException { 40 | //Given 41 | IdempotentTestPayload idempotentTestPayload = new IdempotentTestPayload(); 42 | idempotentTestPayload.setEventId(1l); 43 | ChainData chainData = new ChainData(); 44 | chainData.setArgs(idempotentTestPayload); 45 | chainData.setDeclaredField(idempotentTestPayload.getClass().getDeclaredField("eventId")); 46 | 47 | //When 48 | KeyValuePair process = jdempotentPropertyAnnotationChain.process(chainData); 49 | 50 | //Then 51 | assertEquals("transactionId", process.getKey()); 52 | assertEquals(1l, process.getValue()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/test/java/RedisTemplateMocker.java: -------------------------------------------------------------------------------- 1 | import org.mockito.Mockito; 2 | import org.springframework.context.annotation.Bean; 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.redis.connection.RedisConnection; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.HashOperations; 7 | import org.springframework.data.redis.core.ListOperations; 8 | import org.springframework.data.redis.core.RedisOperations; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.data.redis.core.SetOperations; 11 | import org.springframework.data.redis.core.ValueOperations; 12 | import org.springframework.data.redis.core.ZSetOperations; 13 | 14 | import static org.mockito.Mockito.when; 15 | 16 | @Configuration 17 | public class RedisTemplateMocker { 18 | 19 | @Bean 20 | public RedisTemplate redisTemplate() { 21 | RedisTemplate redisTemplate = Mockito.mock(RedisTemplate.class); 22 | ValueOperations valueOperations = Mockito.mock(ValueOperations.class); 23 | SetOperations setOperations = Mockito.mock(SetOperations.class); 24 | HashOperations hashOperations = redisTemplate.opsForHash(); 25 | ListOperations listOperations = redisTemplate.opsForList(); 26 | ZSetOperations zSetOperations = redisTemplate.opsForZSet(); 27 | 28 | when(redisTemplate.opsForSet()).thenReturn(setOperations); 29 | when(redisTemplate.opsForValue()).thenReturn(valueOperations); 30 | when(redisTemplate.opsForHash()).thenReturn(hashOperations); 31 | when(redisTemplate.opsForList()).thenReturn(listOperations); 32 | when(redisTemplate.opsForZSet()).thenReturn(zSetOperations); 33 | 34 | RedisOperations redisOperations = Mockito.mock(RedisOperations.class); 35 | RedisConnection redisConnection = Mockito.mock(RedisConnection.class); 36 | RedisConnectionFactory redisConnectionFactory = Mockito.mock(RedisConnectionFactory.class); 37 | when(redisTemplate.getConnectionFactory()).thenReturn(redisConnectionFactory); 38 | when(valueOperations.getOperations()).thenReturn(redisOperations); 39 | when(redisTemplate.getConnectionFactory().getConnection()).thenReturn(redisConnection); 40 | 41 | return redisTemplate; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/chain/JdempotentNoAnnotationChainTest.java: -------------------------------------------------------------------------------- 1 | package aspect.chain; 2 | 3 | import aspect.core.IdempotentTestPayload; 4 | import com.trendyol.jdempotent.core.chain.JdempotentDefaultChain; 5 | import com.trendyol.jdempotent.core.chain.JdempotentNoAnnotationChain; 6 | import com.trendyol.jdempotent.core.model.ChainData; 7 | import com.trendyol.jdempotent.core.model.KeyValuePair; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.mockito.ArgumentMatchers.eq; 17 | import static org.mockito.Mockito.verify; 18 | 19 | @RunWith(SpringRunner.class) 20 | public class JdempotentNoAnnotationChainTest { 21 | 22 | @InjectMocks 23 | private JdempotentNoAnnotationChain jdempotentNoAnnotationChain; 24 | 25 | @Mock 26 | private JdempotentDefaultChain jdempotentDefaultChain; 27 | 28 | @Before 29 | public void setup(){ 30 | jdempotentNoAnnotationChain.next(jdempotentDefaultChain); 31 | } 32 | 33 | @Test 34 | public void should_process_with_no_annotation() throws IllegalAccessException, NoSuchFieldException { 35 | //Given 36 | MockData mockData = new MockData(); 37 | ChainData chainData = new ChainData(); 38 | chainData.setArgs(mockData); 39 | chainData.setDeclaredField(mockData.getClass().getDeclaredField("name")); 40 | 41 | //When 42 | KeyValuePair process = jdempotentNoAnnotationChain.process(chainData); 43 | 44 | //Then 45 | assertEquals("name", process.getKey()); 46 | assertEquals(null, process.getValue()); 47 | } 48 | 49 | 50 | @Test 51 | public void should_process_with_another_annotated_property() throws IllegalAccessException, NoSuchFieldException { 52 | //Given 53 | IdempotentTestPayload idempotentTestPayload = new IdempotentTestPayload(); 54 | idempotentTestPayload.setEventId(1l); 55 | ChainData chainData = new ChainData(); 56 | chainData.setArgs(idempotentTestPayload); 57 | chainData.setDeclaredField(idempotentTestPayload.getClass().getDeclaredField("eventId")); 58 | 59 | //When 60 | KeyValuePair process = jdempotentNoAnnotationChain.process(chainData); 61 | 62 | //Then 63 | verify(jdempotentDefaultChain).process(eq(chainData)); 64 | } 65 | 66 | class MockData{ 67 | private String name; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/chain/JdempotentIgnoreAnnotationChainTest.java: -------------------------------------------------------------------------------- 1 | package aspect.chain; 2 | 3 | import aspect.core.IdempotentTestPayload; 4 | import com.trendyol.jdempotent.core.chain.JdempotentDefaultChain; 5 | import com.trendyol.jdempotent.core.chain.JdempotentIgnoreAnnotationChain; 6 | import com.trendyol.jdempotent.core.model.ChainData; 7 | import com.trendyol.jdempotent.core.model.KeyValuePair; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.mockito.ArgumentMatchers.eq; 17 | import static org.mockito.Mockito.verify; 18 | 19 | @RunWith(SpringRunner.class) 20 | public class JdempotentIgnoreAnnotationChainTest { 21 | 22 | @InjectMocks 23 | private JdempotentIgnoreAnnotationChain jdempotentIgnoreAnnotationChain; 24 | 25 | @Mock 26 | private JdempotentDefaultChain jdempotentDefaultChain; 27 | 28 | @Before 29 | public void setup(){ 30 | jdempotentIgnoreAnnotationChain.next(jdempotentDefaultChain); 31 | } 32 | 33 | @Test 34 | public void should_process() throws IllegalAccessException, NoSuchFieldException { 35 | //Given 36 | IdempotentTestPayload idempotentTestPayload = new IdempotentTestPayload(); 37 | idempotentTestPayload.setAge(1l); 38 | ChainData chainData = new ChainData(); 39 | chainData.setArgs(idempotentTestPayload); 40 | chainData.setDeclaredField(idempotentTestPayload.getClass().getDeclaredField("age")); 41 | 42 | //When 43 | KeyValuePair process = jdempotentIgnoreAnnotationChain.process(chainData); 44 | 45 | //Then 46 | assertEquals(null, process.getKey()); 47 | assertEquals(null, process.getValue()); 48 | } 49 | 50 | @Test 51 | public void should_not_process_when_given_another_annotated_field() throws IllegalAccessException, NoSuchFieldException { 52 | //Given 53 | IdempotentTestPayload idempotentTestPayload = new IdempotentTestPayload(); 54 | idempotentTestPayload.setName("name"); 55 | ChainData chainData = new ChainData(); 56 | chainData.setArgs(idempotentTestPayload); 57 | chainData.setDeclaredField(idempotentTestPayload.getClass().getDeclaredField("name")); 58 | 59 | //When 60 | jdempotentIgnoreAnnotationChain.process(chainData); 61 | 62 | //Then 63 | verify(jdempotentDefaultChain).process(eq(chainData)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 2 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/datasource/AbstractIdempotentRepository.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.core.datasource; 2 | 3 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 4 | import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; 5 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 6 | import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; 7 | 8 | import java.util.Map; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * Includes all the methods of IdempotentRequestStore 13 | */ 14 | public abstract class AbstractIdempotentRepository implements IdempotentRepository { 15 | 16 | @Override 17 | public boolean contains(IdempotencyKey key) { 18 | return getMap().containsKey(key); 19 | } 20 | 21 | @Override 22 | public IdempotentResponseWrapper getResponse(IdempotencyKey key) { 23 | return getMap().containsKey(key) ? getMap().get(key).getResponse() : null; 24 | } 25 | 26 | @Override 27 | public void store(IdempotencyKey key, IdempotentRequestWrapper request) { 28 | getMap().put(key, new IdempotentRequestResponseWrapper(request)); 29 | } 30 | 31 | @Override 32 | public void store(IdempotencyKey key, IdempotentRequestWrapper request,Long ttl, TimeUnit timeUnit) { 33 | getMap().put(key, new IdempotentRequestResponseWrapper(request)); 34 | } 35 | 36 | @Override 37 | public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, 38 | IdempotentResponseWrapper idempotentResponse) { 39 | if (getMap().containsKey(key)) { 40 | IdempotentRequestResponseWrapper requestResponseWrapper = getMap().get(key); 41 | requestResponseWrapper.setResponse(idempotentResponse); 42 | getMap().put(key, requestResponseWrapper); 43 | } 44 | } 45 | 46 | @Override 47 | public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, 48 | IdempotentResponseWrapper idempotentResponse, Long ttl, TimeUnit timeUnit) { 49 | if (getMap().containsKey(key)) { 50 | IdempotentRequestResponseWrapper requestResponseWrapper = getMap().get(key); 51 | requestResponseWrapper.setResponse(idempotentResponse); 52 | getMap().put(key, requestResponseWrapper); 53 | } 54 | } 55 | 56 | @Override 57 | public void remove(IdempotencyKey key) { 58 | getMap().remove(key); 59 | } 60 | 61 | 62 | /** 63 | * @return 64 | */ 65 | protected abstract Map getMap(); 66 | } -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.5.RELEASE 9 | 10 | 11 | com.jdempotent.example 12 | demo 13 | 0.0.1-SNAPSHOT 14 | Mail Sender App 15 | Demo project for Jdempotent 16 | 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.kafka 32 | spring-kafka 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-mail 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | true 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-test 46 | test 47 | 48 | 49 | org.junit.vintage 50 | junit-vintage-engine 51 | 52 | 53 | 54 | 55 | org.springframework.kafka 56 | spring-kafka-test 57 | test 58 | 59 | 60 | com.trendyol 61 | Jdempotent-spring-boot-redis-starter 62 | 1.1.0 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.5.RELEASE 9 | 10 | 11 | com.jdempotent.example 12 | demo 13 | 0.0.1-SNAPSHOT 14 | Mail Sender App 15 | Demo project for Jdempotent 16 | 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.kafka 32 | spring-kafka 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-mail 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | true 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-test 46 | test 47 | 48 | 49 | org.junit.vintage 50 | junit-vintage-engine 51 | 52 | 53 | 54 | 55 | org.springframework.kafka 56 | spring-kafka-test 57 | test 58 | 59 | 60 | com.trendyol 61 | Jdempotent-spring-boot-couchbase-starter 62 | 1.1.0 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/controller/MailController.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.controller; 2 | 3 | import com.jdempotent.example.demo.exception.InvalidEmailAddressException; 4 | import com.jdempotent.example.demo.model.SendEmailRequest; 5 | import com.jdempotent.example.demo.model.SendEmailResponse; 6 | import com.jdempotent.example.demo.service.MailSenderService; 7 | import com.trendyol.jdempotent.core.annotation.IdempotentResource; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.util.StringUtils; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import javax.mail.MessagingException; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | @RestController 22 | public class MailController { 23 | 24 | @Autowired 25 | private MailSenderService mailSenderService; 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(MailController.class); 28 | 29 | @PostMapping("/send-email") 30 | @IdempotentResource(cachePrefix = "MailController.sendEmail") 31 | public ResponseEntity sendEmail(@RequestBody SendEmailRequest request) { 32 | if (StringUtils.isEmpty(request.getEmail())) { 33 | throw new InvalidEmailAddressException(); 34 | } 35 | 36 | try { 37 | mailSenderService.sendMail(request); 38 | } catch (MessagingException e) { 39 | logger.debug("MailSenderService.sendEmail() throw exception: {} request: {} ", e, request); 40 | } 41 | 42 | return new ResponseEntity(new SendEmailResponse("We will send your message"), HttpStatus.ACCEPTED); 43 | } 44 | 45 | @PostMapping("v2/send-email") 46 | @IdempotentResource( 47 | cachePrefix = "MailController.sendEmailV2", 48 | ttl = 1, 49 | ttlTimeUnit = TimeUnit.MINUTES) 50 | public ResponseEntity sendEmailV2(@RequestBody SendEmailRequest request) { 51 | if (StringUtils.isEmpty(request.getEmail())) { 52 | throw new InvalidEmailAddressException(); 53 | } 54 | 55 | try { 56 | mailSenderService.sendMail(request); 57 | } catch (MessagingException e) { 58 | logger.debug("MailSenderService.sendEmail() throw exception: {} request: {} ", e, request); 59 | } 60 | 61 | return new ResponseEntity(new SendEmailResponse("We will send your message"), HttpStatus.ACCEPTED); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisSentinelConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.redis; 2 | 3 | 4 | import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.data.redis.connection.RedisConfiguration; 11 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 14 | import org.springframework.data.redis.serializer.StringRedisSerializer; 15 | 16 | /** 17 | * 18 | */ 19 | @Configuration 20 | @ConditionalOnProperty( 21 | prefix="jdempotent", name = "enable", 22 | havingValue = "true", 23 | matchIfMissing = true) 24 | public class RedisSentinelConfiguration { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(RedisConfiguration.class); 27 | 28 | private final RedisConfigProperties redisProperties; 29 | 30 | public RedisSentinelConfiguration(RedisConfigProperties redisProperties) { 31 | this.redisProperties = redisProperties; 32 | } 33 | 34 | @Bean 35 | public LettuceConnectionFactory lettuceConnectionFactory() { 36 | org.springframework.data.redis.connection.RedisSentinelConfiguration sentinelConfiguration = new org.springframework.data.redis.connection.RedisSentinelConfiguration() 37 | .master(redisProperties.getSentinelMasterName()); 38 | redisProperties.getSentinelHostList().forEach( 39 | host -> sentinelConfiguration.sentinel(host, redisProperties.getSentinelPort())); 40 | sentinelConfiguration.setPassword(redisProperties.getPassword()); 41 | sentinelConfiguration.setDatabase(redisProperties.getDatabase()); 42 | return new LettuceConnectionFactory(sentinelConfiguration, 43 | org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration 44 | .defaultConfiguration()); 45 | } 46 | 47 | @Bean 48 | public RedisTemplate redisTemplate() { 49 | RedisTemplate redisTemplate = new RedisTemplate<>(); 50 | redisTemplate.setConnectionFactory(lettuceConnectionFactory()); 51 | redisTemplate.afterPropertiesSet(); 52 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 53 | redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer()); 54 | return redisTemplate; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.couchbase; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @ConditionalOnProperty( 8 | prefix="jdempotent", name = "enable", 9 | havingValue = "true", 10 | matchIfMissing = true) 11 | @Configuration 12 | public class CouchbaseConfig { 13 | @Value("${jdempotent.cache.couchbase.connection-string}") 14 | private String connectionString; 15 | @Value("${jdempotent.cache.couchbase.username}") 16 | private String username; 17 | @Value("${jdempotent.cache.couchbase.password}") 18 | private String password; 19 | @Value("${jdempotent.cache.couchbase.bucket-name}") 20 | private String bucketName; 21 | @Value("${jdempotent.cache.couchbase.connect-timeout}") 22 | private Long connectTimeout; 23 | @Value("${jdempotent.cache.couchbase.query-timeout}") 24 | private Long queryTimeout; 25 | @Value("${jdempotent.cache.couchbase.kv-timeout}") 26 | private Long kvTimeout; 27 | @Value("${jdempotent.cache.persistReqRes:false}") 28 | private Boolean persistReqRes; 29 | 30 | public String getConnectionString() { 31 | return connectionString; 32 | } 33 | 34 | public void setConnectionString(String connectionString) { 35 | this.connectionString = connectionString; 36 | } 37 | 38 | public String getUsername() { 39 | return username; 40 | } 41 | 42 | public void setUsername(String username) { 43 | this.username = username; 44 | } 45 | 46 | public String getPassword() { 47 | return password; 48 | } 49 | 50 | public void setPassword(String password) { 51 | this.password = password; 52 | } 53 | 54 | public String getBucketName() { 55 | return bucketName; 56 | } 57 | 58 | public void setBucketName(String bucketName) { 59 | this.bucketName = bucketName; 60 | } 61 | 62 | public Long getConnectTimeout() { 63 | return connectTimeout; 64 | } 65 | 66 | public void setConnectTimeout(Long connectTimeout) { 67 | this.connectTimeout = connectTimeout; 68 | } 69 | 70 | public Long getQueryTimeout() { 71 | return queryTimeout; 72 | } 73 | 74 | public void setQueryTimeout(Long queryTimeout) { 75 | this.queryTimeout = queryTimeout; 76 | } 77 | 78 | public Long getKvTimeout() { 79 | return kvTimeout; 80 | } 81 | 82 | public void setKvTimeout(Long kvTimeout) { 83 | this.kvTimeout = kvTimeout; 84 | } 85 | 86 | public Boolean getPersistReqRes() { 87 | return persistReqRes; 88 | } 89 | 90 | public void setPersistReqRes(Boolean persistReqRes) { 91 | this.persistReqRes = persistReqRes; 92 | } 93 | } -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.couchbase; 2 | 3 | import com.couchbase.client.core.deps.io.netty.channel.epoll.EpollEventLoopGroup; 4 | import com.couchbase.client.core.env.*; 5 | import com.couchbase.client.java.Cluster; 6 | import com.couchbase.client.java.ClusterOptions; 7 | import com.couchbase.client.java.Collection; 8 | import com.couchbase.client.java.codec.JacksonJsonSerializer; 9 | import com.couchbase.client.java.env.ClusterEnvironment; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import org.apache.commons.lang3.SystemUtils; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.context.annotation.Primary; 16 | 17 | import java.time.Duration; 18 | 19 | @Configuration 20 | @ConditionalOnProperty( 21 | prefix="jdempotent", name = "enable", 22 | havingValue = "true", 23 | matchIfMissing = true) 24 | public class CouchbaseBeanConfig { 25 | 26 | private final CouchbaseConfig couchbaseConfig; 27 | 28 | public CouchbaseBeanConfig(CouchbaseConfig couchbaseConfig) { 29 | this.couchbaseConfig = couchbaseConfig; 30 | } 31 | 32 | @Bean 33 | @Primary 34 | public Cluster cluster(ObjectMapper objectMapper) { 35 | var builder = ClusterEnvironment.builder(); 36 | if (SystemUtils.IS_OS_LINUX) { 37 | builder.ioEnvironment( 38 | IoEnvironment.kvEventLoopGroup( 39 | new EpollEventLoopGroup( 40 | Runtime.getRuntime().availableProcessors() * 2 41 | ) 42 | ) 43 | ) 44 | .ioConfig(IoConfig.configPollInterval(Duration.ofSeconds(10))) 45 | .securityConfig(SecurityConfig.enableNativeTls(false).enableTls(false)); 46 | } 47 | var couchbaseEnvironment = builder 48 | .jsonSerializer(JacksonJsonSerializer.create(objectMapper)) 49 | .timeoutConfig( 50 | TimeoutConfig.kvTimeout(Duration.ofMillis(couchbaseConfig.getKvTimeout())) 51 | .connectTimeout(Duration.ofMillis(couchbaseConfig.getConnectTimeout())) 52 | .queryTimeout(Duration.ofMillis(couchbaseConfig.getQueryTimeout())) 53 | ) 54 | .compressionConfig(CompressionConfig.enable(true)) 55 | .loggerConfig(LoggerConfig.enableDiagnosticContext(false)) 56 | .build(); 57 | return Cluster.connect( 58 | couchbaseConfig.getConnectionString(), 59 | ClusterOptions.clusterOptions(couchbaseConfig.getUsername(), couchbaseConfig.getPassword()) 60 | .environment(couchbaseEnvironment) 61 | ); 62 | } 63 | 64 | @Bean 65 | @Primary 66 | public Collection collection(ObjectMapper objectMapper) { 67 | return cluster(objectMapper).bucket(couchbaseConfig.getBucketName()).defaultCollection(); 68 | } 69 | 70 | @Bean 71 | public ObjectMapper objectMapper() { 72 | return new ObjectMapper(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/errorcallback/IdempotentAspectWithErrorCallbackIT.java: -------------------------------------------------------------------------------- 1 | package aspect.errorcallback; 2 | 3 | import aspect.core.IdempotentTestPayload; 4 | import aspect.core.TestException; 5 | import aspect.core.TestIdempotentResource; 6 | import com.trendyol.jdempotent.core.constant.CryptographyAlgorithm; 7 | import com.trendyol.jdempotent.core.datasource.InMemoryIdempotentRepository; 8 | import com.trendyol.jdempotent.core.generator.DefaultKeyGenerator; 9 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 10 | import com.trendyol.jdempotent.core.model.IdempotentIgnorableWrapper; 11 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.test.context.ContextConfiguration; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | 18 | import java.security.MessageDigest; 19 | import java.security.NoSuchAlgorithmException; 20 | 21 | import static org.junit.Assert.*; 22 | 23 | @RunWith(SpringRunner.class) 24 | @ContextConfiguration(classes = {IdempotentAspectWithErrorCallbackIT.class, TestAopWithErrorCallbackContext.class, TestIdempotentResource.class, DefaultKeyGenerator.class, InMemoryIdempotentRepository.class}) 25 | public class IdempotentAspectWithErrorCallbackIT { 26 | 27 | @Autowired 28 | private TestIdempotentResource testIdempotentResource; 29 | 30 | @Autowired 31 | private InMemoryIdempotentRepository idempotentRepository; 32 | 33 | @Autowired 34 | private DefaultKeyGenerator defaultKeyGenerator; 35 | 36 | @Autowired 37 | private TestCustomErrorCallback testCustomErrorCallback; 38 | 39 | @Test 40 | public void given_valid_payload_when_trigger_aspect_then_not_throw_custom_error_callback_and_save_repository() throws NoSuchAlgorithmException { 41 | //given 42 | IdempotentTestPayload test = new IdempotentTestPayload(); 43 | test.setName("another"); 44 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 45 | wrapper.getNonIgnoredFields().put("name", "another"); 46 | IdempotencyKey idempotencyKey = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 47 | 48 | //when 49 | testIdempotentResource.idempotentMethodReturnArg(test); 50 | 51 | //then 52 | assertTrue(idempotentRepository.contains(idempotencyKey)); 53 | } 54 | 55 | @Test(expected = TestException.class) 56 | public void given_invalid_payload_when_trigger_aspect_then_throw_test_exception_from_custom_error_callback_and_remove_repository() throws NoSuchAlgorithmException { 57 | //given 58 | IdempotentTestPayload test = new IdempotentTestPayload(); 59 | test.setName("test"); 60 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 61 | wrapper.getNonIgnoredFields().put("name", "test"); 62 | IdempotencyKey idempotencyKey = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "TestIdempotentResource", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 63 | 64 | //when 65 | testIdempotentResource.idempotentMethodReturnArg(test); 66 | 67 | //then 68 | assertFalse(idempotentRepository.contains(idempotencyKey)); 69 | } 70 | } -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/src/main/java/com/jdempotent/example/demo/controller/MailController.java: -------------------------------------------------------------------------------- 1 | package com.jdempotent.example.demo.controller; 2 | 3 | import com.jdempotent.example.demo.exception.InvalidEmailAddressException; 4 | import com.jdempotent.example.demo.model.SendEmailRequest; 5 | import com.jdempotent.example.demo.model.SendEmailResponse; 6 | import com.jdempotent.example.demo.service.MailSenderService; 7 | import com.trendyol.jdempotent.core.annotation.IdempotentResource; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.util.StringUtils; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RestController; 17 | import com.trendyol.jdempotent.core.annotation.JdempotentRequestPayload; 18 | import javax.mail.MessagingException; 19 | import java.util.concurrent.TimeUnit; 20 | import org.springframework.web.bind.annotation.RequestHeader; 21 | 22 | @RestController 23 | public class MailController { 24 | 25 | @Autowired 26 | private MailSenderService mailSenderService; 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(MailController.class); 29 | 30 | @PostMapping("/send-email") 31 | @IdempotentResource(cachePrefix = "MailController.sendEmail") 32 | public ResponseEntity sendEmail(@RequestBody SendEmailRequest request) { 33 | if (StringUtils.isEmpty(request.getEmail())) { 34 | throw new InvalidEmailAddressException(); 35 | } 36 | 37 | try { 38 | mailSenderService.sendMail(request); 39 | } catch (MessagingException e) { 40 | logger.debug("MailSenderService.sendEmail() throw exception: {} request: {} ", e, request); 41 | } 42 | 43 | return new ResponseEntity(new SendEmailResponse("We will send your message"), HttpStatus.ACCEPTED); 44 | } 45 | 46 | 47 | 48 | @PostMapping("/send-email-header") 49 | @IdempotentResource(cachePrefix = "MailController.sendEmail") 50 | public ResponseEntity sendEmail(@JdempotentRequestPayload @RequestHeader("x-idempotency-key") String idempotencyKey, @RequestBody SendEmailRequest request) { 51 | if (StringUtils.isEmpty(request.getEmail())) { 52 | throw new InvalidEmailAddressException(); 53 | } 54 | 55 | try { 56 | mailSenderService.sendMail(request); 57 | } catch (MessagingException e) { 58 | logger.debug("MailSenderService.sendEmail() throw exception: {} request: {} ", e, request); 59 | } 60 | 61 | return new ResponseEntity(new SendEmailResponse("We will send your message"), HttpStatus.ACCEPTED); 62 | } 63 | 64 | @PostMapping("v2/send-email") 65 | @IdempotentResource( 66 | cachePrefix = "MailController.sendEmailV2", 67 | ttl = 1, 68 | ttlTimeUnit = TimeUnit.MINUTES) 69 | public ResponseEntity sendEmailV2(@RequestBody SendEmailRequest request) { 70 | if (StringUtils.isEmpty(request.getEmail())) { 71 | throw new InvalidEmailAddressException(); 72 | } 73 | 74 | try { 75 | mailSenderService.sendMail(request); 76 | } catch (MessagingException e) { 77 | logger.debug("MailSenderService.sendEmail() throw exception: {} request: {} ", e, request); 78 | } 79 | 80 | return new ResponseEntity(new SendEmailResponse("We will send your message"), HttpStatus.ACCEPTED); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisConfigProperties.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.redis; 2 | 3 | 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * 12 | */ 13 | @Configuration 14 | @ConditionalOnProperty( 15 | prefix="jdempotent", name = "enable", 16 | havingValue = "true", 17 | matchIfMissing = true) 18 | public class RedisConfigProperties { 19 | 20 | @Value("${jdempotent.cache.redis.database:0}") 21 | private Integer database; 22 | 23 | @Value("${jdempotent.cache.redis.sentinelPort}") 24 | private Integer sentinelPort; 25 | 26 | @Value("${jdempotent.cache.redis.password}") 27 | private String password; 28 | 29 | @Value("${jdempotent.cache.redis.sentinelMasterName}") 30 | private String sentinelMasterName; 31 | 32 | @Value("${jdempotent.cache.redis.sentinelHostList}") 33 | private List sentinelHostList; 34 | 35 | @Value("${jdempotent.cache.redis.expirationTimeHour}") 36 | private Long expirationTimeHour; 37 | 38 | @Value("${jdempotent.cache.redis.dialTimeoutSecond}") 39 | private String dialTimeoutSecond; 40 | 41 | @Value("${jdempotent.cache.redis.readTimeoutSecond}") 42 | private String readTimeoutSecond; 43 | 44 | @Value("${jdempotent.cache.redis.writeTimeoutSecond}") 45 | private String writeTimeoutSecond; 46 | 47 | @Value("${jdempotent.cache.redis.maxRetryCount}") 48 | private String maxRetryCount; 49 | 50 | @Value("${jdempotent.cache.persistReqRes:true}") 51 | private Boolean persistReqRes; 52 | 53 | public Boolean getPersistReqRes() { 54 | return persistReqRes; 55 | } 56 | 57 | public void setPersistReqRes(Boolean persistReqRes) { 58 | this.persistReqRes = persistReqRes; 59 | } 60 | 61 | public Integer getSentinelPort() { 62 | return sentinelPort; 63 | } 64 | 65 | public void setSentinelPort(Integer sentinelPort) { 66 | this.sentinelPort = sentinelPort; 67 | } 68 | 69 | public String getPassword() { 70 | return password; 71 | } 72 | 73 | public void setPassword(String password) { 74 | this.password = password; 75 | } 76 | 77 | public String getSentinelMasterName() { 78 | return sentinelMasterName; 79 | } 80 | 81 | public void setSentinelMasterName(String sentinelMasterName) { 82 | this.sentinelMasterName = sentinelMasterName; 83 | } 84 | 85 | public List getSentinelHostList() { 86 | return sentinelHostList; 87 | } 88 | 89 | public void setSentinelHostList(List sentinelHostList) { 90 | this.sentinelHostList = sentinelHostList; 91 | } 92 | 93 | public Long getExpirationTimeHour() { 94 | return expirationTimeHour; 95 | } 96 | 97 | public void setExpirationTimeHour(Long expirationTimeHour) { 98 | this.expirationTimeHour = expirationTimeHour; 99 | } 100 | 101 | public String getDialTimeoutSecond() { 102 | return dialTimeoutSecond; 103 | } 104 | 105 | public void setDialTimeoutSecond(String dialTimeoutSecond) { 106 | this.dialTimeoutSecond = dialTimeoutSecond; 107 | } 108 | 109 | public String getReadTimeoutSecond() { 110 | return readTimeoutSecond; 111 | } 112 | 113 | public void setReadTimeoutSecond(String readTimeoutSecond) { 114 | this.readTimeoutSecond = readTimeoutSecond; 115 | } 116 | 117 | public String getWriteTimeoutSecond() { 118 | return writeTimeoutSecond; 119 | } 120 | 121 | public void setWriteTimeoutSecond(String writeTimeoutSecond) { 122 | this.writeTimeoutSecond = writeTimeoutSecond; 123 | } 124 | 125 | public String getMaxRetryCount() { 126 | return maxRetryCount; 127 | } 128 | 129 | public void setMaxRetryCount(String maxRetryCount) { 130 | this.maxRetryCount = maxRetryCount; 131 | } 132 | 133 | public Integer getDatabase() { 134 | return database; 135 | } 136 | 137 | public void setDatabase(Integer database) { 138 | this.database = database; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisIdempotentRepository.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.redis; 2 | 3 | import com.trendyol.jdempotent.core.datasource.IdempotentRepository; 4 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 5 | import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; 6 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 7 | import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | import org.springframework.data.redis.core.ValueOperations; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * 15 | * An implementation of the idempotent IdempotentRepository 16 | * that uses a distributed hash map from Redis 17 | * 18 | * That repository needs to store idempotent hash for idempotency check 19 | * 20 | */ 21 | public class RedisIdempotentRepository implements IdempotentRepository { 22 | 23 | private final ValueOperations valueOperations; 24 | private final RedisTemplate redisTemplate; 25 | private final RedisConfigProperties redisProperties; 26 | 27 | 28 | public RedisIdempotentRepository(RedisTemplate redisTemplate, RedisConfigProperties redisProperties) { 29 | valueOperations = redisTemplate.opsForValue(); 30 | this.redisTemplate = redisTemplate; 31 | this.redisProperties = redisProperties; 32 | } 33 | 34 | @Override 35 | public boolean contains(IdempotencyKey idempotencyKey) { 36 | return valueOperations.get(idempotencyKey.getKeyValue()) != null; 37 | } 38 | 39 | @Override 40 | public IdempotentResponseWrapper getResponse(IdempotencyKey idempotencyKey) { 41 | return valueOperations.get(idempotencyKey.getKeyValue()).getResponse(); 42 | } 43 | 44 | @Override 45 | @Deprecated 46 | public void store(IdempotencyKey idempotencyKey, IdempotentRequestWrapper request) { 47 | valueOperations.set(idempotencyKey.getKeyValue(), prepareValue(request), redisProperties.getExpirationTimeHour(), TimeUnit.HOURS); 48 | } 49 | 50 | @Override 51 | public void store(IdempotencyKey idempotencyKey, IdempotentRequestWrapper request, Long ttl, TimeUnit timeUnit) { 52 | ttl = ttl == 0 ? redisProperties.getExpirationTimeHour() : ttl; 53 | valueOperations.set(idempotencyKey.getKeyValue(), prepareValue(request), ttl, timeUnit); 54 | } 55 | 56 | @Override 57 | public void remove(IdempotencyKey idempotencyKey) { 58 | redisTemplate.delete(idempotencyKey.getKeyValue()); 59 | } 60 | 61 | @Override 62 | @Deprecated 63 | public void setResponse(IdempotencyKey idempotencyKey, IdempotentRequestWrapper request, IdempotentResponseWrapper response) { 64 | if (contains(idempotencyKey)) { 65 | IdempotentRequestResponseWrapper requestResponseWrapper = valueOperations.get(idempotencyKey); 66 | requestResponseWrapper.setResponse(response); 67 | valueOperations.set(idempotencyKey.getKeyValue(), prepareValue(request), redisProperties.getExpirationTimeHour(), TimeUnit.HOURS); 68 | } 69 | } 70 | 71 | /** 72 | * ttl describe 73 | * 74 | * @param idempotencyKey 75 | * @param request 76 | * @param response 77 | * @param ttl 78 | */ 79 | @Override 80 | public void setResponse(IdempotencyKey idempotencyKey, IdempotentRequestWrapper request, IdempotentResponseWrapper response, Long ttl, TimeUnit timeUnit) { 81 | if (contains(idempotencyKey)) { 82 | ttl = ttl == 0 ? redisProperties.getExpirationTimeHour() : ttl; 83 | IdempotentRequestResponseWrapper requestResponseWrapper = valueOperations.get(idempotencyKey.getKeyValue()); 84 | requestResponseWrapper.setResponse(response); 85 | valueOperations.set(idempotencyKey.getKeyValue(), prepareValue(request, response), ttl, timeUnit); 86 | } 87 | } 88 | 89 | /** 90 | * Prepares the value stored in redis 91 | * 92 | * if persistReqRes set to false, 93 | * it does not persist related request values in redis 94 | * @param request 95 | * @return 96 | */ 97 | private IdempotentRequestResponseWrapper prepareValue(IdempotentRequestWrapper request) { 98 | if (redisProperties.getPersistReqRes()) { 99 | return new IdempotentRequestResponseWrapper(request); 100 | } 101 | return new IdempotentRequestResponseWrapper(null); 102 | } 103 | 104 | /** 105 | * Prepares the value stored in redis 106 | * 107 | * if persistReqRes set to false, 108 | * it does not persist related request and response values in redis 109 | * @param request 110 | * @param response 111 | * @return 112 | */ 113 | private IdempotentRequestResponseWrapper prepareValue(IdempotentRequestWrapper request, IdempotentResponseWrapper response) { 114 | if (redisProperties.getPersistReqRes()) { 115 | return new IdempotentRequestResponseWrapper(request, response); 116 | } 117 | return new IdempotentRequestResponseWrapper(null); 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import java.net.*; 18 | import java.io.*; 19 | import java.nio.channels.*; 20 | import java.util.Properties; 21 | 22 | public class MavenWrapperDownloader { 23 | 24 | private static final String WRAPPER_VERSION = "0.5.6"; 25 | /** 26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 27 | */ 28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 30 | 31 | /** 32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 33 | * use instead of the default one. 34 | */ 35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 36 | ".mvn/wrapper/maven-wrapper.properties"; 37 | 38 | /** 39 | * Path where the maven-wrapper.jar will be saved to. 40 | */ 41 | private static final String MAVEN_WRAPPER_JAR_PATH = 42 | ".mvn/wrapper/maven-wrapper.jar"; 43 | 44 | /** 45 | * Name of the property which should be used to override the default download url for the wrapper. 46 | */ 47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 48 | 49 | public static void main(String args[]) { 50 | System.out.println("- Downloader started"); 51 | File baseDirectory = new File(args[0]); 52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 53 | 54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 55 | // wrapperUrl parameter. 56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 57 | String url = DEFAULT_DOWNLOAD_URL; 58 | if (mavenWrapperPropertyFile.exists()) { 59 | FileInputStream mavenWrapperPropertyFileInputStream = null; 60 | try { 61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 62 | Properties mavenWrapperProperties = new Properties(); 63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 65 | } catch (IOException e) { 66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 67 | } finally { 68 | try { 69 | if (mavenWrapperPropertyFileInputStream != null) { 70 | mavenWrapperPropertyFileInputStream.close(); 71 | } 72 | } catch (IOException e) { 73 | // Ignore ... 74 | } 75 | } 76 | } 77 | System.out.println("- Downloading from: " + url); 78 | 79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 80 | if (!outputFile.getParentFile().exists()) { 81 | if (!outputFile.getParentFile().mkdirs()) { 82 | System.out.println( 83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 84 | } 85 | } 86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 87 | try { 88 | downloadFileFromURL(url, outputFile); 89 | System.out.println("Done"); 90 | System.exit(0); 91 | } catch (Throwable e) { 92 | System.out.println("- Error downloading"); 93 | e.printStackTrace(); 94 | System.exit(1); 95 | } 96 | } 97 | 98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 100 | String username = System.getenv("MVNW_USERNAME"); 101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 102 | Authenticator.setDefault(new Authenticator() { 103 | @Override 104 | protected PasswordAuthentication getPasswordAuthentication() { 105 | return new PasswordAuthentication(username, password); 106 | } 107 | }); 108 | } 109 | URL website = new URL(urlString); 110 | ReadableByteChannel rbc; 111 | rbc = Channels.newChannel(website.openStream()); 112 | FileOutputStream fos = new FileOutputStream(destination); 113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 114 | fos.close(); 115 | rbc.close(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import java.net.*; 18 | import java.io.*; 19 | import java.nio.channels.*; 20 | import java.util.Properties; 21 | 22 | public class MavenWrapperDownloader { 23 | 24 | private static final String WRAPPER_VERSION = "0.5.6"; 25 | /** 26 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 27 | */ 28 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 29 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 30 | 31 | /** 32 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 33 | * use instead of the default one. 34 | */ 35 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 36 | ".mvn/wrapper/maven-wrapper.properties"; 37 | 38 | /** 39 | * Path where the maven-wrapper.jar will be saved to. 40 | */ 41 | private static final String MAVEN_WRAPPER_JAR_PATH = 42 | ".mvn/wrapper/maven-wrapper.jar"; 43 | 44 | /** 45 | * Name of the property which should be used to override the default download url for the wrapper. 46 | */ 47 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 48 | 49 | public static void main(String args[]) { 50 | System.out.println("- Downloader started"); 51 | File baseDirectory = new File(args[0]); 52 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 53 | 54 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 55 | // wrapperUrl parameter. 56 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 57 | String url = DEFAULT_DOWNLOAD_URL; 58 | if (mavenWrapperPropertyFile.exists()) { 59 | FileInputStream mavenWrapperPropertyFileInputStream = null; 60 | try { 61 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 62 | Properties mavenWrapperProperties = new Properties(); 63 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 64 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 65 | } catch (IOException e) { 66 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 67 | } finally { 68 | try { 69 | if (mavenWrapperPropertyFileInputStream != null) { 70 | mavenWrapperPropertyFileInputStream.close(); 71 | } 72 | } catch (IOException e) { 73 | // Ignore ... 74 | } 75 | } 76 | } 77 | System.out.println("- Downloading from: " + url); 78 | 79 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 80 | if (!outputFile.getParentFile().exists()) { 81 | if (!outputFile.getParentFile().mkdirs()) { 82 | System.out.println( 83 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 84 | } 85 | } 86 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 87 | try { 88 | downloadFileFromURL(url, outputFile); 89 | System.out.println("Done"); 90 | System.exit(0); 91 | } catch (Throwable e) { 92 | System.out.println("- Error downloading"); 93 | e.printStackTrace(); 94 | System.exit(1); 95 | } 96 | } 97 | 98 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 99 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 100 | String username = System.getenv("MVNW_USERNAME"); 101 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 102 | Authenticator.setDefault(new Authenticator() { 103 | @Override 104 | protected PasswordAuthentication getPasswordAuthentication() { 105 | return new PasswordAuthentication(username, password); 106 | } 107 | }); 108 | } 109 | URL website = new URL(urlString); 110 | ReadableByteChannel rbc; 111 | rbc = Channels.newChannel(website.openStream()); 112 | FileOutputStream fos = new FileOutputStream(destination); 113 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 114 | fos.close(); 115 | rbc.close(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jdempotent 2 | 3 | [![Release Jdempotent](https://github.com/Trendyol/Jdempotent/actions/workflows/jdempotent-spring-boot-redis-starter.yml/badge.svg)](https://github.com/Trendyol/Jdempotent/actions/workflows/jdempotent-spring-boot-redis-starter.yml) 4 | 5 |

6 | 7 |

8 | 9 | # Goal of this Jdempotent-spring-boot-starter 10 | 11 | Make your endpoints idempotent easily 12 | 13 | # Usage 14 | 15 | 1. First of all, you need to add a dependency to pom.xml 16 | 17 | For Redis: 18 | 19 | ```xml 20 | 21 | com.trendyol 22 | Jdempotent-spring-boot-redis-starter 23 | 1.1.0 24 | 25 | ``` 26 | For Couchbase: 27 | 28 | ```xml 29 | 30 | com.trendyol 31 | Jdempotent-spring-boot-couchbase-starter 32 | 1.1.0 33 | 34 | ``` 35 | 36 | 2. You should add `@IdempotentResource` annotation to the method that you want to make idempotent resource, listener etc. 37 | 38 | ```java 39 | @IdempotentResource(cachePrefix = "WelcomingListener") 40 | @KafkaListener(topics = "trendyol.mail.welcome", groupId = "group_id") 41 | public void consumeMessage(@IdempotentRequestPayload String emailAdress) { 42 | SendEmailRequest request = SendEmailRequest.builder() 43 | .email(message) 44 | .subject(subject) 45 | .build(); 46 | 47 | try { 48 | mailSenderService.sendMail(request); 49 | } catch (MessagingException e) { 50 | logger.error("MailSenderService.sendEmail() throw exception {} event: {} ", e, emailAdress); 51 | 52 | // Throwing any exception is enough to delete from redis. When successful, it will not be deleted from redis and will be idempotent. 53 | throw new RetryIdempotentRequestException(e); 54 | } 55 | } 56 | ``` 57 | 58 | If want that idempotencyId in your payload. Put `@JdempotentId` annotation that places the generated idempotency identifier into annotated field. 59 | Can be thought of as @Id annotation in jpa. 60 | 61 | ```java 62 | public class IdempotentPayload { 63 | @JdempotentId 64 | private String jdempotentId; 65 | private Object data; 66 | } 67 | ``` 68 | 69 | You might want to handle the name of the field differently to ensure idempotency. Just use @JdempotentProperty annotation needs to get the field name differently and generate the hash inspired by jackson (@JsonProperty annotation) 70 | 71 | ```java 72 | public class IdempotentPayload { 73 | @JdempotentProperty("userId") 74 | private String customerId; 75 | private Object data; 76 | } 77 | ``` 78 | 79 | 80 | 3. If you want to handle a custom error case, you need to implement `ErrorConditionalCallback` like the following example: 81 | 82 | ```java 83 | @Component 84 | public class AspectConditionalCallback implements ErrorConditionalCallback { 85 | 86 | @Override 87 | public boolean onErrorCondition(Object response) { 88 | return response == IdempotentStateEnum.ERROR; 89 | } 90 | 91 | public RuntimeException onErrorCustomException() { 92 | return new RuntimeException("Status cannot be error"); 93 | } 94 | 95 | } 96 | ``` 97 | 98 | 4. Let's make the configuration: 99 | 100 | For redis configuration: 101 | 102 | ```yaml 103 | jdempotent: 104 | enable: true 105 | cache: 106 | redis: 107 | database: 1 108 | password: "password" 109 | sentinelHostList: 192.168.0.1,192.168.0.2,192.168.0.3 110 | sentinelPort: "26379" 111 | sentinelMasterName: "admin" 112 | expirationTimeHour: 2 113 | dialTimeoutSecond: 3 114 | readTimeoutSecond: 3 115 | writeTimeoutSecond: 3 116 | maxRetryCount: 3 117 | expireTimeoutHour: 3 118 | ``` 119 | 120 | For couchbase configuration: 121 | 122 | ```yaml 123 | jdempotent: 124 | enable: true 125 | cryptography: 126 | algorithm: MD5 127 | cache: 128 | couchbase: 129 | connection-string: XXXXXXXX 130 | password: XXXXXXXX 131 | username: XXXXXXXX 132 | bucket-name: XXXXXXXX 133 | connect-timeout: 100000 134 | query-timeout: 20000 135 | kv-timeout: 3000 136 | ``` 137 | 138 | Please note that you can disable Jdempotent easily if you need to. 139 | For example, assume that you don't have a circuit breaker and your Redis is down. 140 | In that case, you can disable Jdempotent with the following configuration: 141 | 142 | 143 | ```yaml 144 | enable: false 145 | ``` 146 | 147 | ```java 148 | @SpringBootApplication( 149 | exclude = { RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class } 150 | ) 151 | ``` 152 | 153 | ## Performance 154 | 155 | As it is shown in the following image, the most cpu consuming part of Jdempotent is getting a Redis connection so we don't need to worry performance related issues. 156 | 157 |

158 | 159 |

160 | 161 | # Docs 162 | 163 | [Jdempotent Medium Article](https://medium.com/trendyol-tech/an-idempotency-library-jdempotent-5cd2cd0b76ff)
164 | [Jdempotent-core Javadoc](https://memojja.github.io/jdempotent-core/index.html)
165 | [Jdempotent-spring-boot-redis-starter Javadoc](https://memojja.github.io/jdempotent-spring-boot-redis-starter/index.html) 166 | 167 | ## Support 168 | 169 | [memojja's twitter](https://twitter.com/memojja)
170 | 171 | ## Licence 172 | 173 | [MIT Licence](https://opensource.org/licenses/MIT)
174 | 175 | ## Contributing 176 | 177 | 1. Fork it ( https://github.com/Trendyol/Jdempotent/fork ) 178 | 2. Create your feature branch (git checkout -b my-new-feature) 179 | 3. Commit your changes (git commit -am 'Add some feature') 180 | 4. Push to the branch (git push origin my-new-feature) 181 | 5. Create a new Pull Request 182 | 183 | ## Contributors 184 | 185 | - [memojja](https://github.com/memojja) Mehmet ARI - creator, maintainer 186 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.couchbase; 2 | 3 | import com.couchbase.client.java.Collection; 4 | import com.couchbase.client.java.kv.GetOptions; 5 | import com.couchbase.client.java.kv.GetResult; 6 | import com.couchbase.client.java.kv.UpsertOptions; 7 | import com.trendyol.jdempotent.core.datasource.IdempotentRepository; 8 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 9 | import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; 10 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 11 | import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; 12 | 13 | import java.time.Duration; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.function.Function; 18 | 19 | /** 20 | * An implementation of the idempotent IdempotentRepository 21 | * that uses a distributed hash map from Couchbase 22 | *

23 | * That repository needs to store idempotent hash for idempotency check 24 | */ 25 | public class CouchbaseIdempotentRepository implements IdempotentRepository { 26 | private final CouchbaseConfig couchbaseConfig; 27 | private final Collection collection; 28 | private Map> ttlConverter = new HashMap<>(); 29 | 30 | public CouchbaseIdempotentRepository(CouchbaseConfig couchbaseConfig, Collection collection) { 31 | this.couchbaseConfig = couchbaseConfig; 32 | this.collection = collection; 33 | this.prepareTtlConverter(); 34 | } 35 | 36 | 37 | @Override 38 | public boolean contains(IdempotencyKey key) { 39 | return collection.exists(key.getKeyValue()).exists(); 40 | } 41 | 42 | @Override 43 | public IdempotentResponseWrapper getResponse(IdempotencyKey key) { 44 | return collection.get(key.getKeyValue(), GetOptions.getOptions().withExpiry(true)).contentAs(IdempotentRequestResponseWrapper.class).getResponse(); 45 | } 46 | 47 | @Override 48 | public void store(IdempotencyKey key, IdempotentRequestWrapper requestObject) { 49 | collection.insert(key.getKeyValue(), prepareRequestValue(requestObject)); 50 | } 51 | 52 | @Override 53 | public void store(IdempotencyKey key, IdempotentRequestWrapper requestObject, Long ttl, TimeUnit timeUnit) { 54 | Duration ttlDuration = getDurationByTttlAndTimeUnit(ttl, timeUnit); 55 | collection.upsert( 56 | key.getKeyValue(), prepareRequestValue(requestObject), 57 | UpsertOptions.upsertOptions().expiry(ttlDuration) 58 | ); 59 | } 60 | 61 | @Override 62 | public void remove(IdempotencyKey key) { 63 | collection.remove(key.getKeyValue()); 64 | } 65 | 66 | @Override 67 | public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse) { 68 | if (contains(key)) { 69 | GetResult getResult = collection.get(key.getKeyValue(), GetOptions.getOptions().withExpiry(true)); 70 | IdempotentRequestResponseWrapper requestResponseWrapper = prepareResponseValue(getResult,idempotentResponse); 71 | collection.upsert(key.getKeyValue(), requestResponseWrapper); 72 | } 73 | } 74 | 75 | @Override 76 | public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse, Long ttl, TimeUnit timeUnit) { 77 | if (contains(key)) { 78 | GetResult getResult = collection.get(key.getKeyValue(),GetOptions.getOptions().withExpiry(true)); 79 | IdempotentRequestResponseWrapper requestResponseWrapper = prepareResponseValue(getResult,idempotentResponse); 80 | collection.upsert( 81 | key.getKeyValue(), 82 | requestResponseWrapper, 83 | UpsertOptions.upsertOptions().expiry(getResult.expiry().get())); 84 | } 85 | } 86 | 87 | private Duration getDurationByTttlAndTimeUnit(Long ttl, TimeUnit timeUnit) { 88 | return ttlConverter.get(timeUnit).apply(ttl); 89 | } 90 | 91 | private void prepareTtlConverter() { 92 | ttlConverter.put(TimeUnit.DAYS, Duration::ofDays); 93 | ttlConverter.put(TimeUnit.HOURS, Duration::ofHours); 94 | ttlConverter.put(TimeUnit.MINUTES, Duration::ofMinutes); 95 | ttlConverter.put(TimeUnit.SECONDS, Duration::ofSeconds); 96 | ttlConverter.put(TimeUnit.MILLISECONDS, Duration::ofMillis); 97 | ttlConverter.put(TimeUnit.MICROSECONDS, Duration::ofMillis); 98 | ttlConverter.put(TimeUnit.NANOSECONDS, Duration::ofNanos); 99 | } 100 | 101 | /** 102 | * Prepares the request value stored in couchbase 103 | * 104 | * if persistReqRes set to false, 105 | * it does not persist related request and response values in couchbase 106 | * @param request 107 | * @return 108 | */ 109 | private IdempotentRequestResponseWrapper prepareRequestValue(IdempotentRequestWrapper request) { 110 | if (couchbaseConfig.getPersistReqRes()) { 111 | return new IdempotentRequestResponseWrapper(request); 112 | } 113 | return new IdempotentRequestResponseWrapper(null); 114 | } 115 | 116 | /** 117 | * Prepares the response value stored in couchbase 118 | * 119 | * if persistReqRes set to false, 120 | * it does not persist related request and response values in redis 121 | * @param result 122 | * @param idempotentResponse 123 | * @return 124 | */ 125 | private IdempotentRequestResponseWrapper prepareResponseValue(GetResult result,IdempotentResponseWrapper idempotentResponse) { 126 | IdempotentRequestResponseWrapper requestResponseWrapper = result.contentAs(IdempotentRequestResponseWrapper.class); 127 | if (couchbaseConfig.getPersistReqRes()) { 128 | requestResponseWrapper.setResponse(idempotentResponse); 129 | } 130 | return requestResponseWrapper; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /Jdempotent-spring-boot-couchbase-starter/src/test/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.jdempotent.couchbase; 2 | 3 | import com.couchbase.client.java.Collection; 4 | import com.couchbase.client.java.kv.ExistsResult; 5 | import com.couchbase.client.java.kv.GetResult; 6 | import com.couchbase.client.java.kv.MutationResult; 7 | import com.couchbase.client.java.kv.UpsertOptions; 8 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 9 | import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; 10 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 11 | import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.ArgumentCaptor; 16 | import org.mockito.Captor; 17 | import org.mockito.InjectMocks; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | 21 | import java.time.Duration; 22 | import java.util.Optional; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | import static org.junit.Assert.assertFalse; 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertTrue; 28 | import static org.mockito.Mockito.*; 29 | 30 | @ExtendWith(MockitoExtension.class) 31 | public class CouchbaseIdempotentRepositoryTest { 32 | @InjectMocks 33 | private CouchbaseIdempotentRepository couchbaseIdempotentRepository; 34 | 35 | @Mock 36 | private CouchbaseConfig couchbaseConfig; 37 | 38 | @Mock 39 | private Collection collection; 40 | 41 | @Captor 42 | private ArgumentCaptor captor; 43 | 44 | @Captor 45 | private ArgumentCaptor upsertOptionCaptor; 46 | 47 | @BeforeEach 48 | public void setUp() { 49 | couchbaseIdempotentRepository = new CouchbaseIdempotentRepository(couchbaseConfig, 50 | collection); 51 | } 52 | 53 | @Test 54 | public void given_an_available_object_when_couchbase_contains_then_return_true() { 55 | //Given 56 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 57 | ExistsResult existsResult = mock(ExistsResult.class); 58 | when(existsResult.exists()).thenReturn(true); 59 | when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); 60 | 61 | //When 62 | Boolean isContain = couchbaseIdempotentRepository.contains(idempotencyKey); 63 | 64 | //Then 65 | verify(collection, times(1)).exists(idempotencyKey.getKeyValue()); 66 | assertTrue(isContain); 67 | } 68 | 69 | @Test 70 | public void given_an_available_object_when_couchbase_contains_then_return_false() { 71 | //Given 72 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 73 | ExistsResult existsResult = mock(ExistsResult.class); 74 | when(existsResult.exists()).thenReturn(false); 75 | when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); 76 | 77 | //When 78 | Boolean isContain = couchbaseIdempotentRepository.contains(idempotencyKey); 79 | 80 | //Then 81 | verify(collection, times(1)).exists(idempotencyKey.getKeyValue()); 82 | assertFalse(isContain); 83 | } 84 | 85 | @Test 86 | public void given_an_available_object_when_couchbase_get_response_then_return_expected_idempotent_response_wrapper() { 87 | //Given 88 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 89 | IdempotentRequestResponseWrapper wrapper = new IdempotentRequestResponseWrapper(); 90 | GetResult getResult = mock(GetResult.class); 91 | when(getResult.contentAs(IdempotentRequestResponseWrapper.class)).thenReturn(wrapper); 92 | when(collection.get(eq(idempotencyKey.getKeyValue()),any())).thenReturn(getResult); 93 | 94 | //When 95 | IdempotentResponseWrapper result = couchbaseIdempotentRepository.getResponse(idempotencyKey); 96 | 97 | //Then 98 | verify(collection, times(1)).get(eq(idempotencyKey.getKeyValue()),any()); 99 | assertEquals(result, wrapper.getResponse()); 100 | } 101 | 102 | @Test 103 | public void given_an_available_object_when_couchbase_store_then_collection_insert_once_time() { 104 | //Given 105 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 106 | IdempotentRequestWrapper wrapper = new IdempotentRequestWrapper(); 107 | IdempotentRequestResponseWrapper responseWrapper = new IdempotentRequestResponseWrapper(wrapper); 108 | 109 | //When 110 | couchbaseIdempotentRepository.store(idempotencyKey, wrapper); 111 | 112 | //Then 113 | verify(collection, times(1)).insert(eq(idempotencyKey.getKeyValue()), captor.capture()); 114 | IdempotentRequestResponseWrapper idempotentRequestResponseWrapper = captor.getValue(); 115 | assertEquals(idempotentRequestResponseWrapper.getResponse(), responseWrapper.getResponse()); 116 | } 117 | 118 | @Test 119 | public void given_an_available_object_when_couchbase_store_with_ttl_and_time_unit_is_days_then_collection_insert_once_time() { 120 | //Given 121 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 122 | IdempotentRequestWrapper wrapper = new IdempotentRequestWrapper(); 123 | Long ttl = 1L; 124 | TimeUnit timeUnit = TimeUnit.DAYS; 125 | IdempotentRequestResponseWrapper responseWrapper = new IdempotentRequestResponseWrapper(wrapper); 126 | 127 | //When 128 | couchbaseIdempotentRepository.store(idempotencyKey, wrapper, ttl, timeUnit); 129 | 130 | //Then 131 | verify(collection, times(1)).upsert(eq(idempotencyKey.getKeyValue()), 132 | captor.capture(), 133 | upsertOptionCaptor.capture()); 134 | IdempotentRequestResponseWrapper idempotentRequestResponseWrapper = captor.getValue(); 135 | assertEquals(idempotentRequestResponseWrapper.getResponse(), responseWrapper.getResponse()); 136 | } 137 | 138 | 139 | @Test 140 | public void setResponse() { 141 | //Given 142 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 143 | IdempotentRequestResponseWrapper wrapper = new IdempotentRequestResponseWrapper(); 144 | GetResult getResult = mock(GetResult.class); 145 | ExistsResult existsResult = mock(ExistsResult.class); 146 | when(existsResult.exists()).thenReturn(true); 147 | when(getResult.contentAs(IdempotentRequestResponseWrapper.class)).thenReturn(wrapper); 148 | 149 | when(collection.get(eq(idempotencyKey.getKeyValue()),any())).thenReturn(getResult); 150 | when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); 151 | when(collection.upsert(idempotencyKey.getKeyValue(),wrapper)).thenReturn(mock(MutationResult.class)); 152 | //When 153 | couchbaseIdempotentRepository.setResponse(idempotencyKey,mock(IdempotentRequestWrapper.class), 154 | mock(IdempotentResponseWrapper.class)); 155 | 156 | //Then 157 | verify(collection, times(1)).get(eq(idempotencyKey.getKeyValue()),any()); 158 | } 159 | 160 | @Test 161 | public void setResponse_when_given_a_ttl() { 162 | //Given 163 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 164 | IdempotentRequestResponseWrapper wrapper = new IdempotentRequestResponseWrapper(); 165 | GetResult getResult = mock(GetResult.class); 166 | ExistsResult existsResult = mock(ExistsResult.class); 167 | when(existsResult.exists()).thenReturn(true); 168 | when(getResult.contentAs(IdempotentRequestResponseWrapper.class)).thenReturn(wrapper); 169 | when(getResult.expiry()).thenReturn(Optional.of(mock(Duration.class))); 170 | when(collection.get(eq(idempotencyKey.getKeyValue()),any())).thenReturn(getResult); 171 | when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); 172 | when(collection.upsert(eq(idempotencyKey.getKeyValue()),eq(wrapper),any())).thenReturn(mock(MutationResult.class)); 173 | //When 174 | couchbaseIdempotentRepository.setResponse(idempotencyKey,mock(IdempotentRequestWrapper.class), 175 | mock(IdempotentResponseWrapper.class),5L,TimeUnit.DAYS); 176 | 177 | //Then 178 | verify(collection, times(1)).get(eq(idempotencyKey.getKeyValue()),any()); 179 | } 180 | } -------------------------------------------------------------------------------- /Jdempotent-core/src/test/java/aspect/withaspect/IdempotentAspectIT.java: -------------------------------------------------------------------------------- 1 | package aspect.withaspect; 2 | 3 | import aspect.core.IdempotentTestPayload; 4 | import aspect.core.TestException; 5 | import aspect.core.TestIdempotentResource; 6 | import com.trendyol.jdempotent.core.annotation.JdempotentResource; 7 | import com.trendyol.jdempotent.core.constant.CryptographyAlgorithm; 8 | import com.trendyol.jdempotent.core.datasource.InMemoryIdempotentRepository; 9 | import com.trendyol.jdempotent.core.generator.DefaultKeyGenerator; 10 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 11 | import com.trendyol.jdempotent.core.model.IdempotentIgnorableWrapper; 12 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.aop.framework.AopProxyUtils; 16 | import org.springframework.aop.support.AopUtils; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.test.context.ContextConfiguration; 19 | import org.springframework.test.context.junit4.SpringRunner; 20 | import org.springframework.test.util.AopTestUtils; 21 | 22 | import java.security.MessageDigest; 23 | import java.security.NoSuchAlgorithmException; 24 | 25 | import static org.junit.Assert.*; 26 | 27 | @RunWith(SpringRunner.class) 28 | @ContextConfiguration(classes = {IdempotentAspectIT.class, TestAopContext.class, TestIdempotentResource.class, DefaultKeyGenerator.class, InMemoryIdempotentRepository.class}) 29 | public class IdempotentAspectIT { 30 | 31 | @Autowired 32 | private TestIdempotentResource testIdempotentResource; 33 | 34 | @Autowired 35 | private InMemoryIdempotentRepository idempotentRepository; 36 | 37 | @Autowired 38 | private DefaultKeyGenerator defaultKeyGenerator; 39 | 40 | 41 | @Test 42 | public void given_aop_context_then_run_with_aop_context() { 43 | JdempotentResource jdempotentResource = TestIdempotentResource.class.getDeclaredMethods()[0].getAnnotation(JdempotentResource.class); 44 | 45 | assertNotEquals(testIdempotentResource.getClass(), TestIdempotentResource.class); 46 | assertTrue(AopUtils.isAopProxy(testIdempotentResource)); 47 | assertTrue(AopUtils.isCglibProxy(testIdempotentResource)); 48 | assertNotNull(jdempotentResource); 49 | 50 | assertEquals(AopProxyUtils.ultimateTargetClass(testIdempotentResource), TestIdempotentResource.class); 51 | assertEquals(AopTestUtils.getTargetObject(testIdempotentResource).getClass(), TestIdempotentResource.class); 52 | assertEquals(AopTestUtils.getUltimateTargetObject(testIdempotentResource).getClass(), TestIdempotentResource.class); 53 | } 54 | 55 | @Test 56 | public void given_new_payload_when_trigger_aspect_then_that_will_be_aviable_in_repository() throws NoSuchAlgorithmException { 57 | //given 58 | IdempotentTestPayload test = new IdempotentTestPayload(); 59 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 60 | wrapper.getNonIgnoredFields().put("name", null); 61 | 62 | IdempotencyKey idempotencyKey = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 63 | 64 | //when 65 | testIdempotentResource.idempotentMethod(test); 66 | 67 | //then 68 | assertTrue(idempotentRepository.contains(idempotencyKey)); 69 | } 70 | 71 | @Test 72 | public void given_new_multiple_payloads_when_trigger_aspect_then_that_will_be_available_in_repository() throws NoSuchAlgorithmException { 73 | //given 74 | IdempotentTestPayload test = new IdempotentTestPayload(); 75 | IdempotentTestPayload test1 = new IdempotentTestPayload(); 76 | IdempotentTestPayload test2 = new IdempotentTestPayload(); 77 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 78 | wrapper.getNonIgnoredFields().put("name", null); 79 | 80 | IdempotencyKey idempotencyKey = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "TestIdempotentResource", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 81 | 82 | //when 83 | testIdempotentResource.idempotentMethodWithThreeParameter(test, test1, test2); 84 | 85 | //then 86 | assertTrue(idempotentRepository.contains(idempotencyKey)); 87 | } 88 | 89 | @Test(expected = TestException.class) 90 | public void given_invalid_payload_when_trigger_aspect_then_throw_test_exception_and_repository_will_be_empty() throws NoSuchAlgorithmException { 91 | //given 92 | IdempotentTestPayload test = new IdempotentTestPayload(); 93 | test.setName("invalid"); 94 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 95 | wrapper.getNonIgnoredFields().put("name", "invalid"); 96 | 97 | IdempotencyKey idempotencyKey = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "TestIdempotentResource", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 98 | 99 | //when 100 | testIdempotentResource.idempotentMethodThrowingARuntimeException(test); 101 | 102 | //then 103 | assertFalse(idempotentRepository.contains(idempotencyKey)); 104 | } 105 | 106 | @Test 107 | public void given_new_multiple_payloads_with_multiple_annotations_when_trigger_aspect_then_first_annotated_payload_that_will_be_available_in_repository() throws NoSuchAlgorithmException { 108 | //given 109 | IdempotentTestPayload test = new IdempotentTestPayload(); 110 | IdempotentTestPayload test1 = new IdempotentTestPayload(); 111 | Object test2 = new Object(); 112 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 113 | wrapper.getNonIgnoredFields().put("name", null); 114 | IdempotencyKey idempotencyKey = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "TestIdempotentResource", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 115 | 116 | //when 117 | testIdempotentResource.idempotentMethodWithThreeParamaterAndMultipleJdempotentRequestPayloadAnnotation(test, test1, test2); 118 | 119 | //then 120 | assertTrue(idempotentRepository.contains(idempotencyKey)); 121 | } 122 | 123 | @Test(expected = IllegalStateException.class) 124 | public void given_no_args_when_trigger_aspect_then_throw_illegal_state_exception() throws NoSuchAlgorithmException { 125 | //given 126 | //when 127 | //then 128 | testIdempotentResource.idempotentMethodWithZeroParamater(); 129 | } 130 | 131 | @Test(expected = IllegalStateException.class) 132 | public void given_multiple_args_without_idempotent_request_annotation_when_trigger_aspect_then_throw_illegal_state_exception() throws NoSuchAlgorithmException { 133 | //given 134 | IdempotentTestPayload test = new IdempotentTestPayload(); 135 | IdempotentTestPayload test1 = new IdempotentTestPayload(); 136 | 137 | //when 138 | //then 139 | testIdempotentResource.methodWithTwoParamater(test, test1); 140 | } 141 | 142 | @Test 143 | public void given_jdempotent_id_then_args_should_have_idempotency_id() throws NoSuchAlgorithmException { 144 | //given 145 | IdempotentTestPayload test = new IdempotentTestPayload(); 146 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 147 | wrapper.getNonIgnoredFields().put("name", null); 148 | 149 | IdempotencyKey idempotencyKey = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 150 | 151 | //when 152 | testIdempotentResource.idempotentMethod(test); 153 | 154 | //then 155 | assertTrue(idempotentRepository.contains(idempotencyKey)); 156 | } 157 | 158 | @Test 159 | public void given_new_payload_as_string_when_trigger_aspect_then_that_will_be_aviable_in_repository() throws NoSuchAlgorithmException { 160 | //given 161 | String idempotencyKey = "key"; 162 | IdempotentTestPayload test = new IdempotentTestPayload(); 163 | IdempotentIgnorableWrapper wrapper = new IdempotentIgnorableWrapper(); 164 | wrapper.getNonIgnoredFields().put(idempotencyKey, idempotencyKey); 165 | IdempotencyKey key = defaultKeyGenerator.generateIdempotentKey(new IdempotentRequestWrapper(wrapper), "", new StringBuilder(), MessageDigest.getInstance(CryptographyAlgorithm.MD5.value())); 166 | 167 | //when 168 | testIdempotentResource.idempotencyKeyAsString(idempotencyKey); 169 | 170 | //then 171 | assertTrue(idempotentRepository.contains(key)); 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /Jdempotent-spring-boot-redis-starter/src/test/java/RedisIdempotentRepositoryTest.java: -------------------------------------------------------------------------------- 1 | import com.trendyol.jdempotent.core.model.IdempotencyKey; 2 | import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; 3 | import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; 4 | import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; 5 | import com.trendyol.jdempotent.redis.RedisConfigProperties; 6 | import com.trendyol.jdempotent.redis.RedisIdempotentRepository; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.ArgumentCaptor; 11 | import org.mockito.Captor; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | import org.springframework.data.redis.core.ValueOperations; 17 | 18 | import java.util.concurrent.TimeUnit; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | import static org.junit.jupiter.api.Assertions.assertFalse; 22 | import static org.junit.jupiter.api.Assertions.assertNull; 23 | import static org.junit.jupiter.api.Assertions.assertTrue; 24 | import static org.mockito.ArgumentMatchers.any; 25 | import static org.mockito.ArgumentMatchers.eq; 26 | import static org.mockito.Mockito.mock; 27 | import static org.mockito.Mockito.times; 28 | import static org.mockito.Mockito.verify; 29 | import static org.mockito.Mockito.when; 30 | 31 | @ExtendWith(MockitoExtension.class) 32 | public class RedisIdempotentRepositoryTest { 33 | 34 | 35 | @InjectMocks 36 | private RedisIdempotentRepository redisIdempotentRepository; 37 | 38 | @Mock 39 | private RedisTemplate redisTemplate; 40 | 41 | @Mock 42 | private RedisConfigProperties redisConfigProperties; 43 | 44 | @Mock 45 | private ValueOperations valueOperations; 46 | 47 | @Captor 48 | private ArgumentCaptor captor; 49 | 50 | @BeforeEach 51 | public void setUp() { 52 | when(redisTemplate.opsForValue()).thenReturn(valueOperations); 53 | redisIdempotentRepository = new RedisIdempotentRepository(redisTemplate, 54 | redisConfigProperties); 55 | } 56 | 57 | @Test 58 | public void given_an_available_object_when_redis_contains_then_return_true() { 59 | //Given 60 | IdempotencyKey idempotencyKey = new IdempotencyKey("key"); 61 | var key = new IdempotencyKey("key"); 62 | var wrapper = new IdempotentRequestResponseWrapper( 63 | new IdempotentRequestWrapper(new Object())); 64 | when(valueOperations.get(key.getKeyValue())).thenReturn(wrapper); 65 | 66 | //When 67 | Boolean isContain = redisIdempotentRepository.contains(idempotencyKey); 68 | 69 | //Then 70 | verify(valueOperations, times(1)).get(idempotencyKey.getKeyValue()); 71 | assertTrue(isContain); 72 | } 73 | 74 | @Test 75 | public void given_an_unavailable_object_when_redis_contains_then_return_false() { 76 | //Given 77 | IdempotencyKey idempotencyKey = new IdempotencyKey("key1"); 78 | 79 | //When 80 | Boolean isContain = redisIdempotentRepository.contains(idempotencyKey); 81 | 82 | //Then 83 | verify(valueOperations, times(1)).get(idempotencyKey.getKeyValue()); 84 | assertFalse(isContain); 85 | } 86 | 87 | @Test 88 | public void given_an_available_object_when_get_response_then_return_response() { 89 | //Given 90 | var key = new IdempotencyKey("key"); 91 | var wrapper = new IdempotentRequestResponseWrapper( 92 | new IdempotentRequestWrapper(new Object())); 93 | when(valueOperations.get(key.getKeyValue())).thenReturn(wrapper); 94 | var t = mock(IdempotentRequestResponseWrapper.class); 95 | 96 | IdempotentResponseWrapper expected = new IdempotentResponseWrapper("testt"); 97 | when(t.getResponse()).thenReturn(expected); 98 | when(valueOperations.get(key.getKeyValue())).thenReturn(t); 99 | 100 | //When 101 | IdempotentResponseWrapper response = redisIdempotentRepository.getResponse(key); 102 | 103 | //Then 104 | verify(t).getResponse(); 105 | assertEquals(response.getResponse(), "testt"); 106 | } 107 | 108 | @Test 109 | public void given_idempotency_key_and_request_object_when_store_then_set_value_to_redis() { 110 | //Given 111 | IdempotencyKey key = new IdempotencyKey("key"); 112 | IdempotentRequestWrapper request = new IdempotentRequestWrapper(123L); 113 | when(redisConfigProperties.getPersistReqRes()).thenReturn(true); 114 | 115 | //When 116 | redisIdempotentRepository.store(key, request, 1L, TimeUnit.HOURS); 117 | 118 | //Then 119 | var argumentCaptor = ArgumentCaptor.forClass(IdempotentRequestResponseWrapper.class); 120 | verify(valueOperations).set(eq(key.getKeyValue()), argumentCaptor.capture(), eq(1L), eq(TimeUnit.HOURS)); 121 | IdempotentRequestResponseWrapper value = argumentCaptor.getValue(); 122 | assertEquals(value.getRequest().getRequest(), 123L); 123 | } 124 | 125 | @Test 126 | public void given_ttl_zero_when_store_then_set_value_to_redis_with_property_ttl() { 127 | //Given 128 | IdempotencyKey key = new IdempotencyKey("key"); 129 | IdempotentRequestWrapper request = new IdempotentRequestWrapper(123L); 130 | when(redisConfigProperties.getExpirationTimeHour()).thenReturn(99L); 131 | 132 | //When 133 | redisIdempotentRepository.store(key, request, 0L, TimeUnit.HOURS); 134 | 135 | //Then 136 | verify(valueOperations).set(eq(key.getKeyValue()), any(), eq(99L), eq(TimeUnit.HOURS)); 137 | } 138 | 139 | @Test 140 | public void given_idempotency_key_when_remove_then_delete_redis_key() { 141 | //Given 142 | IdempotencyKey key = new IdempotencyKey("key"); 143 | 144 | //When 145 | redisIdempotentRepository.remove(key); 146 | 147 | //Then 148 | verify(redisTemplate).delete(eq(key.getKeyValue())); 149 | } 150 | 151 | @Test 152 | public void given_idempotency_key_and_request_and_response_objects_when_set_response_then_set_response_to_key() { 153 | //Given 154 | IdempotencyKey key = new IdempotencyKey("key"); 155 | IdempotentRequestWrapper request = new IdempotentRequestWrapper(123L); 156 | IdempotentResponseWrapper response = new IdempotentResponseWrapper("response"); 157 | var wrapper = new IdempotentRequestResponseWrapper( 158 | new IdempotentRequestWrapper(new Object())); 159 | when(valueOperations.get(key.getKeyValue())).thenReturn(wrapper); 160 | assertNull(wrapper.getResponse()); 161 | when(redisConfigProperties.getPersistReqRes()).thenReturn(true); 162 | 163 | //When 164 | redisIdempotentRepository.setResponse(key, request, response, 1L, TimeUnit.HOURS); 165 | 166 | //Then 167 | var argumentCaptor = ArgumentCaptor.forClass(IdempotentRequestResponseWrapper.class); 168 | verify(valueOperations).set(eq(key.getKeyValue()), argumentCaptor.capture(), eq(1L), eq(TimeUnit.HOURS)); 169 | IdempotentRequestResponseWrapper value = argumentCaptor.getValue(); 170 | assertEquals(value.getRequest().getRequest(), 123L); 171 | assertEquals(value.getResponse().getResponse(), "response"); 172 | assertEquals(wrapper.getResponse().getResponse(), "response"); 173 | } 174 | 175 | @Test 176 | public void given_the_idempotence_key_and_the_request_and_response_objects_when_defining_the_response_one_must_save_the_key_without_the_request_and_response_object() { 177 | //Given 178 | IdempotencyKey key = new IdempotencyKey("key"); 179 | IdempotentRequestWrapper request = new IdempotentRequestWrapper(123L); 180 | IdempotentResponseWrapper response = new IdempotentResponseWrapper("response"); 181 | var wrapper = new IdempotentRequestResponseWrapper( 182 | new IdempotentRequestWrapper(new Object())); 183 | when(valueOperations.get(key.getKeyValue())).thenReturn(wrapper); 184 | assertNull(wrapper.getResponse()); 185 | when(redisConfigProperties.getPersistReqRes()).thenReturn(false); 186 | 187 | //When 188 | redisIdempotentRepository.setResponse(key, request, response, 1L, TimeUnit.HOURS); 189 | 190 | //Then 191 | var argumentCaptor = ArgumentCaptor.forClass(IdempotentRequestResponseWrapper.class); 192 | verify(valueOperations).set(eq(key.getKeyValue()), argumentCaptor.capture(), eq(1L), eq(TimeUnit.HOURS)); 193 | IdempotentRequestResponseWrapper value = argumentCaptor.getValue(); 194 | assertNull(value.getRequest()); 195 | assertNull(value.getResponse()); 196 | assertEquals(wrapper.getResponse().getResponse(), "response"); 197 | } 198 | } -------------------------------------------------------------------------------- /examples/jdempotent-redis-example/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /examples/jdempotent-couchbase-example/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | --------------------------------------------------------------------------------