diff --git a/Jdempotent-core/pom.xml b/Jdempotent-core/pom.xml index d9ca0a4..2e6169e 100644 --- a/Jdempotent-core/pom.xml +++ b/Jdempotent-core/pom.xml @@ -6,7 +6,7 @@ 4.0.0 com.trendyol Jdempotent-core - 1.0.4 + 1.0.6 Jdempotent-core jar https://github.com/Trendyol/Jdempotent/tree/master/Jdempotent-core diff --git a/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestResponseWrapper.java b/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestResponseWrapper.java index 852bac5..a4e1f1c 100644 --- a/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestResponseWrapper.java +++ b/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestResponseWrapper.java @@ -9,9 +9,11 @@ */ @SuppressWarnings("serial") public class IdempotentRequestResponseWrapper implements Serializable { - private final IdempotentRequestWrapper request; + private IdempotentRequestWrapper request; private IdempotentResponseWrapper response = null; + public IdempotentRequestResponseWrapper(){} + public IdempotentRequestResponseWrapper(IdempotentRequestWrapper request) { this.request = request; } diff --git a/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestWrapper.java b/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestWrapper.java index 8ad58cc..16afe39 100644 --- a/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestWrapper.java +++ b/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentRequestWrapper.java @@ -9,7 +9,10 @@ */ @SuppressWarnings("serial") public class IdempotentRequestWrapper implements Serializable { - private final Object request; + private Object request; + + public IdempotentRequestWrapper(){ + } public IdempotentRequestWrapper(Object request) { this.request = request; diff --git a/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentResponseWrapper.java b/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentResponseWrapper.java index 7039db1..90fb3e4 100644 --- a/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentResponseWrapper.java +++ b/Jdempotent-core/src/main/java/com/trendyol/jdempotent/core/model/IdempotentResponseWrapper.java @@ -9,7 +9,9 @@ @SuppressWarnings("serial") public class IdempotentResponseWrapper implements Serializable { - private final Object response; + private Object response; + + public IdempotentResponseWrapper(){} public IdempotentResponseWrapper(Object response) { this.response = response; diff --git a/Jdempotent-spring-boot-couchbase-starter/pom.xml b/Jdempotent-spring-boot-couchbase-starter/pom.xml new file mode 100644 index 0000000..1284db5 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/pom.xml @@ -0,0 +1,316 @@ + + + 4.0.0 + com.trendyol + Jdempotent-spring-boot-couchbase-starter + 1.0.6 + Jdempotent-spring-boot-couchbase-starter + jar + https://github.com/Trendyol/Jdempotent/tree/master/Jdempotent-spring-boot-couchbase-starter + Jdempotent-spring-boot-couchbase-starter + + + org.sonatype.oss + oss-parent + 9 + + + + + The MIT License (MIT) + https://github.com/Trendyol/Jdempotent/blob/master/LICENSE + repo + + + + + scm:git:git@github.com:Trendyol/Jdempotent.git + scm:git:git@github.com:Trendyol/Jdempotent.git + https://github.com/Trendyol/Jdempotent + HEAD + + + + + Mehmet ARI + https://github.com/memojja/ + Trendyol + https://github.com/trendyol + + + + + 11 + 2.3.4.RELEASE + 5.2.9.RELEASE + 1.10.19 + 4.13.1 + + + + + + com.trendyol + Jdempotent-core + 1.0.6 + + + org.aspectj + aspectjweaver + 1.7.4 + compile + + + org.springframework.boot + spring-boot-starter + 2.2.5.RELEASE + + + com.fasterxml.jackson.core + jackson-databind + 2.12.3 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + + com.couchbase.client + java-client + 3.0.10 + + + + + junit + junit + 4.13.1 + test + + + org.junit.vintage + junit-vintage-engine + 5.7.0 + + + org.mockito + mockito-inline + 3.8.0 + test + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.mockito + mockito-all + ${version.mockito} + test + + + + + + + deploy + + + + + org.apache.maven.plugins + maven-source-plugin + 2.4 + + + attach-sources + + jar-no-fork + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + empty-javadoc-jar + package + + jar + + + javadoc + ${basedir}/javadoc + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + --pinentry-mode + loopback + + + + + + + + + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://oss.sonatype.org/ + false + false + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${java.version} + ${java.version} + UTF-8 + + + + + org.apache.maven.plugins + maven-release-plugin + + true + false + release + deploy + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 11 + 11 + UTF-8 + + + + maven-clean-plugin + 3.1.0 + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + + + + jcenter + JCenter + https://jcenter.bintray.com/ + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/Jdempotent-spring-boot-couchbase-starter/settings.xml b/Jdempotent-spring-boot-couchbase-starter/settings.xml new file mode 100644 index 0000000..641e880 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/settings.xml @@ -0,0 +1,21 @@ + + + + ossrh + ${env.SONATYPE_USERNAME} + ${env.SONATYPE_PASSWORD} + + + + + + ossrh + + true + + + ${env.PASSPHRASE} + + + + \ No newline at end of file diff --git a/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java new file mode 100644 index 0000000..b9cf3f1 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java @@ -0,0 +1,39 @@ +package com.trendyol.jdempotent.couchbase; + +import com.couchbase.client.java.Collection; +import com.trendyol.jdempotent.core.aspect.IdempotentAspect; +import com.trendyol.jdempotent.core.callback.ErrorConditionalCallback; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty( + prefix="jdempotent", name = "enable", + havingValue = "true", + matchIfMissing = true) +public class ApplicationConfig { + + private final CouchbaseConfig couchbaseConfig; + + public ApplicationConfig(CouchbaseConfig couchbaseConfig) { + this.couchbaseConfig = couchbaseConfig; + } + + @Bean + @ConditionalOnProperty( + prefix="jdempotent", name = "enable", + havingValue = "true", + matchIfMissing = true) + @ConditionalOnClass(ErrorConditionalCallback.class) + public IdempotentAspect getIdempotentAspect(Collection collection, ErrorConditionalCallback errorConditionalCallback) { + return new IdempotentAspect(new CouchbaseIdempotentRepository(couchbaseConfig, collection), errorConditionalCallback); + } + + @Bean + public IdempotentAspect getIdempotentAspect(Collection collection) { + return new IdempotentAspect(new CouchbaseIdempotentRepository(couchbaseConfig, collection)); + } + +} diff --git a/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java new file mode 100644 index 0000000..3efa4ee --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java @@ -0,0 +1,71 @@ +package com.trendyol.jdempotent.couchbase; + +import com.couchbase.client.core.deps.io.netty.channel.epoll.EpollEventLoopGroup; +import com.couchbase.client.core.env.*; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.codec.JacksonJsonSerializer; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.SystemUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +@Configuration +@ConditionalOnProperty( + prefix="jdempotent", name = "enable", + havingValue = "true", + matchIfMissing = true) +public class CouchbaseBeanConfig { + + private final CouchbaseConfig couchbaseConfig; + + public CouchbaseBeanConfig(CouchbaseConfig couchbaseConfig) { + this.couchbaseConfig = couchbaseConfig; + } + + @Bean + public Cluster cluster(ObjectMapper objectMapper) { + var builder = ClusterEnvironment.builder(); + if (SystemUtils.IS_OS_LINUX) { + builder.ioEnvironment( + IoEnvironment.kvEventLoopGroup( + new EpollEventLoopGroup( + Runtime.getRuntime().availableProcessors() * 2 + ) + ) + ) + .ioConfig(IoConfig.configPollInterval(Duration.ofSeconds(10))) + .securityConfig(SecurityConfig.enableNativeTls(false).enableTls(false)); + } + var couchbaseEnvironment = builder + .jsonSerializer(JacksonJsonSerializer.create(objectMapper)) + .timeoutConfig( + TimeoutConfig.kvTimeout(Duration.ofMillis(couchbaseConfig.getKvTimeout())) + .connectTimeout(Duration.ofMillis(couchbaseConfig.getConnectTimeout())) + .queryTimeout(Duration.ofMillis(couchbaseConfig.getQueryTimeout())) + ) + .compressionConfig(CompressionConfig.enable(true)) + .loggerConfig(LoggerConfig.enableDiagnosticContext(false)) + .build(); + return Cluster.connect( + couchbaseConfig.getConnectionString(), + ClusterOptions.clusterOptions(couchbaseConfig.getUsername(), couchbaseConfig.getPassword()) + .environment(couchbaseEnvironment) + ); + } + + @Bean + public Collection collection(ObjectMapper objectMapper) { + return cluster(objectMapper).bucket(couchbaseConfig.getBucketName()).defaultCollection(); + } + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java new file mode 100644 index 0000000..03f4d16 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java @@ -0,0 +1,93 @@ +package com.trendyol.jdempotent.couchbase; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +@ConditionalOnProperty( + prefix="jdempotent", name = "enable", + havingValue = "true", + matchIfMissing = true) +@Configuration +public class CouchbaseConfig { + @Value("${jdempotent.cache.couchbase.connection-string}") + private String connectionString; + @Value("${jdempotent.cache.couchbase.username}") + private String username; + @Value("${jdempotent.cache.couchbase.password}") + private String password; + @Value("${jdempotent.cache.couchbase.bucket-name}") + private String bucketName; + @Value("${jdempotent.cache.couchbase.connect-timeout}") + private Long connectTimeout; + @Value("${jdempotent.cache.couchbase.query-timeout}") + private Long queryTimeout; + @Value("${jdempotent.cache.couchbase.kv-timeout}") + private Long kvTimeout; + @Value("${jdempotent.cache.persistReqRes:false}") + private Boolean persistReqRes; + + public String getConnectionString() { + return connectionString; + } + + public void setConnectionString(String connectionString) { + this.connectionString = connectionString; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + public Long getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(Long connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Long getQueryTimeout() { + return queryTimeout; + } + + public void setQueryTimeout(Long queryTimeout) { + this.queryTimeout = queryTimeout; + } + + public Long getKvTimeout() { + return kvTimeout; + } + + public void setKvTimeout(Long kvTimeout) { + this.kvTimeout = kvTimeout; + } + + public Boolean getPersistReqRes() { + return persistReqRes; + } + + public void setPersistReqRes(Boolean persistReqRes) { + this.persistReqRes = persistReqRes; + } +} \ No newline at end of file diff --git a/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java new file mode 100644 index 0000000..654a78e --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java @@ -0,0 +1,132 @@ +package com.trendyol.jdempotent.couchbase; + +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.kv.GetOptions; +import com.couchbase.client.java.kv.GetResult; +import com.couchbase.client.java.kv.UpsertOptions; +import com.trendyol.jdempotent.core.datasource.IdempotentRepository; +import com.trendyol.jdempotent.core.model.IdempotencyKey; +import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; +import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; +import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * An implementation of the idempotent IdempotentRepository + * that uses a distributed hash map from Couchbase + *

+ * That repository needs to store idempotent hash for idempotency check + */ +public class CouchbaseIdempotentRepository implements IdempotentRepository { + private final CouchbaseConfig couchbaseConfig; + private final Collection collection; + private Map> ttlConverter = new HashMap<>(); + + public CouchbaseIdempotentRepository(CouchbaseConfig couchbaseConfig, Collection collection) { + this.couchbaseConfig = couchbaseConfig; + this.collection = collection; + this.prepareTtlConverter(); + } + + + @Override + public boolean contains(IdempotencyKey key) { + return collection.exists(key.getKeyValue()).exists(); + } + + @Override + public IdempotentResponseWrapper getResponse(IdempotencyKey key) { + return collection.get(key.getKeyValue(), GetOptions.getOptions().withExpiry(true)).contentAs(IdempotentRequestResponseWrapper.class).getResponse(); + } + + @Override + public void store(IdempotencyKey key, IdempotentRequestWrapper requestObject) { + collection.insert(key.getKeyValue(), prepareRequestValue(requestObject)); + } + + @Override + public void store(IdempotencyKey key, IdempotentRequestWrapper requestObject, Long ttl, TimeUnit timeUnit) { + Duration ttlDuration = getDurationByTttlAndTimeUnit(ttl, timeUnit); + collection.upsert( + key.getKeyValue(), prepareRequestValue(requestObject), + UpsertOptions.upsertOptions().expiry(ttlDuration) + ); + } + + @Override + public void remove(IdempotencyKey key) { + collection.remove(key.getKeyValue()); + } + + @Override + public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse) { + if (contains(key)) { + GetResult getResult = collection.get(key.getKeyValue(), GetOptions.getOptions().withExpiry(true)); + IdempotentRequestResponseWrapper requestResponseWrapper = prepareResponseValue(getResult,idempotentResponse); + collection.upsert(key.getKeyValue(), requestResponseWrapper); + } + } + + @Override + public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse, Long ttl, TimeUnit timeUnit) { + if (contains(key)) { + GetResult getResult = collection.get(key.getKeyValue(),GetOptions.getOptions().withExpiry(true)); + IdempotentRequestResponseWrapper requestResponseWrapper = prepareResponseValue(getResult,idempotentResponse); + collection.upsert( + key.getKeyValue(), + requestResponseWrapper, + UpsertOptions.upsertOptions().expiry(getResult.expiry().get())); + } + } + + private Duration getDurationByTttlAndTimeUnit(Long ttl, TimeUnit timeUnit) { + return ttlConverter.get(timeUnit).apply(ttl); + } + + private void prepareTtlConverter() { + ttlConverter.put(TimeUnit.DAYS, Duration::ofDays); + ttlConverter.put(TimeUnit.HOURS, Duration::ofHours); + ttlConverter.put(TimeUnit.MINUTES, Duration::ofMinutes); + ttlConverter.put(TimeUnit.SECONDS, Duration::ofSeconds); + ttlConverter.put(TimeUnit.MILLISECONDS, Duration::ofMillis); + ttlConverter.put(TimeUnit.MICROSECONDS, Duration::ofMillis); + ttlConverter.put(TimeUnit.NANOSECONDS, Duration::ofNanos); + } + + /** + * Prepares the request value stored in couchbase + * + * if persistReqRes set to false, + * it does not persist related request and response values in couchbase + * @param request + * @return + */ + private IdempotentRequestResponseWrapper prepareRequestValue(IdempotentRequestWrapper request) { + if (couchbaseConfig.getPersistReqRes()) { + return new IdempotentRequestResponseWrapper(request); + } + return new IdempotentRequestResponseWrapper(null); + } + + /** + * Prepares the response value stored in couchbase + * + * if persistReqRes set to false, + * it does not persist related request and response values in redis + * @param result + * @param idempotentResponse + * @return + */ + private IdempotentRequestResponseWrapper prepareResponseValue(GetResult result,IdempotentResponseWrapper idempotentResponse) { + IdempotentRequestResponseWrapper requestResponseWrapper = result.contentAs(IdempotentRequestResponseWrapper.class); + if (couchbaseConfig.getPersistReqRes()) { + requestResponseWrapper.setResponse(idempotentResponse); + } + return requestResponseWrapper; + } +} diff --git a/Jdempotent-spring-boot-couchbase-starter/src/main/resources/META-INF/spring.factories b/Jdempotent-spring-boot-couchbase-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..9e0289d --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.trendyol.jdempotent.couchbase.CouchbaseConfig,\ +com.trendyol.jdempotent.couchbase.CouchbaseBeanConfig,\ +com.trendyol.jdempotent.couchbase.ApplicationConfig \ No newline at end of file diff --git a/Jdempotent-spring-boot-couchbase-starter/src/test/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepositoryTest.java b/Jdempotent-spring-boot-couchbase-starter/src/test/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepositoryTest.java new file mode 100644 index 0000000..ac622c2 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/test/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepositoryTest.java @@ -0,0 +1,180 @@ +package com.trendyol.jdempotent.couchbase; + +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.kv.ExistsResult; +import com.couchbase.client.java.kv.GetResult; +import com.couchbase.client.java.kv.MutationResult; +import com.couchbase.client.java.kv.UpsertOptions; +import com.trendyol.jdempotent.core.model.IdempotencyKey; +import com.trendyol.jdempotent.core.model.IdempotentRequestResponseWrapper; +import com.trendyol.jdempotent.core.model.IdempotentRequestWrapper; +import com.trendyol.jdempotent.core.model.IdempotentResponseWrapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class CouchbaseIdempotentRepositoryTest { + @InjectMocks + private CouchbaseIdempotentRepository couchbaseIdempotentRepository; + + @Mock + private CouchbaseConfig couchbaseConfig; + + @Mock + private Collection collection; + + @Captor + private ArgumentCaptor captor; + + @Captor + private ArgumentCaptor upsertOptionCaptor; + + @BeforeEach + public void setUp() { + couchbaseIdempotentRepository = new CouchbaseIdempotentRepository(couchbaseConfig, + collection); + } + + @Test + public void given_an_available_object_when_couchbase_contains_then_return_true() { + //Given + IdempotencyKey idempotencyKey = new IdempotencyKey("key"); + ExistsResult existsResult = mock(ExistsResult.class); + when(existsResult.exists()).thenReturn(true); + when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); + + //When + Boolean isContain = couchbaseIdempotentRepository.contains(idempotencyKey); + + //Then + verify(collection, times(1)).exists(idempotencyKey.getKeyValue()); + assertTrue(isContain); + } + + @Test + public void given_an_available_object_when_couchbase_contains_then_return_false() { + //Given + IdempotencyKey idempotencyKey = new IdempotencyKey("key"); + ExistsResult existsResult = mock(ExistsResult.class); + when(existsResult.exists()).thenReturn(false); + when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); + + //When + Boolean isContain = couchbaseIdempotentRepository.contains(idempotencyKey); + + //Then + verify(collection, times(1)).exists(idempotencyKey.getKeyValue()); + assertFalse(isContain); + } + + @Test + public void given_an_available_object_when_couchbase_get_response_then_return_expected_idempotent_response_wrapper() { + //Given + IdempotencyKey idempotencyKey = new IdempotencyKey("key"); + IdempotentRequestResponseWrapper wrapper = new IdempotentRequestResponseWrapper(); + GetResult getResult = mock(GetResult.class); + when(getResult.contentAs(IdempotentRequestResponseWrapper.class)).thenReturn(wrapper); + when(collection.get(eq(idempotencyKey.getKeyValue()),any())).thenReturn(getResult); + + //When + IdempotentResponseWrapper result = couchbaseIdempotentRepository.getResponse(idempotencyKey); + + //Then + verify(collection, times(1)).get(eq(idempotencyKey.getKeyValue()),any()); + assertEquals(result, wrapper.getResponse()); + } + + @Test + public void given_an_available_object_when_couchbase_store_then_collection_insert_once_time() { + //Given + IdempotencyKey idempotencyKey = new IdempotencyKey("key"); + IdempotentRequestWrapper wrapper = new IdempotentRequestWrapper(); + IdempotentRequestResponseWrapper responseWrapper = new IdempotentRequestResponseWrapper(wrapper); + + //When + couchbaseIdempotentRepository.store(idempotencyKey, wrapper); + + //Then + verify(collection, times(1)).insert(eq(idempotencyKey.getKeyValue()), captor.capture()); + IdempotentRequestResponseWrapper idempotentRequestResponseWrapper = captor.getValue(); + assertEquals(idempotentRequestResponseWrapper.getResponse(), responseWrapper.getResponse()); + } + + @Test + public void given_an_available_object_when_couchbase_store_with_ttl_and_time_unit_is_days_then_collection_insert_once_time() { + //Given + IdempotencyKey idempotencyKey = new IdempotencyKey("key"); + IdempotentRequestWrapper wrapper = new IdempotentRequestWrapper(); + Long ttl = 1L; + TimeUnit timeUnit = TimeUnit.DAYS; + IdempotentRequestResponseWrapper responseWrapper = new IdempotentRequestResponseWrapper(wrapper); + + //When + couchbaseIdempotentRepository.store(idempotencyKey, wrapper, ttl, timeUnit); + + //Then + verify(collection, times(1)).upsert(eq(idempotencyKey.getKeyValue()), + captor.capture(), + upsertOptionCaptor.capture()); + IdempotentRequestResponseWrapper idempotentRequestResponseWrapper = captor.getValue(); + assertEquals(idempotentRequestResponseWrapper.getResponse(), responseWrapper.getResponse()); + } + + + @Test + public void setResponse() { + //Given + IdempotencyKey idempotencyKey = new IdempotencyKey("key"); + IdempotentRequestResponseWrapper wrapper = new IdempotentRequestResponseWrapper(); + GetResult getResult = mock(GetResult.class); + ExistsResult existsResult = mock(ExistsResult.class); + when(existsResult.exists()).thenReturn(true); + when(getResult.contentAs(IdempotentRequestResponseWrapper.class)).thenReturn(wrapper); + + when(collection.get(eq(idempotencyKey.getKeyValue()),any())).thenReturn(getResult); + when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); + when(collection.upsert(idempotencyKey.getKeyValue(),wrapper)).thenReturn(mock(MutationResult.class)); + //When + couchbaseIdempotentRepository.setResponse(idempotencyKey,mock(IdempotentRequestWrapper.class), + mock(IdempotentResponseWrapper.class)); + + //Then + verify(collection, times(1)).get(eq(idempotencyKey.getKeyValue()),any()); + } + + @Test + public void setResponse_when_given_a_ttl() { + //Given + IdempotencyKey idempotencyKey = new IdempotencyKey("key"); + IdempotentRequestResponseWrapper wrapper = new IdempotentRequestResponseWrapper(); + GetResult getResult = mock(GetResult.class); + ExistsResult existsResult = mock(ExistsResult.class); + when(existsResult.exists()).thenReturn(true); + when(getResult.contentAs(IdempotentRequestResponseWrapper.class)).thenReturn(wrapper); + when(getResult.expiry()).thenReturn(Optional.of(mock(Duration.class))); + when(collection.get(eq(idempotencyKey.getKeyValue()),any())).thenReturn(getResult); + when(collection.exists(idempotencyKey.getKeyValue())).thenReturn(existsResult); + when(collection.upsert(eq(idempotencyKey.getKeyValue()),eq(wrapper),any())).thenReturn(mock(MutationResult.class)); + //When + couchbaseIdempotentRepository.setResponse(idempotencyKey,mock(IdempotentRequestWrapper.class), + mock(IdempotentResponseWrapper.class),5L,TimeUnit.DAYS); + + //Then + verify(collection, times(1)).get(eq(idempotencyKey.getKeyValue()),any()); + } +} \ No newline at end of file diff --git a/Jdempotent-spring-boot-couchbase-starter/target/Jdempotent-spring-boot-couchbase-starter-1.0.5.jar b/Jdempotent-spring-boot-couchbase-starter/target/Jdempotent-spring-boot-couchbase-starter-1.0.5.jar new file mode 100644 index 0000000..e50f192 Binary files /dev/null and b/Jdempotent-spring-boot-couchbase-starter/target/Jdempotent-spring-boot-couchbase-starter-1.0.5.jar differ diff --git a/Jdempotent-spring-boot-couchbase-starter/target/classes/META-INF/spring.factories b/Jdempotent-spring-boot-couchbase-starter/target/classes/META-INF/spring.factories new file mode 100644 index 0000000..9e0289d --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/target/classes/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.trendyol.jdempotent.couchbase.CouchbaseConfig,\ +com.trendyol.jdempotent.couchbase.CouchbaseBeanConfig,\ +com.trendyol.jdempotent.couchbase.ApplicationConfig \ No newline at end of file diff --git a/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/ApplicationConfig.class b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/ApplicationConfig.class new file mode 100644 index 0000000..6188197 Binary files /dev/null and b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/ApplicationConfig.class differ diff --git a/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class new file mode 100644 index 0000000..a047f8f Binary files /dev/null and b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class differ diff --git a/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseConfig.class b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseConfig.class new file mode 100644 index 0000000..ef302e8 Binary files /dev/null and b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseConfig.class differ diff --git a/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class new file mode 100644 index 0000000..5223d02 Binary files /dev/null and b/Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class differ diff --git a/Jdempotent-spring-boot-couchbase-starter/target/maven-archiver/pom.properties b/Jdempotent-spring-boot-couchbase-starter/target/maven-archiver/pom.properties new file mode 100644 index 0000000..10437fa --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/target/maven-archiver/pom.properties @@ -0,0 +1,4 @@ +#Created by Apache Maven 3.6.3 +groupId=com.trendyol +artifactId=Jdempotent-spring-boot-couchbase-starter +version=1.0.5 diff --git a/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..d352f8b --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,4 @@ +com/trendyol/jdempotent/couchbase/ApplicationConfig.class +com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class +com/trendyol/jdempotent/couchbase/CouchbaseConfig.class +com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class diff --git a/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..07abe28 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,4 @@ +/Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java +/Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java +/Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java +/Users/mehmet.ari/Documents/codes/ty/Jdempotent/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java diff --git a/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/Jdempotent-spring-boot-couchbase-starter/target/test-classes/CouchbaseIdempotentRepositoryTest.class b/Jdempotent-spring-boot-couchbase-starter/target/test-classes/CouchbaseIdempotentRepositoryTest.class new file mode 100644 index 0000000..18898d1 Binary files /dev/null and b/Jdempotent-spring-boot-couchbase-starter/target/test-classes/CouchbaseIdempotentRepositoryTest.class differ diff --git a/Jdempotent-spring-boot-redis-starter/pom.xml b/Jdempotent-spring-boot-redis-starter/pom.xml index cdb4746..fc3e1f6 100644 --- a/Jdempotent-spring-boot-redis-starter/pom.xml +++ b/Jdempotent-spring-boot-redis-starter/pom.xml @@ -6,7 +6,7 @@ 4.0.0 com.trendyol Jdempotent-spring-boot-redis-starter - 1.0.4 + 1.0.6 Jdempotent-spring-boot-redis-starter jar https://github.com/Trendyol/Jdempotent/tree/master/Jdempotent-spring-boot-redis-starter @@ -54,7 +54,7 @@ com.trendyol Jdempotent-core - 1.0.4 + 1.0.6 redis.clients @@ -80,6 +80,11 @@ 4.13.1 test + + org.junit.vintage + junit-vintage-engine + 5.7.0 + org.springframework.boot spring-boot-starter-test diff --git a/Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisIdempotentRepository.java b/Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisIdempotentRepository.java index 00cb51a..f9890c5 100644 --- a/Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisIdempotentRepository.java +++ b/Jdempotent-spring-boot-redis-starter/src/main/java/com/trendyol/jdempotent/redis/RedisIdempotentRepository.java @@ -100,6 +100,5 @@ private IdempotentRequestResponseWrapper prepareValue(IdempotentRequestWrapper r } return new IdempotentRequestResponseWrapper(null); } - } diff --git a/README.md b/README.md index 1e2e39c..de509b6 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,22 @@ Make your endpoints idempotent easily 1 - First of all, you need to add a dependency to pom.xml +For Redis: ```xml com.trendyol Jdempotent-spring-boot-redis-starter - 1.0.4 + 1.0.6 ``` +For Couchbase +```xml + + com.trendyol + Jdempotent-spring-boot-couchbase-starter + 1.0.6 + +``` 2 - You should add `@IdempotentResource` annotation to the method that you want to make idempotent resource, listener etc. @@ -61,7 +70,9 @@ public class AspectConditionalCallback implements ErrorConditionalCallback { } ``` -4 - Let's make the redis configuration: +4 - Let's make the configuration: + +For redis configuration: ```yaml jdempotent: @@ -81,10 +92,29 @@ jdempotent: expireTimeoutHour: 3 ``` +For couchbase configuration: + +```yaml +jdempotent: + enable: true + cryptography: + algorithm: MD5 + cache: + couchbase: + connection-string: XXXXXXXX + password: XXXXXXXX + username: XXXXXXXX + bucket-name: XXXXXXXX + connect-timeout: 100000 + query-timeout: 20000 + kv-timeout: 3000 +``` + Please note that you can disable Jdempotent easily if you need to. For example, assume that you don't have a circut breaker and your Redis is down. In that case, you can disable Jdempotent with the following configuration: + ```yaml enable: false ``` @@ -106,11 +136,4 @@ As it is shown in the following image, the most cpu consuming part of Jdempotent ### Docs [Jdempotent Medium Article](https://medium.com/trendyol-tech/an-idempotency-library-jdempotent-5cd2cd0b76ff)
[Jdempotent-core Javadoc](https://memojja.github.io/jdempotent-core/index.html)
-[Jdempotent-spring-boot-redis-starter Javadoc](https://memojja.github.io/jdempotent-spring-boot-redis-starter/index.html) - -### TODOS -- [ ] Disable request&response configgi -- [ ] Write examples under the examples folders -- [ ] Support multiple request paylaod as a paramater -- [ ] Ignore a throwing custom exception like ErrorConditionalCallback -- [ ] Support multiple datasources +[Jdempotent-spring-boot-redis-starter Javadoc](https://memojja.github.io/jdempotent-spring-boot-redis-starter/index.html) \ No newline at end of file diff --git a/examples/jdempotent-couchbase-example/.gitignore b/examples/jdempotent-couchbase-example/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/examples/jdempotent-couchbase-example/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/examples/jdempotent-couchbase-example/.mvn/wrapper/MavenWrapperDownloader.java b/examples/jdempotent-couchbase-example/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..a45eb6b --- /dev/null +++ b/examples/jdempotent-couchbase-example/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.jar b/examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.jar differ diff --git a/examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.properties b/examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..642d572 --- /dev/null +++ b/examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/examples/jdempotent-couchbase-example/mvnw b/examples/jdempotent-couchbase-example/mvnw new file mode 100755 index 0000000..a16b543 --- /dev/null +++ b/examples/jdempotent-couchbase-example/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/jdempotent-couchbase-example/mvnw.cmd b/examples/jdempotent-couchbase-example/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/examples/jdempotent-couchbase-example/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/examples/jdempotent-couchbase-example/pom.xml b/examples/jdempotent-couchbase-example/pom.xml new file mode 100644 index 0000000..788b3c3 --- /dev/null +++ b/examples/jdempotent-couchbase-example/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.3.5.RELEASE + + + com.jdempotent.example + demo + 0.0.1-SNAPSHOT + Mail Sender App + Demo project for Jdempotent + + + 11 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.kafka + spring-kafka + + + org.springframework.boot + spring-boot-starter-mail + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.kafka + spring-kafka-test + test + + + com.trendyol + Jdempotent-spring-boot-couchbase-starter + 1.0.6 + + + + diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/DemoApplication.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/DemoApplication.java new file mode 100644 index 0000000..dff792b --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/DemoApplication.java @@ -0,0 +1,13 @@ +package com.jdempotent.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/controller/MailController.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/controller/MailController.java new file mode 100644 index 0000000..5d9ac8b --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/controller/MailController.java @@ -0,0 +1,63 @@ +package com.jdempotent.example.demo.controller; + +import com.jdempotent.example.demo.exception.InvalidEmailAddressException; +import com.jdempotent.example.demo.model.SendEmailRequest; +import com.jdempotent.example.demo.model.SendEmailResponse; +import com.jdempotent.example.demo.service.MailSenderService; +import com.trendyol.jdempotent.core.annotation.IdempotentResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.mail.MessagingException; +import java.util.concurrent.TimeUnit; + +@RestController +public class MailController { + + @Autowired + private MailSenderService mailSenderService; + + private static final Logger logger = LoggerFactory.getLogger(MailController.class); + + @PostMapping("/send-email") + @IdempotentResource(cachePrefix = "MailController.sendEmail") + public ResponseEntity sendEmail(@RequestBody SendEmailRequest request) { + if (StringUtils.isEmpty(request.getEmail())) { + throw new InvalidEmailAddressException(); + } + + try { + mailSenderService.sendMail(request); + } catch (MessagingException e) { + logger.debug("MailSenderService.sendEmail() throw exception: {} request: {} ", e, request); + } + + return new ResponseEntity(new SendEmailResponse("We will send your message"), HttpStatus.ACCEPTED); + } + + @PostMapping("v2/send-email") + @IdempotentResource( + cachePrefix = "MailController.sendEmailV2", + ttl = 1, + ttlTimeUnit = TimeUnit.MINUTES) + public ResponseEntity sendEmailV2(@RequestBody SendEmailRequest request) { + if (StringUtils.isEmpty(request.getEmail())) { + throw new InvalidEmailAddressException(); + } + + try { + mailSenderService.sendMail(request); + } catch (MessagingException e) { + logger.debug("MailSenderService.sendEmail() throw exception: {} request: {} ", e, request); + } + + return new ResponseEntity(new SendEmailResponse("We will send your message"), HttpStatus.ACCEPTED); + } +} diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/CustomExceptionHandler.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/CustomExceptionHandler.java new file mode 100644 index 0000000..85d2924 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/CustomExceptionHandler.java @@ -0,0 +1,31 @@ +package com.jdempotent.example.demo.exception; + +import com.jdempotent.example.demo.model.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.ArrayList; +import java.util.List; + +@ControllerAdvice +public class CustomExceptionHandler extends ResponseEntityExceptionHandler { + @ExceptionHandler(Exception.class) + public final ResponseEntity handleAllExceptions(Exception ex, WebRequest request) { + List details = new ArrayList<>(); + details.add(ex.getLocalizedMessage()); + ErrorResponse error = new ErrorResponse("Server Error", details); + return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(InvalidEmailAddressException.class) + public final ResponseEntity handleRecordNotFoundException(InvalidEmailAddressException ex, WebRequest request) { + List details = new ArrayList<>(); + details.add(ex.getLocalizedMessage()); + ErrorResponse error = new ErrorResponse("Invalid email address", details); + return new ResponseEntity(error, HttpStatus.NOT_FOUND); + } +} \ No newline at end of file diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/InvalidEmailAddressException.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/InvalidEmailAddressException.java new file mode 100644 index 0000000..528a004 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/InvalidEmailAddressException.java @@ -0,0 +1,4 @@ +package com.jdempotent.example.demo.exception; + +public class InvalidEmailAddressException extends RuntimeException { +} diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/RetryIdempotentRequestException.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/RetryIdempotentRequestException.java new file mode 100644 index 0000000..9cb20f1 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/RetryIdempotentRequestException.java @@ -0,0 +1,8 @@ +package com.jdempotent.example.demo.exception; + +import javax.mail.MessagingException; + +public class RetryIdempotentRequestException extends RuntimeException { + public RetryIdempotentRequestException(MessagingException e) { + } +} diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/listener/WelcomingListener.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/listener/WelcomingListener.java new file mode 100644 index 0000000..21e2bf4 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/listener/WelcomingListener.java @@ -0,0 +1,47 @@ +package com.jdempotent.example.demo.listener; + +import com.trendyol.jdempotent.core.annotation.IdempotentResource; +import com.jdempotent.example.demo.exception.RetryIdempotentRequestException; +import com.jdempotent.example.demo.model.SendEmailRequest; +import com.jdempotent.example.demo.service.MailSenderService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; + +import javax.mail.MessagingException; + +@Service +public class WelcomingListener { + + @Autowired + private MailSenderService mailSenderService; + private static final Logger logger = LoggerFactory.getLogger(WelcomingListener.class); + + @Value("${template.welcoming.message}") + private String message; + + @Value("${template.welcoming.subject}") + private String subject; + + @KafkaListener(topics = "trendyol.mail.welcome", groupId = "group_id") + @IdempotentResource + public void consumeMessage(String emailAdress) { + SendEmailRequest request = SendEmailRequest.builder() + .email(message) + .subject(subject) + .build(); + + try { + mailSenderService.sendMail(request); + } catch (MessagingException e) { + logger.error("MailSenderService.sendEmail() throw exception {} event: {} ", e, emailAdress); + + // Throwing any exception is enough to delete from redis. When successful, it will not be deleted from redis and will be idempotent. + throw new RetryIdempotentRequestException(e); + } + } + +} \ No newline at end of file diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/ErrorResponse.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/ErrorResponse.java new file mode 100644 index 0000000..7b04385 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/ErrorResponse.java @@ -0,0 +1,22 @@ +package com.jdempotent.example.demo.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.List; + +@AllArgsConstructor +@Getter +@Setter +@ToString +public class ErrorResponse implements Serializable { + private String message; + private List details; + + public ErrorResponse(String message){ + this.message = message; + } +} diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailRequest.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailRequest.java new file mode 100644 index 0000000..ba87424 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailRequest.java @@ -0,0 +1,18 @@ +package com.jdempotent.example.demo.model; + +import lombok.*; +import org.springframework.lang.NonNull; + +import java.io.Serializable; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@ToString +public class SendEmailRequest implements Serializable { + private String email; + private String subject; + private String message; +} \ No newline at end of file diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailResponse.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailResponse.java new file mode 100644 index 0000000..3edd907 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailResponse.java @@ -0,0 +1,16 @@ +package com.jdempotent.example.demo.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +@Getter +@Setter +@ToString +@AllArgsConstructor +public class SendEmailResponse implements Serializable { + private String message; +} diff --git a/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/service/MailSenderService.java b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/service/MailSenderService.java new file mode 100644 index 0000000..17c881a --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/service/MailSenderService.java @@ -0,0 +1,49 @@ +package com.jdempotent.example.demo.service; + +import com.jdempotent.example.demo.model.SendEmailRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.File; + +@Service +public class MailSenderService { + + private JavaMailSender javaMailSender; + + @Autowired + public MailSenderService(JavaMailSender javaMailSender) { + this.javaMailSender = javaMailSender; + } + + @Value("${email.from.address}") + private String fromAddress; + + public void sendMail(SendEmailRequest emailRequest) throws MessagingException { + sendMailMultipart(emailRequest.getEmail(), emailRequest.getSubject(), emailRequest.getMessage(), null); + } + + public void sendMail(SendEmailRequest emailRequest, File file) throws MessagingException { + sendMailMultipart(emailRequest.getEmail(), emailRequest.getSubject(), emailRequest.getMessage(), file); + } + + private void sendMailMultipart(String toEmail, String subject, String message, File file) throws MessagingException { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); + helper.setFrom(fromAddress); + helper.setTo(toEmail); + helper.setSubject(subject); + helper.setText(message); + + if (file != null) { + helper.addAttachment(file.getName(), file); + } + javaMailSender.send(mimeMessage); + } +} diff --git a/examples/jdempotent-couchbase-example/src/main/resources/application.yml b/examples/jdempotent-couchbase-example/src/main/resources/application.yml new file mode 100644 index 0000000..40a6d3a --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/resources/application.yml @@ -0,0 +1,33 @@ +jdempotent: + enable: true + cryptography: + algorithm: MD5 + cache: + persistReqRes: false + couchbase: + connection-string: XXXXXXXX + password: XXXXXXXX + username: XXXXXXXX + bucket-name: XXXXXXXX + connect-timeout: 100000 + query-timeout: 20000 + kv-timeout: 3000 + +email: + from: + address: XXXXXXX + +spring: + mail: + host: smtp.gmail.com + port: 587 + username: XXXXXXX + password: XXXXXXX + properties.mail.smtp: + auth: true + starttls.enable: true + +template: + welcoming: + subject: "Welcoming" + 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" \ No newline at end of file diff --git a/examples/jdempotent-couchbase-example/src/main/resources/logback-spring.xml b/examples/jdempotent-couchbase-example/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..27937a9 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/resources/logback-spring.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/jdempotent-couchbase-example/src/test/java/com/jdempotent/example/demo/DemoApplicationTests.java b/examples/jdempotent-couchbase-example/src/test/java/com/jdempotent/example/demo/DemoApplicationTests.java new file mode 100644 index 0000000..9f29592 --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/test/java/com/jdempotent/example/demo/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.jdempotent.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/examples/jdempotent-redis-example/pom.xml b/examples/jdempotent-redis-example/pom.xml index 7b08513..0218ada 100644 --- a/examples/jdempotent-redis-example/pom.xml +++ b/examples/jdempotent-redis-example/pom.xml @@ -59,7 +59,7 @@ com.trendyol Jdempotent-spring-boot-redis-starter - 1.0.4 + 1.0.6 diff --git a/pom.xml b/pom.xml index 049d4b0..6a72864 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.trendyol jdempotent pom - 1.0.4 + 1.0.6 Jdempotent https://github.com/Trendyol/Jdempotent Jdempotent @@ -44,6 +44,7 @@ Jdempotent-core Jdempotent-spring-boot-redis-starter + Jdempotent-spring-boot-couchbase-starter