From ee72e733c4a5092d889563f78ad6fecc47003433 Mon Sep 17 00:00:00 2001 From: "mehmet.ari" Date: Thu, 13 May 2021 22:48:11 +0300 Subject: [PATCH 1/8] add jdempotent couchbase starter base and example project --- .../pom.xml | 20 ++ .../settings.xml | 21 ++ .../couchbase/ApplicationConfig.java | 2 + .../couchbase/CouchbaseBeanConfig.java | 2 + .../jdempotent/couchbase/CouchbaseConfig.java | 114 +++++++ .../CouchbaseIdempotentRepository.java | 2 + .../main/resources/META-INF/spring.factories | 7 + .../jdempotent-couchbase-example/.gitignore | 33 ++ .../.mvn/wrapper/MavenWrapperDownloader.java | 118 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + examples/jdempotent-couchbase-example/mvnw | 310 ++++++++++++++++++ .../jdempotent-couchbase-example/mvnw.cmd | 182 ++++++++++ examples/jdempotent-couchbase-example/pom.xml | 66 ++++ .../example/demo/DemoApplication.java | 13 + .../demo/controller/MailController.java | 63 ++++ .../exception/CustomExceptionHandler.java | 31 ++ .../InvalidEmailAddressException.java | 4 + .../RetryIdempotentRequestException.java | 8 + .../demo/listener/WelcomingListener.java | 47 +++ .../example/demo/model/ErrorResponse.java | 22 ++ .../example/demo/model/SendEmailRequest.java | 18 + .../example/demo/model/SendEmailResponse.java | 16 + .../demo/service/MailSenderService.java | 49 +++ .../src/main/resources/application.yml | 36 ++ .../src/main/resources/logback-spring.xml | 17 + .../example/demo/DemoApplicationTests.java | 13 + 27 files changed, 1216 insertions(+) create mode 100644 Jdempotent-spring-boot-couchbase-starter/pom.xml create mode 100644 Jdempotent-spring-boot-couchbase-starter/settings.xml create mode 100644 Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java create mode 100644 Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java create mode 100644 Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java create mode 100644 Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java create mode 100644 Jdempotent-spring-boot-couchbase-starter/src/main/resources/META-INF/spring.factories create mode 100644 examples/jdempotent-couchbase-example/.gitignore create mode 100644 examples/jdempotent-couchbase-example/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.jar create mode 100644 examples/jdempotent-couchbase-example/.mvn/wrapper/maven-wrapper.properties create mode 100755 examples/jdempotent-couchbase-example/mvnw create mode 100644 examples/jdempotent-couchbase-example/mvnw.cmd create mode 100644 examples/jdempotent-couchbase-example/pom.xml create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/DemoApplication.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/controller/MailController.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/CustomExceptionHandler.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/InvalidEmailAddressException.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/exception/RetryIdempotentRequestException.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/listener/WelcomingListener.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/ErrorResponse.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailRequest.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/model/SendEmailResponse.java create mode 100644 examples/jdempotent-couchbase-example/src/main/java/com/jdempotent/example/demo/service/MailSenderService.java create mode 100644 examples/jdempotent-couchbase-example/src/main/resources/application.yml create mode 100644 examples/jdempotent-couchbase-example/src/main/resources/logback-spring.xml create mode 100644 examples/jdempotent-couchbase-example/src/test/java/com/jdempotent/example/demo/DemoApplicationTests.java diff --git a/Jdempotent-spring-boot-couchbase-starter/pom.xml b/Jdempotent-spring-boot-couchbase-starter/pom.xml new file mode 100644 index 0000000..09ab30d --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/pom.xml @@ -0,0 +1,20 @@ + + + + jdempotent + com.trendyol + 1.0.3 + + 4.0.0 + + Jdempotent-spring-boot-couchbase-starter + 1.0.5 + + + 15 + 15 + + + \ No newline at end of file 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..8ed0b31 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java @@ -0,0 +1,2 @@ +package com.trendyol.jdempotent.couchbase;public class ApplicationConfig { +} 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..6e31261 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.java @@ -0,0 +1,2 @@ +package com.trendyol.jdempotent.couchbase;public class CouchbaseBeanConfig { +} 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..dac5617 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseConfig.java @@ -0,0 +1,114 @@ +package com.trendyol.jdempotent.couchbase; + +import com.couchbase.client.core.deps.io.netty.channel.epoll.EpollEventLoopGroup; +import com.couchbase.client.core.env.CompressionConfig; +import com.couchbase.client.core.env.IoConfig; +import com.couchbase.client.core.env.IoEnvironment; +import com.couchbase.client.core.env.LoggerConfig; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.core.env.TimeoutConfig; +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 java.time.Duration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.apache.commons.lang3.SystemUtils; +import org.springframework.context.annotation.Primary; + +@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:true}") + 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..20a0e51 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java @@ -0,0 +1,2 @@ +package com.trendyol.jdempotent.couchbase;public class CouchbaseIdempotentRepository { +} 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..6ef0076 --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.trendyol.jdempotent.redis.ApplicationConfig,\ +com.trendyol.jdempotent.redis.RedisConfigProperties,\ +com.trendyol.jdempotent.redis.RedisSentinelConfiguration,\ +org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration +org.springframework.boot.env.EnvironmentPostProcessor=\ +com.trendyol.jdempotent.redis.RedisEnvironmentPostProcessor \ 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 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 zcmbTd1CVCTmM+|7+wQV$+qP}n>auOywyU~q+qUhh+uxis_~*a##hm*_WW?9E7Pb7N%LRFiwbEGCJ0XP=%-6oeT$XZcYgtzC2~q zk(K08IQL8oTl}>>+hE5YRgXTB@fZ4TH9>7=79e`%%tw*SQUa9~$xKD5rS!;ZG@ocK zQdcH}JX?W|0_Afv?y`-NgLum62B&WSD$-w;O6G0Sm;SMX65z)l%m1e-g8Q$QTI;(Q z+x$xth4KFvH@Bs6(zn!iF#nenk^Y^ce;XIItAoCsow38eq?Y-Auh!1in#Rt-_D>H^ z=EjbclGGGa6VnaMGmMLj`x3NcwA43Jb(0gzl;RUIRAUDcR1~99l2SAPkVhoRMMtN} zXvC<tOmX83grD8GSo_Lo?%lNfhD#EBgPo z*nf@ppMC#B!T)Ae0RG$mlJWmGl7CkuU~B8-==5i;rS;8i6rJ=PoQxf446XDX9g|c> zU64ePyMlsI^V5Jq5A+BPe#e73+kpc_r1tv#B)~EZ;7^67F0*QiYfrk0uVW;Qb=NsG zN>gsuCwvb?s-KQIppEaeXtEMdc9dy6Dfduz-tMTms+i01{eD9JE&h?Kht*$eOl#&L zJdM_-vXs(V#$Ed;5wyNWJdPNh+Z$+;$|%qR(t`4W@kDhd*{(7-33BOS6L$UPDeE_53j${QfKN-0v-HG z(QfyvFNbwPK%^!eIo4ac1;b>c0vyf9}Xby@YY!lkz-UvNp zwj#Gg|4B~?n?G^{;(W;|{SNoJbHTMpQJ*Wq5b{l9c8(%?Kd^1?H1om1de0Da9M;Q=n zUfn{f87iVb^>Exl*nZ0hs(Yt>&V9$Pg`zX`AI%`+0SWQ4Zc(8lUDcTluS z5a_KerZWe}a-MF9#Cd^fi!y3%@RFmg&~YnYZ6<=L`UJ0v={zr)>$A;x#MCHZy1st7 ztT+N07NR+vOwSV2pvWuN1%lO!K#Pj0Fr>Q~R40{bwdL%u9i`DSM4RdtEH#cW)6}+I-eE< z&tZs+(Ogu(H_;$a$!7w`MH0r%h&@KM+<>gJL@O~2K2?VrSYUBbhCn#yy?P)uF3qWU z0o09mIik+kvzV6w>vEZy@&Mr)SgxPzUiDA&%07m17udz9usD82afQEps3$pe!7fUf z0eiidkJ)m3qhOjVHC_M(RYCBO%CZKZXFb8}s0-+}@CIn&EF(rRWUX2g^yZCvl0bI} zbP;1S)iXnRC&}5-Tl(hASKqdSnO?ASGJ*MIhOXIblmEudj(M|W!+I3eDc}7t`^mtg z)PKlaXe(OH+q-)qcQ8a@!llRrpGI8DsjhoKvw9T;TEH&?s=LH0w$EzI>%u;oD@x83 zJL7+ncjI9nn!TlS_KYu5vn%f*@qa5F;| zEFxY&B?g=IVlaF3XNm_03PA)=3|{n-UCgJoTr;|;1AU9|kPE_if8!Zvb}0q$5okF$ zHaJdmO&gg!9oN|M{!qGE=tb|3pVQ8PbL$}e;NgXz<6ZEggI}wO@aBP**2Wo=yN#ZC z4G$m^yaM9g=|&!^ft8jOLuzc3Psca*;7`;gnHm}tS0%f4{|VGEwu45KptfNmwxlE~ z^=r30gi@?cOm8kAz!EylA4G~7kbEiRlRIzwrb~{_2(x^$-?|#e6Bi_**(vyr_~9Of z!n>Gqf+Qwiu!xhi9f53=PM3`3tNF}pCOiPU|H4;pzjcsqbwg*{{kyrTxk<;mx~(;; z1NMrpaQ`57yn34>Jo3b|HROE(UNcQash!0p2-!Cz;{IRv#Vp5!3o$P8!%SgV~k&Hnqhp`5eLjTcy93cK!3Hm-$`@yGnaE=?;*2uSpiZTs_dDd51U%i z{|Zd9ou-;laGS_x=O}a+ zB||za<795A?_~Q=r=coQ+ZK@@ zId~hWQL<%)fI_WDIX#=(WNl!Dm$a&ROfLTd&B$vatq!M-2Jcs;N2vps$b6P1(N}=oI3<3luMTmC|0*{ zm1w8bt7vgX($!0@V0A}XIK)w!AzUn7vH=pZEp0RU0p?}ch2XC-7r#LK&vyc2=-#Q2 z^L%8)JbbcZ%g0Du;|8=q8B>X=mIQirpE=&Ox{TiuNDnOPd-FLI^KfEF729!!0x#Es z@>3ursjFSpu%C-8WL^Zw!7a0O-#cnf`HjI+AjVCFitK}GXO`ME&on|^=~Zc}^LBp9 zj=-vlN;Uc;IDjtK38l7}5xxQF&sRtfn4^TNtnzXv4M{r&ek*(eNbIu!u$>Ed%` z5x7+&)2P&4>0J`N&ZP8$vcR+@FS0126s6+Jx_{{`3ZrIMwaJo6jdrRwE$>IU_JTZ} z(||hyyQ)4Z1@wSlT94(-QKqkAatMmkT7pCycEB1U8KQbFX&?%|4$yyxCtm3=W`$4fiG0WU3yI@c zx{wfmkZAYE_5M%4{J-ygbpH|(|GD$2f$3o_Vti#&zfSGZMQ5_f3xt6~+{RX=$H8at z?GFG1Tmp}}lmm-R->ve*Iv+XJ@58p|1_jRvfEgz$XozU8#iJS})UM6VNI!3RUU!{5 zXB(+Eqd-E;cHQ>)`h0(HO_zLmzR3Tu-UGp;08YntWwMY-9i^w_u#wR?JxR2bky5j9 z3Sl-dQQU$xrO0xa&>vsiK`QN<$Yd%YXXM7*WOhnRdSFt5$aJux8QceC?lA0_if|s> ze{ad*opH_kb%M&~(~&UcX0nFGq^MqjxW?HJIP462v9XG>j(5Gat_)#SiNfahq2Mz2 zU`4uV8m$S~o9(W>mu*=h%Gs(Wz+%>h;R9Sg)jZ$q8vT1HxX3iQnh6&2rJ1u|j>^Qf`A76K%_ubL`Zu?h4`b=IyL>1!=*%!_K)=XC z6d}4R5L+sI50Q4P3upXQ3Z!~1ZXLlh!^UNcK6#QpYt-YC=^H=EPg3)z*wXo*024Q4b2sBCG4I# zlTFFY=kQ>xvR+LsuDUAk)q%5pEcqr(O_|^spjhtpb1#aC& zghXzGkGDC_XDa%t(X`E+kvKQ4zrQ*uuQoj>7@@ykWvF332)RO?%AA&Fsn&MNzmFa$ zWk&&^=NNjxLjrli_8ESU)}U|N{%j&TQmvY~lk!~Jh}*=^INA~&QB9em!in_X%Rl1&Kd~Z(u z9mra#<@vZQlOY+JYUwCrgoea4C8^(xv4ceCXcejq84TQ#sF~IU2V}LKc~Xlr_P=ry zl&Hh0exdCbVd^NPCqNNlxM3vA13EI8XvZ1H9#bT7y*U8Y{H8nwGpOR!e!!}*g;mJ#}T{ekSb}5zIPmye*If(}}_=PcuAW#yidAa^9-`<8Gr0 z)Fz=NiZ{)HAvw{Pl5uu)?)&i&Us$Cx4gE}cIJ}B4Xz~-q7)R_%owbP!z_V2=Aq%Rj z{V;7#kV1dNT9-6R+H}}(ED*_!F=~uz>&nR3gb^Ce%+0s#u|vWl<~JD3MvS0T9thdF zioIG3c#Sdsv;LdtRv3ml7%o$6LTVL>(H`^@TNg`2KPIk*8-IB}X!MT0`hN9Ddf7yN z?J=GxPL!uJ7lqwowsl?iRrh@#5C$%E&h~Z>XQcvFC*5%0RN-Opq|=IwX(dq(*sjs+ zqy99+v~m|6T#zR*e1AVxZ8djd5>eIeCi(b8sUk)OGjAsKSOg^-ugwl2WSL@d#?mdl zib0v*{u-?cq}dDGyZ%$XRY=UkQwt2oGu`zQneZh$=^! zj;!pCBWQNtvAcwcWIBM2y9!*W|8LmQy$H~5BEx)78J`4Z0(FJO2P^!YyQU{*Al+fs z){!4JvT1iLrJ8aU3k0t|P}{RN)_^v%$$r;+p0DY7N8CXzmS*HB*=?qaaF9D@#_$SN zSz{moAK<*RH->%r7xX~9gVW$l7?b|_SYI)gcjf0VAUJ%FcQP(TpBs; zg$25D!Ry_`8xpS_OJdeo$qh#7U+cepZ??TII7_%AXsT$B z=e)Bx#v%J0j``00Zk5hsvv6%T^*xGNx%KN-=pocSoqE5_R)OK%-Pbu^1MNzfds)mL zxz^F4lDKV9D&lEY;I+A)ui{TznB*CE$=9(wgE{m}`^<--OzV-5V4X2w9j(_!+jpTr zJvD*y6;39&T+==$F&tsRKM_lqa1HC}aGL0o`%c9mO=fts?36@8MGm7Vi{Y z^<7m$(EtdSr#22<(rm_(l_(`j!*Pu~Y>>xc>I9M#DJYDJNHO&4=HM%YLIp?;iR&$m z#_$ZWYLfGLt5FJZhr3jpYb`*%9S!zCG6ivNHYzNHcI%khtgHBliM^Ou}ZVD7ehU9 zS+W@AV=?Ro!=%AJ>Kcy9aU3%VX3|XM_K0A+ZaknKDyIS3S-Hw1C7&BSW5)sqj5Ye_ z4OSW7Yu-;bCyYKHFUk}<*<(@TH?YZPHr~~Iy%9@GR2Yd}J2!N9K&CN7Eq{Ka!jdu; zQNB*Y;i(7)OxZK%IHGt#Rt?z`I|A{q_BmoF!f^G}XVeTbe1Wnzh%1g>j}>DqFf;Rp zz7>xIs12@Ke0gr+4-!pmFP84vCIaTjqFNg{V`5}Rdt~xE^I;Bxp4)|cs8=f)1YwHz zqI`G~s2~qqDV+h02b`PQpUE#^^Aq8l%y2|ByQeXSADg5*qMprEAE3WFg0Q39`O+i1 z!J@iV!`Y~C$wJ!5Z+j5$i<1`+@)tBG$JL=!*uk=2k;T<@{|s1$YL079FvK%mPhyHV zP8^KGZnp`(hVMZ;s=n~3r2y;LTwcJwoBW-(ndU-$03{RD zh+Qn$ja_Z^OuMf3Ub|JTY74s&Am*(n{J3~@#OJNYuEVVJd9*H%)oFoRBkySGm`hx! zT3tG|+aAkXcx-2Apy)h^BkOyFTWQVeZ%e2@;*0DtlG9I3Et=PKaPt&K zw?WI7S;P)TWED7aSH$3hL@Qde?H#tzo^<(o_sv_2ci<7M?F$|oCFWc?7@KBj-;N$P zB;q!8@bW-WJY9do&y|6~mEruZAVe$!?{)N9rZZxD-|oltkhW9~nR8bLBGXw<632!l z*TYQn^NnUy%Ds}$f^=yQ+BM-a5X4^GHF=%PDrRfm_uqC zh{sKwIu|O0&jWb27;wzg4w5uA@TO_j(1X?8E>5Zfma|Ly7Bklq|s z9)H`zoAGY3n-+&JPrT!>u^qg9Evx4y@GI4$n-Uk_5wttU1_t?6><>}cZ-U+&+~JE) zPlDbO_j;MoxdLzMd~Ew|1o^a5q_1R*JZ=#XXMzg?6Zy!^hop}qoLQlJ{(%!KYt`MK z8umEN@Z4w!2=q_oe=;QttPCQy3Nm4F@x>@v4sz_jo{4m*0r%J(w1cSo;D_hQtJs7W z><$QrmG^+<$4{d2bgGo&3-FV}avg9zI|Rr(k{wTyl3!M1q+a zD9W{pCd%il*j&Ft z5H$nENf>>k$;SONGW`qo6`&qKs*T z2^RS)pXk9b@(_Fw1bkb)-oqK|v}r$L!W&aXA>IpcdNZ_vWE#XO8X`#Yp1+?RshVcd zknG%rPd*4ECEI0wD#@d+3NbHKxl}n^Sgkx==Iu%}HvNliOqVBqG?P2va zQ;kRJ$J6j;+wP9cS za#m;#GUT!qAV%+rdWolk+)6kkz4@Yh5LXP+LSvo9_T+MmiaP-eq6_k;)i6_@WSJ zlT@wK$zqHu<83U2V*yJ|XJU4farT#pAA&@qu)(PO^8PxEmPD4;Txpio+2)#!9 z>&=i7*#tc0`?!==vk>s7V+PL#S1;PwSY?NIXN2=Gu89x(cToFm))7L;< z+bhAbVD*bD=}iU`+PU+SBobTQ%S!=VL!>q$rfWsaaV}Smz>lO9JXT#`CcH_mRCSf4%YQAw`$^yY z3Y*^Nzk_g$xn7a_NO(2Eb*I=^;4f!Ra#Oo~LLjlcjke*k*o$~U#0ZXOQ5@HQ&T46l z7504MUgZkz2gNP1QFN8Y?nSEnEai^Rgyvl}xZfMUV6QrJcXp;jKGqB=D*tj{8(_pV zqyB*DK$2lgYGejmJUW)*s_Cv65sFf&pb(Yz8oWgDtQ0~k^0-wdF|tj}MOXaN@ydF8 zNr={U?=;&Z?wr^VC+`)S2xl}QFagy;$mG=TUs7Vi2wws5zEke4hTa2)>O0U?$WYsZ z<8bN2bB_N4AWd%+kncgknZ&}bM~eDtj#C5uRkp21hWW5gxWvc6b*4+dn<{c?w9Rmf zIVZKsPl{W2vQAlYO3yh}-{Os=YBnL8?uN5(RqfQ=-1cOiUnJu>KcLA*tQK3FU`_bM zM^T28w;nAj5EdAXFi&Kk1Nnl2)D!M{@+D-}bIEe+Lc4{s;YJc-{F#``iS2uk;2!Zp zF9#myUmO!wCeJIoi^A+T^e~20c+c2C}XltaR!|U-HfDA=^xF97ev}$l6#oY z&-&T{egB)&aV$3_aVA51XGiU07$s9vubh_kQG?F$FycvS6|IO!6q zq^>9|3U^*!X_C~SxX&pqUkUjz%!j=VlXDo$!2VLH!rKj@61mDpSr~7B2yy{>X~_nc zRI+7g2V&k zd**H++P9dg!-AOs3;GM`(g<+GRV$+&DdMVpUxY9I1@uK28$az=6oaa+PutlO9?6#? zf-OsgT>^@8KK>ggkUQRPPgC7zjKFR5spqQb3ojCHzj^(UH~v+!y*`Smv)VpVoPwa6 zWG18WJaPKMi*F6Zdk*kU^`i~NNTfn3BkJniC`yN98L-Awd)Z&mY? zprBW$!qL-OL7h@O#kvYnLsfff@kDIegt~?{-*5A7JrA;#TmTe?jICJqhub-G@e??D zqiV#g{)M!kW1-4SDel7TO{;@*h2=_76g3NUD@|c*WO#>MfYq6_YVUP+&8e4|%4T`w zXzhmVNziAHazWO2qXcaOu@R1MrPP{t)`N)}-1&~mq=ZH=w=;-E$IOk=y$dOls{6sRR`I5>|X zpq~XYW4sd;J^6OwOf**J>a7u$S>WTFPRkjY;BfVgQst)u4aMLR1|6%)CB^18XCz+r ztkYQ}G43j~Q&1em(_EkMv0|WEiKu;z2zhb(L%$F&xWwzOmk;VLBYAZ8lOCziNoPw1 zv2BOyXA`A8z^WH!nXhKXM`t0;6D*-uGds3TYGrm8SPnJJOQ^fJU#}@aIy@MYWz**H zvkp?7I5PE{$$|~{-ZaFxr6ZolP^nL##mHOErB^AqJqn^hFA=)HWj!m3WDaHW$C)i^ z9@6G$SzB=>jbe>4kqr#sF7#K}W*Cg-5y6kun3u&0L7BpXF9=#7IN8FOjWrWwUBZiU zT_se3ih-GBKx+Uw0N|CwP3D@-C=5(9T#BH@M`F2!Goiqx+Js5xC92|Sy0%WWWp={$(am!#l~f^W_oz78HX<0X#7 zp)p1u~M*o9W@O8P{0Qkg@Wa# z2{Heb&oX^CQSZWSFBXKOfE|tsAm#^U-WkDnU;IowZ`Ok4!mwHwH=s|AqZ^YD4!5!@ zPxJj+Bd-q6w_YG`z_+r;S86zwXb+EO&qogOq8h-Ect5(M2+>(O7n7)^dP*ws_3U6v zVsh)sk^@*c>)3EML|0<-YROho{lz@Nd4;R9gL{9|64xVL`n!m$-Jjrx?-Bacp!=^5 z1^T^eB{_)Y<9)y{-4Rz@9_>;_7h;5D+@QcbF4Wv7hu)s0&==&6u)33 zHRj+&Woq-vDvjwJCYES@$C4{$?f$Ibi4G()UeN11rgjF+^;YE^5nYprYoJNoudNj= zm1pXSeG64dcWHObUetodRn1Fw|1nI$D9z}dVEYT0lQnsf_E1x2vBLql7NrHH!n&Sq z6lc*mvU=WS6=v9Lrl}&zRiu_6u;6g%_DU{9b+R z#YHqX7`m9eydf?KlKu6Sb%j$%_jmydig`B*TN`cZL-g!R)iE?+Q5oOqBFKhx z%MW>BC^(F_JuG(ayE(MT{S3eI{cKiwOtPwLc0XO*{*|(JOx;uQOfq@lp_^cZo=FZj z4#}@e@dJ>Bn%2`2_WPeSN7si^{U#H=7N4o%Dq3NdGybrZgEU$oSm$hC)uNDC_M9xc zGzwh5Sg?mpBIE8lT2XsqTt3j3?We8}3bzLBTQd639vyg^$0#1epq8snlDJP2(BF)K zSx30RM+{f+b$g{9usIL8H!hCO117Xgv}ttPJm9wVRjPk;ePH@zxv%j9k5`TzdXLeT zFgFX`V7cYIcBls5WN0Pf6SMBN+;CrQ(|EsFd*xtwr#$R{Z9FP`OWtyNsq#mCgZ7+P z^Yn$haBJ)r96{ZJd8vlMl?IBxrgh=fdq_NF!1{jARCVz>jNdC)H^wfy?R94#MPdUjcYX>#wEx+LB#P-#4S-%YH>t-j+w zOFTI8gX$ard6fAh&g=u&56%3^-6E2tpk*wx3HSCQ+t7+*iOs zPk5ysqE}i*cQocFvA68xHfL|iX(C4h*67@3|5Qwle(8wT&!&{8*{f%0(5gH+m>$tq zp;AqrP7?XTEooYG1Dzfxc>W%*CyL16q|fQ0_jp%%Bk^k!i#Nbi(N9&T>#M{gez_Ws zYK=l}adalV(nH}I_!hNeb;tQFk3BHX7N}}R8%pek^E`X}%ou=cx8InPU1EE0|Hen- zyw8MoJqB5=)Z%JXlrdTXAE)eqLAdVE-=>wGHrkRet}>3Yu^lt$Kzu%$3#(ioY}@Gu zjk3BZuQH&~7H+C*uX^4}F*|P89JX;Hg2U!pt>rDi(n(Qe-c}tzb0#6_ItoR0->LSt zR~UT<-|@TO%O`M+_e_J4wx7^)5_%%u+J=yF_S#2Xd?C;Ss3N7KY^#-vx+|;bJX&8r zD?|MetfhdC;^2WG`7MCgs>TKKN=^=!x&Q~BzmQio_^l~LboTNT=I zC5pme^P@ER``p$2md9>4!K#vV-Fc1an7pl>_|&>aqP}+zqR?+~Z;f2^`a+-!Te%V? z;H2SbF>jP^GE(R1@%C==XQ@J=G9lKX+Z<@5}PO(EYkJh=GCv#)Nj{DkWJM2}F&oAZ6xu8&g7pn1ps2U5srwQ7CAK zN&*~@t{`31lUf`O;2w^)M3B@o)_mbRu{-`PrfNpF!R^q>yTR&ETS7^-b2*{-tZAZz zw@q5x9B5V8Qd7dZ!Ai$9hk%Q!wqbE1F1c96&zwBBaRW}(^axoPpN^4Aw}&a5dMe+*Gomky_l^54*rzXro$ z>LL)U5Ry>~FJi=*{JDc)_**c)-&faPz`6v`YU3HQa}pLtb5K)u%K+BOqXP0)rj5Au$zB zW1?vr?mDv7Fsxtsr+S6ucp2l#(4dnr9sD*v+@*>g#M4b|U?~s93>Pg{{a5|rm2xfI z`>E}?9S@|IoUX{Q1zjm5YJT|3S>&09D}|2~BiMo=z4YEjXlWh)V&qs;*C{`UMxp$9 zX)QB?G$fPD6z5_pNs>Jeh{^&U^)Wbr?2D6-q?)`*1k@!UvwQgl8eG$r+)NnFoT)L6 zg7lEh+E6J17krfYJCSjWzm67hEth24pomhz71|Qodn#oAILN)*Vwu2qpJirG)4Wnv}9GWOFrQg%Je+gNrPl8mw7ykE8{ z=|B4+uwC&bpp%eFcRU6{mxRV32VeH8XxX>v$du<$(DfinaaWxP<+Y97Z#n#U~V zVEu-GoPD=9$}P;xv+S~Ob#mmi$JQmE;Iz4(){y*9pFyW-jjgdk#oG$fl4o9E8bo|L zWjo4l%n51@Kz-n%zeSCD`uB?T%FVk+KBI}=ve zvlcS#wt`U6wrJo}6I6Rwb=1GzZfwE=I&Ne@p7*pH84XShXYJRgvK)UjQL%R9Zbm(m zxzTQsLTON$WO7vM)*vl%Pc0JH7WhP;$z@j=y#avW4X8iqy6mEYr@-}PW?H)xfP6fQ z&tI$F{NNct4rRMSHhaelo<5kTYq+(?pY)Ieh8*sa83EQfMrFupMM@nfEV@EmdHUv9 z35uzIrIuo4#WnF^_jcpC@uNNaYTQ~uZWOE6P@LFT^1@$o&q+9Qr8YR+ObBkpP9=F+$s5+B!mX2~T zAuQ6RenX?O{IlLMl1%)OK{S7oL}X%;!XUxU~xJN8xk z`xywS*naF(J#?vOpB(K=o~lE;m$zhgPWDB@=p#dQIW>xe_p1OLoWInJRKbEuoncf; zmS1!u-ycc1qWnDg5Nk2D)BY%jmOwCLC+Ny>`f&UxFowIsHnOXfR^S;&F(KXd{ODlm z$6#1ccqt-HIH9)|@fHnrKudu!6B$_R{fbCIkSIb#aUN|3RM>zuO>dpMbROZ`^hvS@ z$FU-;e4W}!ubzKrU@R*dW*($tFZ>}dd*4_mv)#O>X{U@zSzQt*83l9mI zI$8O<5AIDx`wo0}f2fsPC_l>ONx_`E7kdXu{YIZbp1$(^oBAH({T~&oQ&1{X951QW zmhHUxd)t%GQ9#ak5fTjk-cahWC;>^Rg7(`TVlvy0W@Y!Jc%QL3Ozu# zDPIqBCy&T2PWBj+d-JA-pxZlM=9ja2ce|3B(^VCF+a*MMp`(rH>Rt6W1$;r{n1(VK zLs>UtkT43LR2G$AOYHVailiqk7naz2yZGLo*xQs!T9VN5Q>eE(w zw$4&)&6xIV$IO^>1N-jrEUg>O8G4^@y+-hQv6@OmF@gy^nL_n1P1-Rtyy$Bl;|VcV zF=p*&41-qI5gG9UhKmmnjs932!6hceXa#-qfK;3d*a{)BrwNFeKU|ge?N!;zk+kB! zMD_uHJR#%b54c2tr~uGPLTRLg$`fupo}cRJeTwK;~}A>(Acy4k-Xk&Aa1&eWYS1ULWUj@fhBiWY$pdfy+F z@G{OG{*v*mYtH3OdUjwEr6%_ZPZ3P{@rfbNPQG!BZ7lRyC^xlMpWH`@YRar`tr}d> z#wz87t?#2FsH-jM6m{U=gp6WPrZ%*w0bFm(T#7m#v^;f%Z!kCeB5oiF`W33W5Srdt zdU?YeOdPG@98H7NpI{(uN{FJdu14r(URPH^F6tOpXuhU7T9a{3G3_#Ldfx_nT(Hec zo<1dyhsVsTw;ZkVcJ_0-h-T3G1W@q)_Q30LNv)W?FbMH+XJ* zy=$@39Op|kZv`Rt>X`zg&at(?PO^I=X8d9&myFEx#S`dYTg1W+iE?vt#b47QwoHI9 zNP+|3WjtXo{u}VG(lLUaW0&@yD|O?4TS4dfJI`HC-^q;M(b3r2;7|FONXphw-%7~* z&;2!X17|05+kZOpQ3~3!Nb>O94b&ZSs%p)TK)n3m=4eiblVtSx@KNFgBY_xV6ts;NF;GcGxMP8OKV^h6LmSb2E#Qnw ze!6Mnz7>lE9u{AgQ~8u2zM8CYD5US8dMDX-5iMlgpE9m*s+Lh~A#P1er*rF}GHV3h z=`STo?kIXw8I<`W0^*@mB1$}pj60R{aJ7>C2m=oghKyxMbFNq#EVLgP0cH3q7H z%0?L93-z6|+jiN|@v>ix?tRBU(v-4RV`}cQH*fp|)vd3)8i9hJ3hkuh^8dz{F5-~_ zUUr1T3cP%cCaTooM8dj|4*M=e6flH0&8ve32Q)0dyisl))XkZ7Wg~N}6y`+Qi2l+e zUd#F!nJp{#KIjbQdI`%oZ`?h=5G^kZ_uN`<(`3;a!~EMsWV|j-o>c?x#;zR2ktiB! z);5rrHl?GPtr6-o!tYd|uK;Vbsp4P{v_4??=^a>>U4_aUXPWQ$FPLE4PK$T^3Gkf$ zHo&9$U&G`d(Os6xt1r?sg14n)G8HNyWa^q8#nf0lbr4A-Fi;q6t-`pAx1T*$eKM*$ z|CX|gDrk#&1}>5H+`EjV$9Bm)Njw&7-ZR{1!CJTaXuP!$Pcg69`{w5BRHysB$(tWUes@@6aM69kb|Lx$%BRY^-o6bjH#0!7b;5~{6J+jKxU!Kmi# zndh@+?}WKSRY2gZ?Q`{(Uj|kb1%VWmRryOH0T)f3cKtG4oIF=F7RaRnH0Rc_&372={_3lRNsr95%ZO{IX{p@YJ^EI%+gvvKes5cY+PE@unghjdY5#9A!G z70u6}?zmd?v+{`vCu-53_v5@z)X{oPC@P)iA3jK$`r zSA2a7&!^zmUiZ82R2=1cumBQwOJUPz5Ay`RLfY(EiwKkrx%@YN^^XuET;tE zmr-6~I7j!R!KrHu5CWGSChO6deaLWa*9LLJbcAJsFd%Dy>a!>J`N)Z&oiU4OEP-!Ti^_!p}O?7`}i7Lsf$-gBkuY*`Zb z7=!nTT;5z$_5$=J=Ko+Cp|Q0J=%oFr>hBgnL3!tvFoLNhf#D0O=X^h+x08iB;@8pXdRHxX}6R4k@i6%vmsQwu^5z zk1ip`#^N)^#Lg#HOW3sPI33xqFB4#bOPVnY%d6prwxf;Y-w9{ky4{O6&94Ra8VN@K zb-lY;&`HtxW@sF!doT5T$2&lIvJpbKGMuDAFM#!QPXW87>}=Q4J3JeXlwHys?!1^#37q_k?N@+u&Ns20pEoBeZC*np;i;M{2C0Z4_br2gsh6eL z#8`#sn41+$iD?^GL%5?cbRcaa-Nx0vE(D=*WY%rXy3B%gNz0l?#noGJGP728RMY#q z=2&aJf@DcR?QbMmN)ItUe+VM_U!ryqA@1VVt$^*xYt~-qvW!J4Tp<-3>jT=7Zow5M z8mSKp0v4b%a8bxFr>3MwZHSWD73D@+$5?nZAqGM#>H@`)mIeC#->B)P8T$zh-Pxnc z8)~Zx?TWF4(YfKuF3WN_ckpCe5;x4V4AA3(i$pm|78{%!q?|~*eH0f=?j6i)n~Hso zmTo>vqEtB)`%hP55INf7HM@taH)v`Fw40Ayc*R!T?O{ziUpYmP)AH`euTK!zg9*6Z z!>M=$3pd0!&TzU=hc_@@^Yd3eUQpX4-33}b{?~5t5lgW=ldJ@dUAH%`l5US1y_`40 zs(X`Qk}vvMDYYq+@Rm+~IyCX;iD~pMgq^KY)T*aBz@DYEB={PxA>)mI6tM*sx-DmGQHEaHwRrAmNjO!ZLHO4b;;5mf@zzlPhkP($JeZGE7 z?^XN}Gf_feGoG~BjUgVa*)O`>lX=$BSR2)uD<9 z>o^|nb1^oVDhQbfW>>!;8-7<}nL6L^V*4pB=>wwW+RXAeRvKED(n1;R`A6v$6gy0I(;Vf?!4;&sgn7F%LpM}6PQ?0%2Z@b{It<(G1CZ|>913E0nR2r^Pa*Bp z@tFGi*CQ~@Yc-?{cwu1 zsilf=k^+Qs>&WZG(3WDixisHpR>`+ihiRwkL(3T|=xsoNP*@XX3BU8hr57l3k;pni zI``=3Nl4xh4oDj<%>Q1zYXHr%Xg_xrK3Nq?vKX3|^Hb(Bj+lONTz>4yhU-UdXt2>j z<>S4NB&!iE+ao{0Tx^N*^|EZU;0kJkx@zh}S^P{ieQjGl468CbC`SWnwLRYYiStXm zOxt~Rb3D{dz=nHMcY)#r^kF8|q8KZHVb9FCX2m^X*(|L9FZg!5a7((!J8%MjT$#Fs)M1Pb zq6hBGp%O1A+&%2>l0mpaIzbo&jc^!oN^3zxap3V2dNj3x<=TwZ&0eKX5PIso9j1;e zwUg+C&}FJ`k(M|%%}p=6RPUq4sT3-Y;k-<68ciZ~_j|bt>&9ZLHNVrp#+pk}XvM{8 z`?k}o-!if>hVlCP9j%&WI2V`5SW)BCeR5>MQhF)po=p~AYN%cNa_BbV6EEh_kk^@a zD>4&>uCGCUmyA-c)%DIcF4R6!>?6T~Mj_m{Hpq`*(wj>foHL;;%;?(((YOxGt)Bhx zuS+K{{CUsaC++%}S6~CJ=|vr(iIs-je)e9uJEU8ZJAz)w166q)R^2XI?@E2vUQ!R% zn@dxS!JcOimXkWJBz8Y?2JKQr>`~SmE2F2SL38$SyR1^yqj8_mkBp)o$@+3BQ~Mid z9U$XVqxX3P=XCKj0*W>}L0~Em`(vG<>srF8+*kPrw z20{z(=^w+ybdGe~Oo_i|hYJ@kZl*(9sHw#Chi&OIc?w`nBODp?ia$uF%Hs(X>xm?j zqZQ`Ybf@g#wli`!-al~3GWiE$K+LCe=Ndi!#CVjzUZ z!sD2O*;d28zkl))m)YN7HDi^z5IuNo3^w(zy8 zszJG#mp#Cj)Q@E@r-=NP2FVxxEAeOI2e=|KshybNB6HgE^(r>HD{*}S}mO>LuRGJT{*tfTzw_#+er-0${}%YPe@CMJ1Ng#j#)i)SnY@ss3gL;g zg2D~#Kpdfu#G;q1qz_TwSz1VJT(b3zby$Vk&;Y#1(A)|xj`_?i5YQ;TR%jice5E;0 zYHg;`zS5{S*9xI6o^j>rE8Ua*XhIw{_-*&@(R|C(am8__>+Ws&Q^ymy*X4~hR2b5r zm^p3sw}yv=tdyncy_Ui7{BQS732et~Z_@{-IhHDXAV`(Wlay<#hb>%H%WDi+K$862nA@BDtM#UCKMu+kM`!JHyWSi?&)A7_ z3{cyNG%a~nnH_!+;g&JxEMAmh-Z}rC!o7>OVzW&PoMyTA_g{hqXG)SLraA^OP**<7 zjWbr7z!o2n3hnx7A=2O=WL;`@9N{vQIM@&|G-ljrPvIuJHYtss0Er0fT5cMXNUf1B z7FAwBDixt0X7C3S)mPe5g`YtME23wAnbU)+AtV}z+e8G;0BP=bI;?(#|Ep!vVfDbK zvx+|CKF>yt0hWQ3drchU#XBU+HiuG*V^snFAPUp-5<#R&BUAzoB!aZ+e*KIxa26V}s6?nBK(U-7REa573wg-jqCg>H8~>O{ z*C0JL-?X-k_y%hpUFL?I>0WV{oV`Nb)nZbJG01R~AG>flIJf)3O*oB2i8~;!P?Wo_ z0|QEB*fifiL6E6%>tlAYHm2cjTFE@*<);#>689Z6S#BySQ@VTMhf9vYQyLeDg1*F} zjq>i1*x>5|CGKN{l9br3kB0EHY|k4{%^t7-uhjd#NVipUZa=EUuE5kS1_~qYX?>hJ z$}!jc9$O$>J&wnu0SgfYods^z?J4X;X7c77Me0kS-dO_VUQ39T(Kv(Y#s}Qqz-0AH z^?WRL(4RzpkD+T5FG_0NyPq-a-B7A5LHOCqwObRJi&oRi(<;OuIN7SV5PeHU$<@Zh zPozEV`dYmu0Z&Tqd>t>8JVde9#Pt+l95iHe$4Xwfy1AhI zDM4XJ;bBTTvRFtW>E+GzkN)9k!hA5z;xUOL2 zq4}zn-DP{qc^i|Y%rvi|^5k-*8;JZ~9a;>-+q_EOX+p1Wz;>i7c}M6Nv`^NY&{J-> z`(mzDJDM}QPu5i44**2Qbo(XzZ-ZDu%6vm8w@DUarqXj41VqP~ zs&4Y8F^Waik3y1fQo`bVUH;b=!^QrWb)3Gl=QVKr+6sxc=ygauUG|cm?|X=;Q)kQ8 zM(xrICifa2p``I7>g2R~?a{hmw@{!NS5`VhH8+;cV(F>B94M*S;5#O`YzZH1Z%yD? zZ61w(M`#aS-*~Fj;x|J!KM|^o;MI#Xkh0ULJcA?o4u~f%Z^16ViA27FxU5GM*rKq( z7cS~MrZ=f>_OWx8j#-Q3%!aEU2hVuTu(7`TQk-Bi6*!<}0WQi;_FpO;fhpL4`DcWp zGOw9vx0N~6#}lz(r+dxIGZM3ah-8qrqMmeRh%{z@dbUD2w15*_4P?I~UZr^anP}DB zU9CCrNiy9I3~d#&!$DX9e?A});BjBtQ7oGAyoI$8YQrkLBIH@2;lt4E^)|d6Jwj}z z&2_E}Y;H#6I4<10d_&P0{4|EUacwFHauvrjAnAm6yeR#}f}Rk27CN)vhgRqEyPMMS7zvunj2?`f;%?alsJ+-K+IzjJx>h8 zu~m_y$!J5RWAh|C<6+uiCNsOKu)E72M3xKK(a9Okw3e_*O&}7llNV!=P87VM2DkAk zci!YXS2&=P0}Hx|wwSc9JP%m8dMJA*q&VFB0yMI@5vWoAGraygwn){R+Cj6B1a2Px z5)u(K5{+;z2n*_XD!+Auv#LJEM)(~Hx{$Yb^ldQmcYF2zNH1V30*)CN_|1$v2|`LnFUT$%-tO0Eg|c5$BB~yDfzS zcOXJ$wpzVK0MfTjBJ0b$r#_OvAJ3WRt+YOLlJPYMx~qp>^$$$h#bc|`g0pF-Ao43? z>*A+8lx>}L{p(Tni2Vvk)dtzg$hUKjSjXRagj)$h#8=KV>5s)J4vGtRn5kP|AXIz! zPgbbVxW{2o4s-UM;c#We8P&mPN|DW7_uLF!a|^0S=wr6Esx9Z$2|c1?GaupU6$tb| zY_KU`(_29O_%k(;>^|6*pZURH3`@%EuKS;Ns z1lujmf;r{qAN&Q0&m{wJSZ8MeE7RM5+Sq;ul_ z`+ADrd_Um+G37js6tKsArNB}n{p*zTUxQr>3@wA;{EUbjNjlNd6$Mx zg0|MyU)v`sa~tEY5$en7^PkC=S<2@!nEdG6L=h(vT__0F=S8Y&eM=hal#7eM(o^Lu z2?^;05&|CNliYrq6gUv;|i!(W{0N)LWd*@{2q*u)}u*> z7MQgk6t9OqqXMln?zoMAJcc zMKaof_Up})q#DzdF?w^%tTI7STI^@8=Wk#enR*)&%8yje>+tKvUYbW8UAPg55xb70 zEn5&Ba~NmOJlgI#iS8W3-@N%>V!#z-ZRwfPO1)dQdQkaHsiqG|~we2ALqG7Ruup(DqSOft2RFg_X%3w?6VqvV1uzX_@F(diNVp z4{I|}35=11u$;?|JFBEE*gb;T`dy+8gWJ9~pNsecrO`t#V9jW-6mnfO@ff9od}b(3s4>p0i30gbGIv~1@a^F2kl7YO;DxmF3? zWi-RoXhzRJV0&XE@ACc?+@6?)LQ2XNm4KfalMtsc%4!Fn0rl zpHTrHwR>t>7W?t!Yc{*-^xN%9P0cs0kr=`?bQ5T*oOo&VRRu+1chM!qj%2I!@+1XF z4GWJ=7ix9;Wa@xoZ0RP`NCWw0*8247Y4jIZ>GEW7zuoCFXl6xIvz$ezsWgKdVMBH> z{o!A7f;R-@eK9Vj7R40xx)T<2$?F2E<>Jy3F;;=Yt}WE59J!1WN367 zA^6pu_zLoZIf*x031CcwotS{L8bJE(<_F%j_KJ2P_IusaZXwN$&^t716W{M6X2r_~ zaiMwdISX7Y&Qi&Uh0upS3TyEIXNDICQlT5fHXC`aji-c{U(J@qh-mWl-uMN|T&435 z5)a1dvB|oe%b2mefc=Vpm0C%IUYYh7HI*;3UdgNIz}R##(#{(_>82|zB0L*1i4B5j-xi9O4x10rs_J6*gdRBX=@VJ+==sWb&_Qc6tSOowM{BX@(zawtjl zdU!F4OYw2@Tk1L^%~JCwb|e#3CC>srRHQ*(N%!7$Mu_sKh@|*XtR>)BmWw!;8-mq7 zBBnbjwx8Kyv|hd*`5}84flTHR1Y@@uqjG`UG+jN_YK&RYTt7DVwfEDXDW4U+iO{>K zw1hr{_XE*S*K9TzzUlJH2rh^hUm2v7_XjwTuYap|>zeEDY$HOq3X4Tz^X}E9z)x4F zs+T?Ed+Hj<#jY-`Va~fT2C$=qFT-5q$@p9~0{G&eeL~tiIAHXA!f6C(rAlS^)&k<- zXU|ZVs}XQ>s5iONo~t!XXZgtaP$Iau;JT%h)>}v54yut~pykaNye4axEK#5@?TSsQ zE;Jvf9I$GVb|S`7$pG)4vgo9NXsKr?u=F!GnA%VS2z$@Z(!MR9?EPcAqi5ft)Iz6sNl`%kj+_H-X`R<>BFrBW=fSlD|{`D%@Rcbu2?%>t7i34k?Ujb)2@J-`j#4 zLK<69qcUuniIan-$A1+fR=?@+thwDIXtF1Tks@Br-xY zfB+zblrR(ke`U;6U~-;p1Kg8Lh6v~LjW@9l2P6s+?$2!ZRPX`(ZkRGe7~q(4&gEi<$ch`5kQ?*1=GSqkeV z{SA1EaW_A!t{@^UY2D^YO0(H@+kFVzZaAh0_`A`f(}G~EP~?B|%gtxu&g%^x{EYSz zk+T;_c@d;+n@$<>V%P=nk36?L!}?*=vK4>nJSm+1%a}9UlmTJTrfX4{Lb7smNQn@T zw9p2%(Zjl^bWGo1;DuMHN(djsEm)P8mEC2sL@KyPjwD@d%QnZ$ zMJ3cnn!_!iP{MzWk%PI&D?m?C(y2d|2VChluN^yHya(b`h>~GkI1y;}O_E57zOs!{ zt2C@M$^PR2U#(dZmA-sNreB@z-yb0Bf7j*yONhZG=onhx>t4)RB`r6&TP$n zgmN*)eCqvgriBO-abHQ8ECN0bw?z5Bxpx z=jF@?zFdVn?@gD5egM4o$m`}lV(CWrOKKq(sv*`mNcHcvw&Xryfw<{ch{O&qc#WCTXX6=#{MV@q#iHYba!OUY+MGeNTjP%Fj!WgM&`&RlI^=AWTOqy-o zHo9YFt!gQ*p7{Fl86>#-JLZo(b^O`LdFK~OsZBRR@6P?ad^Ujbqm_j^XycM4ZHFyg ziUbIFW#2tj`65~#2V!4z7DM8Z;fG0|APaQ{a2VNYpNotB7eZ5kp+tPDz&Lqs0j%Y4tA*URpcfi z_M(FD=fRGdqf430j}1z`O0I=;tLu81bwJXdYiN7_&a-?ly|-j*+=--XGvCq#32Gh(=|qj5F?kmihk{%M&$}udW5)DHK zF_>}5R8&&API}o0osZJRL3n~>76nUZ&L&iy^s>PMnNcYZ|9*1$v-bzbT3rpWsJ+y{ zPrg>5Zlery96Um?lc6L|)}&{992{_$J&=4%nRp9BAC6!IB=A&=tF>r8S*O-=!G(_( zwXbX_rGZgeiK*&n5E;f=k{ktyA1(;x_kiMEt0*gpp_4&(twlS2e5C?NoD{n>X2AT# zY@Zp?#!b1zNq96MQqeO*M1MMBin5v#RH52&Xd~DO6-BZLnA6xO1$sou(YJ1Dlc{WF zVa%2DyYm`V#81jP@70IJ;DX@y*iUt$MLm)ByAD$eUuji|5{ptFYq(q)mE(5bOpxjM z^Q`AHWq44SG3`_LxC9fwR)XRVIp=B%<(-lOC3jI#bb@dK(*vjom!=t|#<@dZql%>O z15y^{4tQoeW9Lu%G&V$90x6F)xN6y_oIn;!Q zs)8jT$;&;u%Y>=T3hg34A-+Y*na=|glcStr5D;&5*t5*DmD~x;zQAV5{}Ya`?RRGa zT*t9@$a~!co;pD^!J5bo?lDOWFx%)Y=-fJ+PDGc0>;=q=s?P4aHForSB+)v0WY2JH z?*`O;RHum6j%#LG)Vu#ciO#+jRC3!>T(9fr+XE7T2B7Z|0nR5jw@WG)kDDzTJ=o4~ zUpeyt7}_nd`t}j9BKqryOha{34erm)RmST)_9Aw)@ zHbiyg5n&E{_CQR@h<}34d7WM{s{%5wdty1l+KX8*?+-YkNK2Be*6&jc>@{Fd;Ps|| z26LqdI3#9le?;}risDq$K5G3yoqK}C^@-8z^wj%tdgw-6@F#Ju{Sg7+y)L?)U$ez> zoOaP$UFZ?y5BiFycir*pnaAaY+|%1%8&|(@VB)zweR%?IidwJyK5J!STzw&2RFx zZV@qeaCB01Hu#U9|1#=Msc8Pgz5P*4Lrp!Q+~(G!OiNR{qa7|r^H?FC6gVhkk3y7=uW#Sh;&>78bZ}aK*C#NH$9rX@M3f{nckYI+5QG?Aj1DM)@~z_ zw!UAD@gedTlePB*%4+55naJ8ak_;))#S;4ji!LOqY5VRI){GMwHR~}6t4g>5C_#U# ztYC!tjKjrKvRy=GAsJVK++~$|+s!w9z3H4G^mACv=EErXNSmH7qN}%PKcN|8%9=i)qS5+$L zu&ya~HW%RMVJi4T^pv?>mw*Gf<)-7gf#Qj|e#w2|v4#t!%Jk{&xlf;$_?jW*n!Pyx zkG$<18kiLOAUPuFfyu-EfWX%4jYnjBYc~~*9JEz6oa)_R|8wjZA|RNrAp%}14L7fW zi7A5Wym*K+V8pkqqO-X#3ft{0qs?KVt^)?kS>AicmeO&q+~J~ zp0YJ_P~_a8j= zsAs~G=8F=M{4GZL{|B__UorX@MRNQLn?*_gym4aW(~+i13knnk1P=khoC-ViMZk+x zLW(l}oAg1H`dU+Fv**;qw|ANDSRs>cGqL!Yw^`; zv;{E&8CNJcc)GHzTYM}f&NPw<6j{C3gaeelU#y!M)w-utYEHOCCJo|Vgp7K6C_$14 zqIrLUB0bsgz^D%V%fbo2f9#yb#CntTX?55Xy|Kps&Xek*4_r=KDZ z+`TQuv|$l}MWLzA5Ay6Cvsa^7xvwXpy?`w(6vx4XJ zWuf1bVSb#U8{xlY4+wlZ$9jjPk)X_;NFMqdgq>m&W=!KtP+6NL57`AMljW+es zzqjUjgz;V*kktJI?!NOg^s_)ph45>4UDA!Vo0hn>KZ+h-3=?Y3*R=#!fOX zP$Y~+14$f66ix?UWB_6r#fMcC^~X4R-<&OD1CSDNuX~y^YwJ>sW0j`T<2+3F9>cLo z#!j57$ll2K9(%$4>eA7(>FJX5e)pR5&EZK!IMQzOfik#FU*o*LGz~7u(8}XzIQRy- z!U7AlMTIe|DgQFmc%cHy_9^{o`eD%ja_L>ckU6$O4*U**o5uR7`FzqkU8k4gxtI=o z^P^oGFPm5jwZMI{;nH}$?p@uV8FT4r=|#GziKXK07bHJLtK}X%I0TON$uj(iJ`SY^ zc$b2CoxCQ>7LH@nxcdW&_C#fMYBtTxcg46dL{vf%EFCZ~eErMvZq&Z%Lhumnkn^4A zsx$ay(FnN7kYah}tZ@0?-0Niroa~13`?hVi6`ndno`G+E8;$<6^gsE-K3)TxyoJ4M zb6pj5=I8^FD5H@`^V#Qb2^0cx7wUz&cruA5g>6>qR5)O^t1(-qqP&1g=qvY#s&{bx zq8Hc%LsbK1*%n|Y=FfojpE;w~)G0-X4i*K3{o|J7`krhIOd*c*$y{WIKz2n2*EXEH zT{oml3Th5k*vkswuFXdGDlcLj15Nec5pFfZ*0?XHaF_lVuiB%Pv&p7z)%38}%$Gup zVTa~C8=cw%6BKn_|4E?bPNW4PT7}jZQLhDJhvf4z;~L)506IE0 zX!tWXX(QOQPRj-p80QG79t8T2^az4Zp2hOHziQlvT!|H)jv{Ixodabzv6lBj)6WRB z{)Kg@$~~(7$-az?lw$4@L%I&DI0Lo)PEJJziWP33a3azb?jyXt1v0N>2kxwA6b%l> zZqRpAo)Npi&loWbjFWtEV)783BbeIAhqyuc+~>i7aQ8shIXt)bjCWT6$~ro^>99G} z2XfmT0(|l!)XJb^E!#3z4oEGIsL(xd; zYX1`1I(cG|u#4R4T&C|m*9KB1`UzKvho5R@1eYtUL9B72{i(ir&ls8g!pD ztR|25xGaF!4z5M+U@@lQf(12?xGy`!|3E}7pI$k`jOIFjiDr{tqf0va&3pOn6Pu)% z@xtG2zjYuJXrV)DUrIF*y<1O1<$#54kZ#2;=X51J^F#0nZ0(;S$OZDt_U2bx{RZ=Q zMMdd$fH|!s{ zXq#l;{`xfV`gp&C>A`WrQU?d{!Ey5(1u*VLJt>i27aZ-^&2IIk=zP5p+{$q(K?2(b z8?9h)kvj9SF!Dr zoyF}?V|9;6abHxWk2cEvGs$-}Pg}D+ZzgkaN&$Snp%;5m%zh1E#?Wac-}x?BYlGN#U#Mek*}kek#I9XaHt?mz3*fDrRTQ#&#~xyeqJk1QJ~E$7qsw6 z?sV;|?*=-{M<1+hXoj?@-$y+(^BJ1H~wQ9G8C0#^aEAyhDduNX@haoa=PuPp zYsGv8UBfQaRHgBgLjmP^eh>fLMeh{8ic)?xz?#3kX-D#Z{;W#cd_`9OMFIaJg-=t`_3*!YDgtNQ2+QUEAJB9M{~AvT$H`E)IKmCR21H532+ata8_i_MR@ z2Xj<3w<`isF~Ah$W{|9;51ub*f4#9ziKrOR&jM{x7I_7()O@`F*5o$KtZ?fxU~g`t zUovNEVKYn$U~VX8eR)qb`7;D8pn*Pp$(otYTqL)5KH$lUS-jf}PGBjy$weoceAcPp z&5ZYB$r&P$MN{0H0AxCe4Qmd3T%M*5d4i%#!nmBCN-WU-4m4Tjxn-%j3HagwTxCZ9 z)j5vO-C7%s%D!&UfO>bi2oXiCw<-w{vVTK^rVbv#W=WjdADJy8$khnU!`ZWCIU`># zyjc^1W~pcu>@lDZ{zr6gv%)2X4n27~Ve+cQqcND%0?IFSP4sH#yIaXXYAq^z3|cg` z`I3$m%jra>e2W-=DiD@84T!cb%||k)nPmEE09NC%@PS_OLhkrX*U!cgD*;;&gIaA(DyVT4QD+q_xu z>r`tg{hiGY&DvD-)B*h+YEd+Zn)WylQl}<4>(_NlsKXCRV;a)Rcw!wtelM2_rWX`j zTh5A|i6=2BA(iMCnj_fob@*eA;V?oa4Z1kRBGaU07O70fb6-qmA$Hg$ps@^ka1=RO zTbE_2#)1bndC3VuK@e!Sftxq4=Uux}fDxXE#Q5_x=E1h>T5`DPHz zbH<_OjWx$wy7=%0!mo*qH*7N4tySm+R0~(rbus`7;+wGh;C0O%x~fEMkt!eV>U$`i z5>Q(o z=t$gPjgGh0&I7KY#k50V7DJRX<%^X z>6+ebc9efB3@eE2Tr){;?_w`vhgF>`-GDY(YkR{9RH(MiCnyRtd!LxXJ75z+?2 zGi@m^+2hKJ5sB1@Xi@s_@p_Kwbc<*LQ_`mr^Y%j}(sV_$`J(?_FWP)4NW*BIL~sR>t6 zM;qTJZ~GoY36&{h-Pf}L#y2UtR}>ZaI%A6VkU>vG4~}9^i$5WP2Tj?Cc}5oQxe2=q z8BeLa$hwCg_psjZyC2+?yX4*hJ58Wu^w9}}7X*+i5Rjqu5^@GzXiw#SUir1G1`jY% zOL=GE_ENYxhcyUrEt9XlMNP6kx6h&%6^u3@zB8KUCAa18T(R2J`%JjWZ z!{7cXaEW+Qu*iJPu+m>QqW}Lo$4Z+!I)0JNzZ&_M%=|B1yejFRM04bGAvu{=lNPd+ zJRI^DRQ(?FcVUD+bgEcAi@o(msqys9RTCG#)TjI!9~3-dc`>gW;HSJuQvH~d`MQs86R$|SKXHh zqS9Qy)u;T`>>a!$LuaE2keJV%;8g)tr&Nnc;EkvA-RanHXsy)D@XN0a>h}z2j81R; zsUNJf&g&rKpuD0WD@=dDrPHdBoK42WoBU|nMo17o(5^;M|dB4?|FsAGVrSyWcI`+FVw^vTVC`y}f(BwJl zrw3Sp151^9=}B})6@H*i4-dIN_o^br+BkcLa^H56|^2XsT0dESw2 zMX>(KqNl=x2K5=zIKg}2JpGAZu{I_IO}0$EQ5P{4zol**PCt3F4`GX}2@vr8#Y)~J zKb)gJeHcFnR@4SSh%b;c%J`l=W*40UPjF#q{<}ywv-=vHRFmDjv)NtmC zQx9qm)d%0zH&qG7AFa3VAU1S^(n8VFTC~Hb+HjYMjX8r#&_0MzlNR*mnLH5hi}`@{ zK$8qiDDvS_(L9_2vHgzEQ${DYSE;DqB!g*jhJghE&=LTnbgl&Xepo<*uRtV{2wDHN z)l;Kg$TA>Y|K8Lc&LjWGj<+bp4Hiye_@BfU(y#nF{fpR&|Ltbye?e^j0}8JC4#xi% zv29ZR%8%hk=3ZDvO-@1u8KmQ@6p%E|dlHuy#H1&MiC<*$YdLkHmR#F3ae;bKd;@*i z2_VfELG=B}JMLCO-6UQy^>RDE%K4b>c%9ki`f~Z2Qu8hO7C#t%Aeg8E%+}6P7Twtg z-)dj(w}_zFK&86KR@q9MHicUAucLVshUdmz_2@32(V`y3`&Kf8Q2I)+!n0mR=rrDU zXvv^$ho;yh*kNqJ#r1}b0|i|xRUF6;lhx$M*uG3SNLUTC@|htC z-=fsw^F%$qqz4%QdjBrS+ov}Qv!z00E+JWas>p?z@=t!WWU3K*?Z(0meTuTOC7OTx zU|kFLE0bLZ+WGcL$u4E}5dB0g`h|uwv3=H6f+{5z9oLv-=Q45+n~V4WwgO=CabjM% zBAN+RjM65(-}>Q2V#i1Na@a0`08g&y;W#@sBiX6Tpy8r}*+{RnyGUT`?XeHSqo#|J z^ww~c;ou|iyzpErDtlVU=`8N7JSu>4M z_pr9=tX0edVn9B}YFO2y(88j#S{w%E8vVOpAboK*27a7e4Ekjt0)hIX99*1oE;vex z7#%jhY=bPijA=Ce@9rRO(Vl_vnd00!^TAc<+wVvRM9{;hP*rqEL_(RzfK$er_^SN; z)1a8vo8~Dr5?;0X0J62Cusw$A*c^Sx1)dom`-)Pl7hsW4i(r*^Mw`z5K>!2ixB_mu z*Ddqjh}zceRFdmuX1akM1$3>G=#~|y?eYv(e-`Qy?bRHIq=fMaN~fB zUa6I8Rt=)jnplP>yuS+P&PxeWpJ#1$F`iqRl|jF$WL_aZFZl@kLo&d$VJtu&w?Q0O zzuXK>6gmygq(yXJy0C1SL}T8AplK|AGNUOhzlGeK_oo|haD@)5PxF}rV+5`-w{Aag zus45t=FU*{LguJ11Sr-28EZkq;!mJO7AQGih1L4rEyUmp>B!%X0YemsrV3QFvlgt* z5kwlPzaiJ+kZ^PMd-RRbl(Y?F*m`4*UIhIuf#8q>H_M=fM*L_Op-<_r zBZagV=4B|EW+KTja?srADTZXCd3Yv%^Chfpi)cg{ED${SI>InNpRj5!euKv?=Xn92 zsS&FH(*w`qLIy$doc>RE&A5R?u zzkl1sxX|{*fLpXvIW>9d<$ePROttn3oc6R!sN{&Y+>Jr@yeQN$sFR z;w6A<2-0%UA?c8Qf;sX7>>uKRBv3Ni)E9pI{uVzX|6Bb0U)`lhLE3hK58ivfRs1}d zNjlGK0hdq0qjV@q1qI%ZFMLgcpWSY~mB^LK)4GZ^h_@H+3?dAe_a~k*;9P_d7%NEFP6+ zgV(oGr*?W(ql?6SQ~`lUsjLb%MbfC4V$)1E0Y_b|OIYxz4?O|!kRb?BGrgiH5+(>s zoqM}v*;OBfg-D1l`M6T6{K`LG+0dJ1)!??G5g(2*vlNkm%Q(MPABT$r13q?|+kL4- zf)Mi5r$sn;u41aK(K#!m+goyd$c!KPl~-&-({j#D4^7hQkV3W|&>l_b!}!z?4($OA z5IrkfuT#F&S1(`?modY&I40%gtroig{YMvF{K{>5u^I51k8RriGd${z)=5k2tG zM|&Bp5kDTfb#vfuTTd?)a=>bX=lokw^y9+2LS?kwHQIWI~pYgy7 zb?A-RKVm_vM5!9?C%qYdfRAw& zAU7`up~%g=p@}pg#b7E)BFYx3g%(J36Nw(Dij!b>cMl@CSNbrW!DBDbTD4OXk!G4x zi}JBKc8HBYx$J~31PXH+4^x|UxK~(<@I;^3pWN$E=sYma@JP|8YL`L(zI6Y#c%Q{6 z*APf`DU$S4pr#_!60BH$FGViP14iJmbrzSrOkR;f3YZa{#E7Wpd@^4E-zH8EgPc-# zKWFPvh%WbqU_%ZEt`=Q?odKHc7@SUmY{GK`?40VuL~o)bS|is$Hn=<=KGHOsEC5tB zFb|q}gGlL97NUf$G$>^1b^3E18PZ~Pm9kX%*ftnolljiEt@2#F2R5ah$zbXd%V_Ev zyDd{1o_uuoBga$fB@Fw!V5F3jIr=a-ykqrK?WWZ#a(bglI_-8pq74RK*KfQ z0~Dzus7_l;pMJYf>Bk`)`S8gF!To-BdMnVw5M-pyu+aCiC5dwNH|6fgRsIKZcF&)g zr}1|?VOp}I3)IR@m1&HX1~#wsS!4iYqES zK}4J{Ei>;e3>LB#Oly>EZkW14^@YmpbgxCDi#0RgdM${&wxR+LiX}B+iRioOB0(pDKpVEI;ND?wNx>%e|m{RsqR_{(nmQ z3ZS}@t!p4a(BKx_-CYwrcyJ5u1TO9bcXti$8sy>xcLKqKCc#~UOZYD{llKTSFEjJ~ zyNWt>tLU}*>^`TvPxtP%F`ZJQw@W0^>x;!^@?k_)9#bF$j0)S3;mH-IR5y82l|%=F z2lR8zhP?XNP-ucZZ6A+o$xOyF!w;RaLHGh57GZ|TCXhJqY~GCh)aXEV$1O&$c}La1 zjuJxkY9SM4av^Hb;i7efiYaMwI%jGy`3NdY)+mcJhF(3XEiSlU3c|jMBi|;m-c?~T z+x0_@;SxcoY=(6xNgO$bBt~Pj8`-<1S|;Bsjrzw3@zSjt^JC3X3*$HI79i~!$RmTz zsblZsLYs7L$|=1CB$8qS!tXrWs!F@BVuh?kN(PvE5Av-*r^iYu+L^j^m9JG^#=m>@ z=1soa)H*w6KzoR$B8mBCXoU;f5^bVuwQ3~2LKg!yxomG1#XPmn(?YH@E~_ED+W6mxs%x{%Z<$pW`~ON1~2XjP5v(0{C{+6Dm$00tsd3w=f=ZENy zOgb-=f}|Hb*LQ$YdWg<(u7x3`PKF)B7ZfZ6;1FrNM63 z?O6tE%EiU@6%rVuwIQjvGtOofZBGZT1Sh(xLIYt9c4VI8`!=UJd2BfLjdRI#SbVAX ziT(f*RI^T!IL5Ac>ql7uduF#nuCRJ1)2bdvAyMxp-5^Ww5p#X{rb5)(X|fEhDHHW{ zw(Lfc$g;+Q`B0AiPGtmK%*aWfQQ$d!*U<|-@n2HZvCWSiw^I>#vh+LyC;aaVWGbmkENr z&kl*8o^_FW$T?rDYLO1Pyi%>@&kJKQoH2E0F`HjcN}Zlnx1ddoDA>G4Xu_jyp6vuT zPvC}pT&Owx+qB`zUeR|4G;OH(<<^_bzkjln0k40t`PQxc$7h(T8Ya~X+9gDc8Z9{Z z&y0RAU}#_kQGrM;__MK9vwIwK^aoqFhk~dK!ARf1zJqHMxF2?7-8|~yoO@_~Ed;_wvT%Vs{9RK$6uUQ|&@#6vyBsFK9eZW1Ft#D2)VpQRwpR(;x^ zdoTgMqfF9iBl%{`QDv7B0~8{8`8k`C4@cbZAXBu00v#kYl!#_Wug{)2PwD5cNp?K^ z9+|d-4z|gZ!L{57>!Ogfbzchm>J1)Y%?NThxIS8frAw@z>Zb9v%3_3~F@<=LG%r*U zaTov}{{^z~SeX!qgSYow`_5)ij*QtGp4lvF`aIGQ>@3ZTkDmsl#@^5*NGjOuu82}o zzLF~Q9SW+mP=>88%eSA1W4_W7-Q>rdq^?t=m6}^tDPaBRGFLg%ak93W!kOp#EO{6& zP%}Iff5HZQ9VW$~+9r=|Quj#z*=YwcnssS~9|ub2>v|u1JXP47vZ1&L1O%Z1DsOrDfSIMHU{VT>&>H=9}G3i@2rP+rx@eU@uE8rJNec zij~#FmuEBj03F1~ct@C@$>y)zB+tVyjV3*n`mtAhIM0$58vM9jOQC}JJOem|EpwqeMuYPxu3sv}oMS?S#o6GGK@8PN59)m&K4Dc&X% z(;XL_kKeYkafzS3Wn5DD>Yiw{LACy_#jY4op(>9q>>-*9@C0M+=b#bknAWZ37^(Ij zq>H%<@>o4a#6NydoF{_M4i4zB_KG)#PSye9bk0Ou8h%1Dtl7Q_y#7*n%g)?m>xF~( zjqvOwC;*qvN_3(*a+w2|ao0D?@okOvg8JskUw(l7n`0fncglavwKd?~l_ryKJ^Ky! zKCHkIC-o7%fFvPa$)YNh022lakMar^dgL=t#@XLyNHHw!b?%WlM)R@^!)I!smZL@k zBi=6wE5)2v&!UNV(&)oOYW(6Qa!nUjDKKBf-~Da=#^HE4(@mWk)LPvhyN3i4goB$3K8iV7uh zsv+a?#c4&NWeK(3AH;ETrMOIFgu{_@%XRwCZ;L=^8Ts)hix4Pf3yJRQ<8xb^CkdmC z?c_gB)XmRsk`9ch#tx4*hO=#qS7={~Vb4*tTf<5P%*-XMfUUYkI9T1cEF;ObfxxI-yNuA=I$dCtz3ey znVkctYD*`fUuZ(57+^B*R=Q}~{1z#2!ca?)+YsRQb+lt^LmEvZt_`=j^wqig+wz@n@ z`LIMQJT3bxMzuKg8EGBU+Q-6cs5(@5W?N>JpZL{$9VF)veF`L5%DSYTNQEypW%6$u zm_~}T{HeHj1bAlKl8ii92l9~$dm=UM21kLemA&b$;^!wB7#IKWGnF$TVq!!lBlG4 z{?Rjz?P(uvid+|i$VH?`-C&Gcb3{(~Vpg`w+O);Wk1|Mrjxrht0GfRUnZqz2MhrXa zqgVC9nemD5)H$to=~hp)c=l9?#~Z_7i~=U-`FZxb-|TR9@YCxx;Zjo-WpMNOn2)z) zFPGGVl%3N$f`gp$gPnWC+f4(rmts%fidpo^BJx72zAd7|*Xi{2VXmbOm)1`w^tm9% znM=0Fg4bDxH5PxPEm{P3#A(mxqlM7SIARP?|2&+c7qmU8kP&iApzL|F>Dz)Ixp_`O zP%xrP1M6@oYhgo$ZWwrAsYLa4 z|I;DAvJxno9HkQrhLPQk-8}=De{9U3U%)dJ$955?_AOms!9gia%)0E$Mp}$+0er@< zq7J&_SzvShM?e%V?_zUu{niL@gt5UFOjFJUJ}L?$f%eU%jUSoujr{^O=?=^{19`ON zlRIy8Uo_nqcPa6@yyz`CM?pMJ^^SN^Fqtt`GQ8Q#W4kE7`V9^LT}j#pMChl!j#g#J zr-=CCaV%xyFeQ9SK+mG(cTwW*)xa(eK;_Z(jy)woZp~> zA(4}-&VH+TEeLzPTqw&FOoK(ZjD~m{KW05fiGLe@E3Z2`rLukIDahE*`u!ubU)9`o zn^-lyht#E#-dt~S>}4y$-mSbR8{T@}22cn^refuQ08NjLOv?JiEWjyOnzk<^R5%gO zhUH_B{oz~u#IYwVnUg8?3P*#DqD8#X;%q%HY**=I>>-S|!X*-!x1{^l#OnR56O>iD zc;i;KS+t$koh)E3)w0OjWJl_aW2;xF=9D9Kr>)(5}4FqUbk# zI#$N8o0w;IChL49m9CJTzoC!|u{Ljd%ECgBOf$}&jA^$(V#P#~)`&g`H8E{uv52pp zwto`xUL-L&WTAVREEm$0g_gYPL(^vHq(*t1WCH_6alhkeW&GCZ3hL)|{O-jiFOBrF z!EW=Jej|dqQitT6!B-7&io2K)WIm~Q)v@yq%U|VpV+I?{y0@Yd%n8~-NuuM*pM~KA z85YB};IS~M(c<}4Hxx>qRK0cdl&e?t253N%vefkgds>Ubn8X}j6Vpgs>a#nFq$osY z1ZRwLqFv=+BTb=i%D2Wv>_yE0z}+niZ4?rE|*a3d7^kndWGwnFqt+iZ(7+aln<}jzbAQ(#Z2SS}3S$%Bd}^ zc9ghB%O)Z_mTZMRC&H#)I#fiLuIkGa^`4e~9oM5zKPx?zjkC&Xy0~r{;S?FS%c7w< zWbMpzc(xSw?9tGxG~_l}Acq}zjt5ClaB7-!vzqnlrX;}$#+PyQ9oU)_DfePh2E1<7 ztok6g6K^k^DuHR*iJ?jw?bs_whk|bx`dxu^nC6#e{1*m~z1eq7m}Cf$*^Eua(oi_I zAL+3opNhJteu&mWQ@kQWPucmiP)4|nFG`b2tpC;h{-PI@`+h?9v=9mn|0R-n8#t=+Z*FD(c5 zjj79Jxkgck*DV=wpFgRZuwr%}KTm+dx?RT@aUHJdaX-ODh~gByS?WGx&czAkvkg;x zrf92l8$Or_zOwJVwh>5rB`Q5_5}ef6DjS*$x30nZbuO3dijS*wvNEqTY5p1_A0gWr znH<(Qvb!os14|R)n2Ost>jS2;d1zyLHu`Svm|&dZD+PpP{Bh>U&`Md;gRl64q;>{8MJJM$?UNUd`aC>BiLe>*{ zJY15->yW+<3rLgYeTruFDtk1ovU<$(_y7#HgUq>)r0{^}Xbth}V#6?%5jeFYt;SG^ z3qF)=uWRU;Jj)Q}cpY8-H+l_n$2$6{ZR?&*IGr{>ek!69ZH0ZoJ*Ji+ezzlJ^%qL3 zO5a`6gwFw(moEzqxh=yJ9M1FTn!eo&qD#y5AZXErHs%22?A+JmS&GIolml!)rZTnUDM3YgzYfT#;OXn)`PWv3Ta z!-i|-Wojv*k&bC}_JJDjiAK(Ba|YZgUI{f}TdEOFT2+}nPmttytw7j%@bQZDV1vvj z^rp{gRkCDmYJHGrE1~e~AE!-&6B6`7UxVQuvRrfdFkGX8H~SNP_X4EodVd;lXd^>eV1jN+Tt4}Rsn)R0LxBz0c=NXU|pUe!MQQFkGBWbR3&(jLm z%RSLc#p}5_dO{GD=DEFr=Fc% z85CBF>*t!6ugI?soX(*JNxBp+-DdZ4X0LldiK}+WWGvXV(C(Ht|!3$psR=&c*HIM=BmX;pRIpz@Ale{9dhGe(U2|Giv;# zOc|;?p67J=Q(kamB*aus=|XP|m{jN^6@V*Bpm?ye56Njh#vyJqE=DweC;?Rv7faX~ zde03n^I~0B2vUmr;w^X37tVxUK?4}ifsSH5_kpKZIzpYu0;Kv}SBGfI2AKNp+VN#z`nI{UNDRbo-wqa4NEls zICRJpu)??cj^*WcZ^MAv+;bDbh~gpN$1Cor<{Y2oyIDws^JsfW^5AL$azE(T0p&pP z1Mv~6Q44R&RHoH95&OuGx2srIr<@zYJTOMKiVs;Bx3py89I87LOb@%mr`0)#;7_~Z zzcZj8?w=)>%5@HoCHE_&hnu(n_yQ-L(~VjpjjkbT7e)Dk5??fApg(d>vwLRJ-x{um z*Nt?DqTSxh_MIyogY!vf1mU1`Gld-&L)*43f6dilz`Q@HEz;+>MDDYv9u!s;WXeao zUq=TaL$P*IFgJzrGc>j1dDOd zed+=ZBo?w4mr$2)Ya}?vedDopomhW1`#P<%YOJ_j=WwClX0xJH-f@s?^tmzs_j7t!k zK@j^zS0Q|mM4tVP5Ram$VbS6|YDY&y?Q1r1joe9dj08#CM{RSMTU}(RCh`hp_Rkl- zGd|Cv~G@F{DLhCizAm9AN!^{rNs8hu!G@8RpnGx7e`-+K$ffN<0qjR zGq^$dj_Tv!n*?zOSyk5skI7JVKJ)3jysnjIu-@VSzQiP8r6MzudCU=~?v-U8yzo^7 zGf~SUTvEp+S*!X9uX!sq=o}lH;r{pzk~M*VA(uyQ`3C8!{C;)&6)95fv(cK!%Cuz$ z_Zal57H6kPN>25KNiI6z6F)jzEkh#%OqU#-__Xzy)KyH};81#N6OfX$$IXWzOn`Q& z4f$Z1t>)8&8PcYfEwY5UadU1yg+U*(1m2ZlHoC-!2?gB!!fLhmTl))D@dhvkx#+Yj z1O=LV{(T%{^IeCuFK>%QR!VZ4GnO5tK8a+thWE zg4VytZrwcS?7^ zuZfhYnB8dwd%VLO?DK7pV5Wi<(`~DYqOXn8#jUIL^)12*Dbhk4GmL_E2`WX&iT16o zk(t|hok(Y|v-wzn?4x34T)|+SfZP>fiq!><*%vnxGN~ypST-FtC+@TPv*vYv@iU!_ z@2gf|PrgQ?Ktf*9^CnJ(x*CtZVB8!OBfg0%!wL;Z8(tYYre0vcnPGlyCc$V(Ipl*P z_(J!a=o@vp^%Efme!K74(Ke7A>Y}|sxV+JL^aYa{~m%5#$$+R1? zGaQhZTTX!#s#=Xtpegqero$RNt&`4xn3g$)=y*;=N=Qai)}~`xtxI_N*#MMCIq#HFifT zz(-*m;pVH&+4bixL&Bbg)W5FN^bH87pAHp)zPkWNMfTFqS=l~AC$3FX3kQUSh_C?-ZftyClgM)o_D7cX$RGlEYblux0jv5 zTr|i-I3@ZPCGheCl~BGhImF)K4!9@?pC(gi3ozX=a!|r1)LFxy_8c&wY0<^{2cm|P zv6Y`QktY*;I)IUd5y3ne1CqpVanlY45z8hf4&$EUBnucDj16pDa4&GI&TArYhf*xh zdj>*%APH8(h~c>o@l#%T>R$e>rwVx_WUB|~V`p^JHsg*y12lzj&zF}w6W09HwB2yb z%Q~`es&(;7#*DUC_w-Dmt7|$*?TA_m;zB+-u{2;Bg{O}nV7G_@7~<)Bv8fH^G$XG8$(&{A zwXJK5LRK%M34(t$&NI~MHT{UQ9qN-V_yn|%PqC81EIiSzmMM=2zb`mIwiP_b)x+2M z7Gd`83h79j#SItpQ}luuf2uOU`my_rY5T{6P#BNlb%h%<#MZb=m@y5aW;#o1^2Z)SWo+b`y0gV^iRcZtz5!-05vF z7wNo=hc6h4hc&s@uL^jqRvD6thVYtbErDK9k!;+a0xoE0WL7zLixjn5;$fXvT=O3I zT6jI&^A7k6R{&5#lVjz#8%_RiAa2{di{`kx79K+j72$H(!ass|B%@l%KeeKchYLe_ z>!(JC2fxsv>XVen+Y42GeYPxMWqm`6F$(E<6^s|g(slNk!lL*6v^W2>f6hh^mE$s= z3D$)}{V5(Qm&A6bp%2Q}*GZ5Qrf}n7*Hr51?bJOyA-?B4vg6y_EX<*-e20h{=0Mxs zbuQGZ$fLyO5v$nQ&^kuH+mNq9O#MWSfThtH|0q1i!NrWj^S}_P;Q1OkYLW6U^?_7G zx2wg?CULj7))QU(n{$0JE%1t2dWrMi2g-Os{v|8^wK{@qlj%+1b^?NI z$}l2tjp0g>K3O+p%yK<9!XqmQ?E9>z&(|^Pi~aSRwI5x$jaA62GFz9%fmO3t3a>cq zK8Xbv=5Ps~4mKN5+Eqw12(!PEyedFXv~VLxMB~HwT1Vfo51pQ#D8e$e4pFZ{&RC2P z5gTIzl{3!&(tor^BwZfR8j4k{7Rq#`riKXP2O-Bh66#WWK2w=z;iD9GLl+3 zpHIaI4#lQ&S-xBK8PiQ%dwOh?%BO~DCo06pN7<^dnZCN@NzY{_Z1>rrB0U|nC&+!2 z2y!oBcTd2;@lzyk(B=TkyZ)zy0deK05*Q0zk+o$@nun`VI1Er7pjq>8V zNmlW{p7S^Btgb(TA}jL(uR>`0w8gHP^T~Sh5Tkip^spk4SBAhC{TZU}_Z)UJw-}zm zPq{KBm!k)?P{`-(9?LFt&YN4s%SIZ-9lJ!Ws~B%exHOeVFk3~}HewnnH(d)qkLQ_d z6h>O)pEE{vbOVw}E+jdYC^wM+AAhaI(YAibUc@B#_mDss0Ji&BK{WG`4 zOk>vSNq(Bq2IB@s>>Rxm6Wv?h;ZXkpb1l8u|+_qXWdC*jjcPCixq;!%BVPSp#hP zqo`%cNf&YoQXHC$D=D45RiT|5ngPlh?0T~?lUf*O)){K@*Kbh?3RW1j9-T?%lDk@y z4+~?wKI%Y!-=O|_IuKz|=)F;V7ps=5@g)RrE;;tvM$gUhG>jHcw2Hr@fS+k^Zr~>G z^JvPrZc}_&d_kEsqAEMTMJw!!CBw)u&ZVzmq+ZworuaE&TT>$pYsd9|g9O^0orAe8 z221?Va!l1|Y5X1Y?{G7rt1sX#qFA^?RLG^VjoxPf63;AS=_mVDfGJKg73L zsGdnTUD40y(>S##2l|W2Cy!H(@@5KBa(#gs`vlz}Y~$ot5VsqPQ{{YtjYFvIumZzt zA{CcxZLJR|4#{j7k~Tu*jkwz8QA|5G1$Cl895R`Zyp;irp1{KN){kB30O8P1W5;@bG znvX74roeMmQlUi=v9Y%(wl$ZC#9tKNFpvi3!C}f1m6Ct|l2g%psc{TJp)@yu)*e2> z((p0Fg*8gJ!|3WZke9;Z{8}&NRkv7iP=#_y-F}x^y?2m%-D_aj^)f04%mneyjo_;) z6qc_Zu$q37d~X``*eP~Q>I2gg%rrV8v=kDfpp$=%Vj}hF)^dsSWygoN(A$g*E=Do6FX?&(@F#7pbiJ`;c0c@Ul zDqW_90Wm#5f2L<(Lf3)3TeXtI7nhYwRm(F;*r_G6K@OPW4H(Y3O5SjUzBC}u3d|eQ8*8d@?;zUPE+i#QNMn=r(ap?2SH@vo*m z3HJ%XuG_S6;QbWy-l%qU;8x;>z>4pMW7>R}J%QLf%@1BY(4f_1iixd-6GlO7Vp*yU zp{VU^3?s?90i=!#>H`lxT!q8rk>W_$2~kbpz7eV{3wR|8E=8**5?qn8#n`*(bt1xRQrdGxyx2y%B$qmw#>ZV$c7%cO#%JM1lY$Y0q?Yuo> ze9KdJoiM)RH*SB%^;TAdX-zEjA7@%y=!0=Zg%iWK7jVI9b&Dk}0$Af&08KHo+ zOwDhFvA(E|ER%a^cdh@^wLUlmIv6?_3=BvX8jKk92L=Y}7Jf5OGMfh` zBdR1wFCi-i5@`9km{isRb0O%TX+f~)KNaEz{rXQa89`YIF;EN&gN)cigu6mNh>?Cm zAO&Im2flv6D{jwm+y<%WsPe4!89n~KN|7}Cb{Z;XweER73r}Qp2 zz}WP4j}U0&(uD&9yGy6`!+_v-S(yG*iytsTR#x_Rc>=6u^vnRDnf1gP{#2>`ffrAC% zTZ5WQ@hAK;P;>kX{D)mIXe4%a5p=LO1xXH@8T?mz7Q@d)$3pL{{B!2{-v70L*o1AO+|n5beiw~ zk@(>m?T3{2k2c;NWc^`4@P&Z?BjxXJ@;x1qhn)9Mn*IFdt_J-dIqx5#d`NfyfX~m( zIS~5)MfZ2Uy?_4W`47i}u0ZgPh<{D|w_d#;D}Q&U$Q-G}xM1A@1f{#%A$jh6Qp&0hQ<0bPOM z-{1Wm&p%%#eb_?x7i;bol EfAhh=DF6Tf literal 0 HcmV?d00001 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..145a470 --- /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-redis-starter + 1.0.5 + + + + 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..0f7f06d --- /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; +} 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..938556f --- /dev/null +++ b/examples/jdempotent-couchbase-example/src/main/resources/application.yml @@ -0,0 +1,36 @@ +jdempotent: + enable: true + cryptography: + algorithm: MD5 + cache: + redis: + database: 9 + password: "pass" + sentinelHostList: localhost + sentinelPort: 26379 + sentinelMasterName: "master" + expirationTimeHour: 2 + dialTimeoutSecond: 3 + readTimeoutSecond: 3 + writeTimeoutSecond: 3 + maxRetryCount: 3 + expireTimeoutHour: 84 + +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() { + } + +} From b4db0b1c8d5e21c9afa4565c511fa58fcc24c199 Mon Sep 17 00:00:00 2001 From: memojja Date: Thu, 13 May 2021 22:50:42 +0300 Subject: [PATCH 2/8] add jdempotent couchbase starter base and example project --- Jdempotent-core/pom.xml | 2 +- .../IdempotentRequestResponseWrapper.java | 4 +- .../core/model/IdempotentRequestWrapper.java | 5 +- .../core/model/IdempotentResponseWrapper.java | 4 +- .../pom.xml | 303 +++++++++++++++++- .../couchbase/ApplicationConfig.java | 39 ++- .../couchbase/CouchbaseBeanConfig.java | 71 +++- .../CouchbaseIdempotentRepository.java | 86 ++++- .../main/resources/META-INF/spring.factories | 9 +- ...nt-spring-boot-couchbase-starter-1.0.5.jar | Bin 0 -> 10438 bytes .../target/classes/META-INF/spring.factories | 4 + .../couchbase/ApplicationConfig.class | Bin 0 -> 2048 bytes .../couchbase/CouchbaseBeanConfig.class | Bin 0 -> 6208 bytes .../couchbase/CouchbaseConfig.class | Bin 0 -> 3002 bytes .../CouchbaseIdempotentRepository.class | Bin 0 -> 4936 bytes .../target/maven-archiver/pom.properties | 4 + .../compile/default-compile/createdFiles.lst | 4 + .../compile/default-compile/inputFiles.lst | 4 + .../default-testCompile/inputFiles.lst | 0 Jdempotent-spring-boot-redis-starter/pom.xml | 4 +- .../redis/RedisIdempotentRepository.java | 1 - README.md | 2 +- examples/jdempotent-couchbase-example/pom.xml | 2 +- .../example/demo/model/SendEmailRequest.java | 2 +- .../src/main/resources/application.yml | 20 +- examples/jdempotent-redis-example/pom.xml | 2 +- pom.xml | 3 +- 27 files changed, 533 insertions(+), 42 deletions(-) create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/Jdempotent-spring-boot-couchbase-starter-1.0.5.jar create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/classes/META-INF/spring.factories create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/ApplicationConfig.class create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseBeanConfig.class create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseConfig.class create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/classes/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.class create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/maven-archiver/pom.properties create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst diff --git a/Jdempotent-core/pom.xml b/Jdempotent-core/pom.xml index d9ca0a4..1c96a26 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.5 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 index 09ab30d..dcdb663 100644 --- a/Jdempotent-spring-boot-couchbase-starter/pom.xml +++ b/Jdempotent-spring-boot-couchbase-starter/pom.xml @@ -2,19 +2,304 @@ - - jdempotent - com.trendyol - 1.0.3 - 4.0.0 - + com.trendyol Jdempotent-spring-boot-couchbase-starter 1.0.5 + 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 + + - 15 - 15 + 11 + 2.3.4.RELEASE + 5.2.9.RELEASE + 1.10.19 + 4.13.1 - \ No newline at end of file + + + + com.trendyol + Jdempotent-core + 1.0.5 + + + 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.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/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/ApplicationConfig.java index 8ed0b31..b9cf3f1 100644 --- 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 @@ -1,2 +1,39 @@ -package com.trendyol.jdempotent.couchbase;public class ApplicationConfig { +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 index 6e31261..3efa4ee 100644 --- 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 @@ -1,2 +1,71 @@ -package com.trendyol.jdempotent.couchbase;public class CouchbaseBeanConfig { +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/CouchbaseIdempotentRepository.java b/Jdempotent-spring-boot-couchbase-starter/src/main/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepository.java index 20a0e51..7db2a4d 100644 --- 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 @@ -1,2 +1,86 @@ -package com.trendyol.jdempotent.couchbase;public class CouchbaseIdempotentRepository { +package com.trendyol.jdempotent.couchbase; + +import com.couchbase.client.java.Collection; +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.concurrent.TimeUnit; + +/** + * 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; + + public CouchbaseIdempotentRepository(CouchbaseConfig couchbaseConfig, Collection collection) { + this.couchbaseConfig = couchbaseConfig; + this.collection = collection; + } + + + @Override + public boolean contains(IdempotencyKey key) { + return collection.exists(key.getKeyValue()).exists(); + } + + @Override + public IdempotentResponseWrapper getResponse(IdempotencyKey key) { + return collection.get(key.getKeyValue()).contentAs(IdempotentRequestResponseWrapper.class).getResponse(); + } + + @Override + public void store(IdempotencyKey key, IdempotentRequestWrapper requestObject) { + collection.insert(key.getKeyValue(),new IdempotentRequestResponseWrapper(requestObject)); + } + + @Override + public void store(IdempotencyKey key, IdempotentRequestWrapper requestObject, Long ttl, TimeUnit timeUnit) { + Duration ttlDuration = getDurationByTttlAndTimeUnit(ttl, timeUnit); + collection.upsert( + key.getKeyValue(), new IdempotentRequestResponseWrapper(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)) { + IdempotentRequestResponseWrapper requestResponseWrapper = collection.get(key.getKeyValue()).contentAs(IdempotentRequestResponseWrapper.class); + requestResponseWrapper.setResponse(idempotentResponse); + collection.upsert(key.getKeyValue(), requestResponseWrapper); + } + } + + @Override + public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse, Long ttl, TimeUnit timeUnit) { + if (contains(key)) { + IdempotentRequestResponseWrapper requestResponseWrapper = collection.get(key.getKeyValue()).contentAs(IdempotentRequestResponseWrapper.class); + requestResponseWrapper.setResponse(idempotentResponse); + collection.upsert(key.getKeyValue(), requestResponseWrapper); + } + } + + private Duration getDurationByTttlAndTimeUnit(Long ttl, TimeUnit timeUnit) { + if (TimeUnit.DAYS.equals(timeUnit)) { + return Duration.ofDays(ttl); + } else if (TimeUnit.HOURS.equals(timeUnit)) { + return Duration.ofHours(ttl); + } else { //TODO look here + return Duration.ofMillis(ttl); + } + } } 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 index 6ef0076..9e0289d 100644 --- 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 @@ -1,7 +1,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.trendyol.jdempotent.redis.ApplicationConfig,\ -com.trendyol.jdempotent.redis.RedisConfigProperties,\ -com.trendyol.jdempotent.redis.RedisSentinelConfiguration,\ -org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration -org.springframework.boot.env.EnvironmentPostProcessor=\ -com.trendyol.jdempotent.redis.RedisEnvironmentPostProcessor \ No newline at end of file +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/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 0000000000000000000000000000000000000000..e50f192aec2415846f4863e4b77958d3e5238bc1 GIT binary patch literal 10438 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 ZjO{A&2zNdK1A~3{F+zZWQK7v5_dn;YlIs8f literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..61881975fe3e62b34143413a1832204e7bb74619 GIT binary patch literal 2048 zcmbuAZBG+H5Xb-97Age+p&Kup7 z6^ov9xsl2*`$)K*d102Sp3MbC;Tnc7W%in%t|zVITYQI;&z?t8WM%f0L$z=S*2{8t z+nLu|X*x0|trqvvm5tfq(+%(t$2rPJ*a^1^!7vzFITMzlN7MmfcyT((*{(43u9kcw z3nC)}N%u47`^tpDrt2A7RyywlMJ@gOhUNwFN$G9MBj1Q!M3cs9N&6|9O*fQKJ z(Q?da`)Dn2<8Cf(yplmcn^7t##xQ?LwzS8CfWR7d%7=YCXR=*9S=LlZyJA7wBI-U? z1>-iWsa{WX>x3(!K!?E(?{>BYEtqIQ0?iDQ^&19VRh?{RcwS$v(voU@rdA89qNoCC zl-{!llXjT0!!&iSvke}L0z-RP-5&Sz&PsMG>;usu+mJq7q?4VmCaw9;r?HLYr%73^ltB5M4h&Y(4cIk&%5g zjP9fHD=C%DH=;Sipyy3Ox)tCy?z{&J$V6X2 zj^2BR`xqdl2ej&vl4TmKFf9_3^)q>tX&>>UNCSsFO;(YT6(kE{(Je>>55sXN#OtT| JFwN`Z{RKI|UjhIC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a047f8f6c207b8af228cfd78d17c4ac98e8ec0f6 GIT binary patch literal 6208 zcmb_gX>`=q8NI?b!m_XdlaRHAK&i1Y3W0;^$=wUXE8NG~t!v!`))~Dh02`Yh;{I zumOi;yjI5RB(xcB))*hrGiHzLj9Oz7HuV{9O7lI_Nlv-8Hjy+_Y1cO$UyGM&J>^7O zB`n-#IhMa&!c`sTK)>#wgvK5>X;Ps+%Q5@2sS(rLuaDS#v#ifGbo-$0Sz;c&Z1l&i zjD#)o5w^qBozO}uEE{$<$b@P5d-ZhM z^tP@GL{4L2*p>iiLeJ@1Pso$(RkwsjLU%wkR}s{1Bx~78p`E;=I8qaiV|qQdp2?Ur z{n{FOm`<*!%PuEpd9IV9j%y-TRBw5#f5~?VSInCyc`Y9D2^i6dTyx>Rtm9iL^PrWn z=&&0d$MtpJavdt&-RF8^S|;sT&e*7@r_5unH_5G4uc_=>z0!?kTig@Fd61*ZWxwXFTjZ^J*6Vyi0cxh ztnU&q=4#e61$0SEz{8x3M!G%V40^ z#oL6)x662kig)7OD&B+lNw~VAs2~Yz2fvK>tM~vuDC2$=AHqo$58y!=A5n1%r)A_- zoWW@o1$(h}eVM&0htvh2; zA(YV2Au9Ie^Ywv*ZBeSt3X(Z^jpC}KM9@B#8-&(L;8HBdscJ33^F@ zvCM?avKSm|mXQ?)gDf=(?lxXdw`KfY!tRdH@8yi^*;3?NV#FdVmSxokFjOlhuP3iM zcI#^cKnSy*^m6ou1KEjK1bZw|aAigy-LqoS7 zcvZBiXGC=fgTgo~^@5l*cqTQp-ta)!%R}Li$z!N(-7b!13k}cIeeRQ^p~`ct#MM%@ zo>%?&ZD@+u>#DO}`sbcC&(s2hd9=XEvUNtk7JPi{Yx+s*pS^|C_S=Q?A7 zUE=f{Y%$^uBgEz-I*hQ`_DWdf@)%;<;?Q5G;6|c~5#KyHEqu>8C3_jeViZ&BrL>Gv z9G*{O^ETDah>EP}S7Qd7HnP4sYkE^LWA(PO>0&7Ya8F^j*aoA z7VSJM!fLnE(#IS(<68zli0h7wfARubACXdSvF^+(yG%-|9u5`RoI3giw{iU%YQiI8wza0d3`KTP-Cki(6IKdeP|!i;D!k zlD%~~L~zYTF#@od-r12gCQZLTykU+6lb|sa*iZ^~Eoeg~25(s&A^;qNeOj|cG(HOn1M3>6Vj&`1b)Y)|!3m)KX z0eh`LD@0r2Vp3d!W?YJ^IXgdGjcaf%XQT_b@+K!P!u&>_(h>Gx8~N>20S$W}MB}~K zvi>ZZ4i~U654mal3>Fp8oQLuVT9!jCptXRuJeF|UP{7iLJeK9rerSDH0n76^uk#cZ zDO^W>yt;D5_1HlyPhcf_u#@`i z;`&99{)6o@d&|2yTga6+@yUJ^5+UxKhrVp@?8>7vG>xP6d2|c2LZ=EsQ0ZxBw#O>8 zM=F#HxQ^xpj-_!60_sI)vElF;XfwI)3b?A_nuu#DuLkT1^ymvrSVeAoIq#>*0~rkB z=360U*pL~@U_TBF;~;M2=N+`Xh0XpO7=!O(V_!Vqi@yfX53`#u<|+2_#q$pK>c#Wx N?8=MhciEHY^MCy*v`PQ~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ef302e831628c1d3a9e18cefcb182af27e51cde4 GIT binary patch literal 3002 zcmb7`YjfK~6o$__wv*Tnxd+NUP>Pd~h)XD?386_!NvWGSmwLE;K$h3G5?kv?E7J_a ze}WkpX86Dl;72jMEBPYJYcqWCkyhuN-Dmf_I`ZHD{P`CVeL|%SW$DQ@WvHOcQ)P~o zIZ>vl%&9VG%A8Nq#UwprG-nCN;g+<8b1q%msWDn8T$*juXqZmT2%qgRx^?C|(r)sS z?b+o9-*+4#O_k*_+AavUW_T@?UUkhT|53PCMwy$AXH-o~3irk^dkIF#Z1@~grrPR; zk#YN{%L;F{gyfFgvP`Saw=CgX^|I-yyDs0R7XbduXv+7v>tG>9cg7}n<5W(|^gMu7 z{wt>&hn!5=x30K64pv$lyHXgrsi2VfLWlYSCnkJ}V~(6@K{z#Re{<}7oe4J8nPUdb znY;G6d()e7V{Cd58%dtNYWFhpV>7SXk+hi>cRkyaXZ-q%qtc6!Z5#;EK#8#vT@3~j z?@QNL$CBQ)9b4`(n#iw}7$pxy1tpv<*bYDTn`Q1^n9!C|ps>tF$#iWs?mSG&x($Sn z3YKUZ(&bL&Ml_7Ra*Up4jKi*a4uw;-)nPBzxWpi)^rie-WJ5izFM7J*<@|80+C>&q zKkMl+i`>WvzG6>z%GV#wz;_VQ*9^ST@q8b-SGt7(;Es^y`?$Tp#VBkLr%-1x<>`Qm zW|2GiB!`$MeX-;a5E;(FTO8EH@&Hp`6?4dpg|0-Yx{(Sjb1r<>;*V@q{iRs>wp2S} zbfoVxLP*2(rN9e=Qu{844xEauS`&28xicFlPSF(*rM!U}(k+))?dMpxFCWM;>?mpO zgeuI|%{Dq((5~QB8pfO5G^JI4TzzVL9-I?sy^PUaEgL{2e-6BU?*mal8MMxw9KA(v z=O{-rIa*L=NtqR8UZPt$dPSMnlzD^RWc0!RRz4a}zn7nsFVW~}741I@>?um9l?i*o zD2wYXesdnp!vUIM$08gc9Cj?j0qSALi*R5k!;Y8X!0v_}ufl;H4?A9m1E()v zN#4U!3ANPv#_yEqFP#aV-lG(K8qBZ~Ms=1~Xki`jP0)ffnoAX|Eni8#mufb5R| z3G0q;vKAy62YILkd0!8bjDs8jF8Ui{wtz?0Y!gOCIA2c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5223d02d28e6da5baf218458c841f0628fcd8bc5 GIT binary patch literal 4936 zcmdT|*;f-+9R4Oi2EyQyvWQEyE)-%kh}K#WkWEoQl(ks93}L{5Arq4cHQlVO-S^ho z`p}m?_dz{1dfIb(`rN;we@RcjJ2ROi?BF>)r!O~|JHPw=zWe=_dy~KZ`Rxw?&*R4c zcHw*(cA{4deF602LI4A}7{DbAif=cdQPVKAsHK}R33WC^&OB=9V*QLd zuX1VDj;s8q^q6k7O1M(L0k3QG(l9EaxYL}}xY<5Erd>$QO=yW>b;96Gu+NOD#;BUm z#j`tEY+cop657{8>6|CipcXfix@9KP++bC%W|tFluv8cpq7lAu_Sk!+z^EoWLWIj^EQ@H<8duP4N8&}W24^J>-<_u zOIlgqBJ>pr&7K>Wm|=QKXn7(;2|sT{!zc3DXV;CP#-<~EW^B6Ic}`in!HSGUQwf%; z6&cp&v=J6lv)FWzWf>B-EL_Qav|KX@yH~+s@(`sfl~A3cu5|1LKqNaWN;sj-nez zsYFyeql?pKuXk>GZJzxydt4kAb*^9^;tF7@;5$282?oA|jy;w>n z_O)C?%y#ut%Y(9pCsj*L+HuI8uX)0=U)GD1II<$hQs=XC!1ACTRgDw_Tv^|cmo`II zLTA60uwMYLb{%3X)voJFi&urV{N!^{8tfWh8f11p(rqtgXj6toj}6OvTwrW(UfeQR z4-!tT-}70!dpmCHR&?uGN+=WeA|6}q-0gvUpPh!9Og87M=!BSiI!$rc6QL!pBB$5a z*NCv_KH=wOPD@a#p?unIkn)1>(q1KO{VB^nUcJDDR#b2#E_@Hf#fePiDpG~dhh2r1 z&60xkl$044*Au*5cDS1j3M`ldB|QTpgXAEaQ$6gFv@JEK`gOz5lRSZT^7pEQr{P0T97_BV8x-df zKg&435BoVD;L|uQiuk!X^axVu;6wOA;fE-?&nX}3I0jM7aT~{4F5Qkz*nwv`v&UJ6 zdNlBf0Ja;+{w5sx=zyn<>!y>>KJhN}D~b+gP@F+Y2L4|-<+${|F#sjWF7Pt{dT;MU3*gLDr0{@QE@$e&* zXP}JbWC_?DL#X5yb8=L9&{ZxpP=zKOrL}}c9CMxAp%XtT)`bq$Qi_U`B@4Yh#->^- zv^j$z@Kn?anQvSk$>J=o8Q3xV^w7QT#(QOI0-4tR91n(l8 zgZ6@VvbeVRPzFzBUkQCGZNEi!K33ZZE&SW0m9*QK%BLwrht1&_-|b>Z+Bhe&w#j4D zL{sGcF!v!vBSs^>_x*QVM- z^ek1832UP&>_qgO&57uh4.0.0 com.trendyol Jdempotent-spring-boot-redis-starter - 1.0.4 + 1.0.5 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.5 redis.clients 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 78dc59b..c0036c3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Make your listener or etc idempotent easily com.trendyol Jdempotent-spring-boot-redis-starter - 1.0.4 + 1.0.5 ``` diff --git a/examples/jdempotent-couchbase-example/pom.xml b/examples/jdempotent-couchbase-example/pom.xml index 145a470..e7b71db 100644 --- a/examples/jdempotent-couchbase-example/pom.xml +++ b/examples/jdempotent-couchbase-example/pom.xml @@ -58,7 +58,7 @@ com.trendyol - Jdempotent-spring-boot-redis-starter + Jdempotent-spring-boot-couchbase-starter 1.0.5 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 index 0f7f06d..ba87424 100644 --- 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 @@ -15,4 +15,4 @@ 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/resources/application.yml b/examples/jdempotent-couchbase-example/src/main/resources/application.yml index 938556f..87bce89 100644 --- a/examples/jdempotent-couchbase-example/src/main/resources/application.yml +++ b/examples/jdempotent-couchbase-example/src/main/resources/application.yml @@ -3,18 +3,14 @@ jdempotent: cryptography: algorithm: MD5 cache: - redis: - database: 9 - password: "pass" - sentinelHostList: localhost - sentinelPort: 26379 - sentinelMasterName: "master" - expirationTimeHour: 2 - dialTimeoutSecond: 3 - readTimeoutSecond: 3 - writeTimeoutSecond: 3 - maxRetryCount: 3 - expireTimeoutHour: 84 + couchbase: + connection-string: XXXXXXXX + password: XXXXXXXX + username: XXXXXXXX + bucket-name: XXXXXXXX + connect-timeout: 100000 + query-timeout: 20000 + kv-timeout: 3000 email: from: diff --git a/examples/jdempotent-redis-example/pom.xml b/examples/jdempotent-redis-example/pom.xml index 7b08513..145a470 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.5 diff --git a/pom.xml b/pom.xml index 049d4b0..be72e0d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.trendyol jdempotent pom - 1.0.4 + 1.0.5 Jdempotent https://github.com/Trendyol/Jdempotent Jdempotent @@ -44,6 +44,7 @@ Jdempotent-core Jdempotent-spring-boot-redis-starter + Jdempotent-spring-boot-couchbase-starter From 5dbc53c8513593adee63c3c978c3170e1310c208 Mon Sep 17 00:00:00 2001 From: "mehmet.ari" Date: Thu, 13 May 2021 22:56:45 +0300 Subject: [PATCH 3/8] update readme to introduce jdempotent couchbase starter --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c0036c3..6d1a292 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ public class AspectConditionalCallback implements ErrorConditionalCallback { } ``` -4 - Let's make redis configuration. +4 - Now you need to decide your datasource. That maybe inmemory,redis or couchbase. +Let's make redis configuration. ```yaml jdempotent: @@ -81,6 +82,22 @@ jdempotent: expireTimeoutHour: 3 ``` +Let's make redis 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 +``` Also you can disable jdempotent any time for example you don't have circut breaker but your redis down etc. You can wish disable jdempotent with following configuration. From ba70448ff7a24210ec99922ee99a4b3702ff2183 Mon Sep 17 00:00:00 2001 From: "mehmet.ari" Date: Thu, 13 May 2021 23:03:40 +0300 Subject: [PATCH 4/8] update readme file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d1a292..0c2b5da 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ jdempotent: expireTimeoutHour: 3 ``` -Let's make redis configuration. +Let's make couchbase configuration. ```yaml jdempotent: enable: true From 1ef6d6b41233705e20143faa603f6cd5323b4a88 Mon Sep 17 00:00:00 2001 From: "mehmet.ari" Date: Thu, 13 May 2021 23:04:53 +0300 Subject: [PATCH 5/8] update readme file --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0c2b5da..79be448 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ jdempotent: query-timeout: 20000 kv-timeout: 3000 ``` + + Also you can disable jdempotent any time for example you don't have circut breaker but your redis down etc. You can wish disable jdempotent with following configuration. From c188b6836684b9765180d8b83314a8d52d0fc44a Mon Sep 17 00:00:00 2001 From: "ahmet.ata" Date: Fri, 4 Jun 2021 11:32:49 +0300 Subject: [PATCH 6/8] jdempotent-couchbase-starter test: Add unit tests --- .../pom.xml | 11 ++ .../CouchbaseIdempotentRepositoryTest.java | 140 ++++++++++++++++++ Jdempotent-spring-boot-redis-starter/pom.xml | 5 + 3 files changed, 156 insertions(+) create mode 100644 Jdempotent-spring-boot-couchbase-starter/src/test/java/com.trendyol.jtempotent.couchbase/CouchbaseIdempotentRepositoryTest.java diff --git a/Jdempotent-spring-boot-couchbase-starter/pom.xml b/Jdempotent-spring-boot-couchbase-starter/pom.xml index dcdb663..15eacc2 100644 --- a/Jdempotent-spring-boot-couchbase-starter/pom.xml +++ b/Jdempotent-spring-boot-couchbase-starter/pom.xml @@ -92,6 +92,17 @@ 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 diff --git a/Jdempotent-spring-boot-couchbase-starter/src/test/java/com.trendyol.jtempotent.couchbase/CouchbaseIdempotentRepositoryTest.java b/Jdempotent-spring-boot-couchbase-starter/src/test/java/com.trendyol.jtempotent.couchbase/CouchbaseIdempotentRepositoryTest.java new file mode 100644 index 0000000..9bbfc6b --- /dev/null +++ b/Jdempotent-spring-boot-couchbase-starter/src/test/java/com.trendyol.jtempotent.couchbase/CouchbaseIdempotentRepositoryTest.java @@ -0,0 +1,140 @@ +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.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 com.trendyol.jdempotent.couchbase.CouchbaseConfig; +import com.trendyol.jdempotent.couchbase.CouchbaseIdempotentRepository; +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.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +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(idempotencyKey.getKeyValue())).thenReturn(getResult); + + //When + IdempotentResponseWrapper result = couchbaseIdempotentRepository.getResponse(idempotencyKey); + + //Then + verify(collection, times(1)).get(idempotencyKey.getKeyValue()); + 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; + Duration duration = Duration.ofDays(1L); + IdempotentRequestResponseWrapper responseWrapper = new IdempotentRequestResponseWrapper(wrapper); + /*var mockUpsertOption = Mockito.mockStatic(UpsertOptions.class); + when(((UpsertOptions)mockUpsertOption)).thenReturn((UpsertOptions) mockUpsertOption);*/ + + //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()); + //verify((UpsertOptions)mockUpsertOption, times(1)).expiry(duration); + } +} \ No newline at end of file diff --git a/Jdempotent-spring-boot-redis-starter/pom.xml b/Jdempotent-spring-boot-redis-starter/pom.xml index af5649a..08cf7fb 100644 --- a/Jdempotent-spring-boot-redis-starter/pom.xml +++ b/Jdempotent-spring-boot-redis-starter/pom.xml @@ -80,6 +80,11 @@ 4.13.1 test + + org.junit.vintage + junit-vintage-engine + 5.7.0 + org.springframework.boot spring-boot-starter-test From 0dfda7814fe492fe1064872a42ab1664716420bf Mon Sep 17 00:00:00 2001 From: "ahmet.ata" Date: Fri, 4 Jun 2021 11:39:21 +0300 Subject: [PATCH 7/8] Add test-classes --- .../CouchbaseIdempotentRepositoryTest.class | Bin 0 -> 6454 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Jdempotent-spring-boot-couchbase-starter/target/test-classes/CouchbaseIdempotentRepositoryTest.class 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 0000000000000000000000000000000000000000..18898d1f9396f1358775d4438e459c3a8b27e3dd GIT binary patch literal 6454 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 literal 0 HcmV?d00001 From 525215a04180b914e1853c938f90b01e74d777f6 Mon Sep 17 00:00:00 2001 From: memojja Date: Fri, 16 Jul 2021 12:08:09 +0300 Subject: [PATCH 8/8] add ttl&set reg&res logic for jdempotent cb starter --- Jdempotent-core/pom.xml | 2 +- .../pom.xml | 4 +- .../jdempotent/couchbase/CouchbaseConfig.java | 23 +----- .../CouchbaseIdempotentRepository.java | 74 +++++++++++++++---- .../CouchbaseIdempotentRepositoryTest.java | 58 ++++++++++++--- Jdempotent-spring-boot-redis-starter/pom.xml | 4 +- README.md | 20 ++--- examples/jdempotent-couchbase-example/pom.xml | 2 +- .../src/main/resources/application.yml | 1 + examples/jdempotent-redis-example/pom.xml | 2 +- pom.xml | 2 +- 11 files changed, 130 insertions(+), 62 deletions(-) rename Jdempotent-spring-boot-couchbase-starter/src/test/java/{com.trendyol.jtempotent.couchbase => com/trendyol/jdempotent/couchbase}/CouchbaseIdempotentRepositoryTest.java (67%) diff --git a/Jdempotent-core/pom.xml b/Jdempotent-core/pom.xml index 1c96a26..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.5 + 1.0.6 Jdempotent-core jar https://github.com/Trendyol/Jdempotent/tree/master/Jdempotent-core diff --git a/Jdempotent-spring-boot-couchbase-starter/pom.xml b/Jdempotent-spring-boot-couchbase-starter/pom.xml index 15eacc2..1284db5 100644 --- a/Jdempotent-spring-boot-couchbase-starter/pom.xml +++ b/Jdempotent-spring-boot-couchbase-starter/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.trendyol Jdempotent-spring-boot-couchbase-starter - 1.0.5 + 1.0.6 Jdempotent-spring-boot-couchbase-starter jar https://github.com/Trendyol/Jdempotent/tree/master/Jdempotent-spring-boot-couchbase-starter @@ -54,7 +54,7 @@ com.trendyol Jdempotent-core - 1.0.5 + 1.0.6 org.aspectj 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 index dac5617..03f4d16 100644 --- 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 @@ -1,29 +1,8 @@ package com.trendyol.jdempotent.couchbase; -import com.couchbase.client.core.deps.io.netty.channel.epoll.EpollEventLoopGroup; -import com.couchbase.client.core.env.CompressionConfig; -import com.couchbase.client.core.env.IoConfig; -import com.couchbase.client.core.env.IoEnvironment; -import com.couchbase.client.core.env.LoggerConfig; -import com.couchbase.client.core.env.SecurityConfig; -import com.couchbase.client.core.env.TimeoutConfig; -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 java.time.Duration; - -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfigurationPackage; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.apache.commons.lang3.SystemUtils; -import org.springframework.context.annotation.Primary; @ConditionalOnProperty( prefix="jdempotent", name = "enable", @@ -45,7 +24,7 @@ public class CouchbaseConfig { private Long queryTimeout; @Value("${jdempotent.cache.couchbase.kv-timeout}") private Long kvTimeout; - @Value("${jdempotent.cache.persistReqRes:true}") + @Value("${jdempotent.cache.persistReqRes:false}") private Boolean persistReqRes; public String getConnectionString() { 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 index 7db2a4d..654a78e 100644 --- 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 @@ -1,6 +1,8 @@ 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; @@ -9,7 +11,10 @@ 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 @@ -20,10 +25,12 @@ 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(); } @@ -34,19 +41,19 @@ public boolean contains(IdempotencyKey key) { @Override public IdempotentResponseWrapper getResponse(IdempotencyKey key) { - return collection.get(key.getKeyValue()).contentAs(IdempotentRequestResponseWrapper.class).getResponse(); + 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(),new IdempotentRequestResponseWrapper(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(), new IdempotentRequestResponseWrapper(requestObject), + key.getKeyValue(), prepareRequestValue(requestObject), UpsertOptions.upsertOptions().expiry(ttlDuration) ); } @@ -59,8 +66,8 @@ public void remove(IdempotencyKey key) { @Override public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse) { if (contains(key)) { - IdempotentRequestResponseWrapper requestResponseWrapper = collection.get(key.getKeyValue()).contentAs(IdempotentRequestResponseWrapper.class); - requestResponseWrapper.setResponse(idempotentResponse); + GetResult getResult = collection.get(key.getKeyValue(), GetOptions.getOptions().withExpiry(true)); + IdempotentRequestResponseWrapper requestResponseWrapper = prepareResponseValue(getResult,idempotentResponse); collection.upsert(key.getKeyValue(), requestResponseWrapper); } } @@ -68,19 +75,58 @@ public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, Id @Override public void setResponse(IdempotencyKey key, IdempotentRequestWrapper request, IdempotentResponseWrapper idempotentResponse, Long ttl, TimeUnit timeUnit) { if (contains(key)) { - IdempotentRequestResponseWrapper requestResponseWrapper = collection.get(key.getKeyValue()).contentAs(IdempotentRequestResponseWrapper.class); - requestResponseWrapper.setResponse(idempotentResponse); - collection.upsert(key.getKeyValue(), requestResponseWrapper); + 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) { - if (TimeUnit.DAYS.equals(timeUnit)) { - return Duration.ofDays(ttl); - } else if (TimeUnit.HOURS.equals(timeUnit)) { - return Duration.ofHours(ttl); - } else { //TODO look here - return Duration.ofMillis(ttl); + 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/test/java/com.trendyol.jtempotent.couchbase/CouchbaseIdempotentRepositoryTest.java b/Jdempotent-spring-boot-couchbase-starter/src/test/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepositoryTest.java similarity index 67% rename from Jdempotent-spring-boot-couchbase-starter/src/test/java/com.trendyol.jtempotent.couchbase/CouchbaseIdempotentRepositoryTest.java rename to Jdempotent-spring-boot-couchbase-starter/src/test/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepositoryTest.java index 9bbfc6b..ac622c2 100644 --- a/Jdempotent-spring-boot-couchbase-starter/src/test/java/com.trendyol.jtempotent.couchbase/CouchbaseIdempotentRepositoryTest.java +++ b/Jdempotent-spring-boot-couchbase-starter/src/test/java/com/trendyol/jdempotent/couchbase/CouchbaseIdempotentRepositoryTest.java @@ -1,13 +1,14 @@ +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 com.trendyol.jdempotent.couchbase.CouchbaseConfig; -import com.trendyol.jdempotent.couchbase.CouchbaseIdempotentRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,10 +16,10 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; 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; @@ -88,13 +89,13 @@ public void given_an_available_object_when_couchbase_get_response_then_return_ex IdempotentRequestResponseWrapper wrapper = new IdempotentRequestResponseWrapper(); GetResult getResult = mock(GetResult.class); when(getResult.contentAs(IdempotentRequestResponseWrapper.class)).thenReturn(wrapper); - when(collection.get(idempotencyKey.getKeyValue())).thenReturn(getResult); + when(collection.get(eq(idempotencyKey.getKeyValue()),any())).thenReturn(getResult); //When IdempotentResponseWrapper result = couchbaseIdempotentRepository.getResponse(idempotencyKey); //Then - verify(collection, times(1)).get(idempotencyKey.getKeyValue()); + verify(collection, times(1)).get(eq(idempotencyKey.getKeyValue()),any()); assertEquals(result, wrapper.getResponse()); } @@ -121,10 +122,7 @@ public void given_an_available_object_when_couchbase_store_with_ttl_and_time_uni IdempotentRequestWrapper wrapper = new IdempotentRequestWrapper(); Long ttl = 1L; TimeUnit timeUnit = TimeUnit.DAYS; - Duration duration = Duration.ofDays(1L); IdempotentRequestResponseWrapper responseWrapper = new IdempotentRequestResponseWrapper(wrapper); - /*var mockUpsertOption = Mockito.mockStatic(UpsertOptions.class); - when(((UpsertOptions)mockUpsertOption)).thenReturn((UpsertOptions) mockUpsertOption);*/ //When couchbaseIdempotentRepository.store(idempotencyKey, wrapper, ttl, timeUnit); @@ -135,6 +133,48 @@ public void given_an_available_object_when_couchbase_store_with_ttl_and_time_uni upsertOptionCaptor.capture()); IdempotentRequestResponseWrapper idempotentRequestResponseWrapper = captor.getValue(); assertEquals(idempotentRequestResponseWrapper.getResponse(), responseWrapper.getResponse()); - //verify((UpsertOptions)mockUpsertOption, times(1)).expiry(duration); + } + + + @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-redis-starter/pom.xml b/Jdempotent-spring-boot-redis-starter/pom.xml index 08cf7fb..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.5 + 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.5 + 1.0.6 redis.clients diff --git a/README.md b/README.md index 79be448..aa8ad81 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,22 @@ Make your listener or etc idempotent easily 1 - First of all you need add dependency to pom.xml +For Redis: ```xml com.trendyol Jdempotent-spring-boot-redis-starter - 1.0.5 + 1.0.6 ``` +For Couchbase +```xml + + com.trendyol + Jdempotent-spring-boot-couchbase-starter + 1.0.6 + +``` 2 - You should add `@IdempotentResource` annotation to that you want to make idempotent resource, listener etc. @@ -124,11 +133,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/pom.xml b/examples/jdempotent-couchbase-example/pom.xml index e7b71db..788b3c3 100644 --- a/examples/jdempotent-couchbase-example/pom.xml +++ b/examples/jdempotent-couchbase-example/pom.xml @@ -59,7 +59,7 @@ com.trendyol Jdempotent-spring-boot-couchbase-starter - 1.0.5 + 1.0.6 diff --git a/examples/jdempotent-couchbase-example/src/main/resources/application.yml b/examples/jdempotent-couchbase-example/src/main/resources/application.yml index 87bce89..40a6d3a 100644 --- a/examples/jdempotent-couchbase-example/src/main/resources/application.yml +++ b/examples/jdempotent-couchbase-example/src/main/resources/application.yml @@ -3,6 +3,7 @@ jdempotent: cryptography: algorithm: MD5 cache: + persistReqRes: false couchbase: connection-string: XXXXXXXX password: XXXXXXXX diff --git a/examples/jdempotent-redis-example/pom.xml b/examples/jdempotent-redis-example/pom.xml index 145a470..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.5 + 1.0.6 diff --git a/pom.xml b/pom.xml index be72e0d..6a72864 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.trendyol jdempotent pom - 1.0.5 + 1.0.6 Jdempotent https://github.com/Trendyol/Jdempotent Jdempotent