From 167655d659ea258532b4d0ea75cb6763452e2ca7 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:56:55 +0530 Subject: [PATCH 01/27] add bucket4j deps --- .../spring-boot-libraries/pom.xml | 213 ++++++++++-------- 1 file changed, 122 insertions(+), 91 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index 090967d8a8..36b9ec17c9 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -87,7 +87,35 @@ javase ${zxing.version} - + + + com.github.vladimir-bukhtoyarov + bucket4j-core + ${bucket4j.version} + + + com.giffing.bucket4j.spring.boot.starter + bucket4j-spring-boot-starter + ${bucket4j-spring-boot-starter.version} + + + org.springframework.boot + spring-boot-starter-cache + + + javax.cache + cache-api + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.github.ben-manes.caffeine + jcache + ${caffeine.version} + @@ -97,109 +125,112 @@ - - spring-boot-libraries - - - src/main/resources - true - - + + spring-boot-libraries + + + src/main/resources + true + + - + - - org.apache.maven.plugins - maven-war-plugin - + + org.apache.maven.plugins + maven-war-plugin + - - pl.project13.maven - git-commit-id-plugin - ${git-commit-id-plugin.version} - - - get-the-git-infos - - revision - - initialize - - - validate-the-git-infos - - validateRevision - - package - - - - true - ${project.build.outputDirectory}/git.properties - - + + pl.project13.maven + git-commit-id-plugin + ${git-commit-id-plugin.version} + + + get-the-git-infos + + revision + + initialize + + + validate-the-git-infos + + validateRevision + + package + + + + true + ${project.build.outputDirectory}/git.properties + + - - - autoconfiguration - - - - org.apache.maven.plugins - maven-surefire-plugin - - - integration-test - - test - - - - **/*LiveTest.java - **/*IntegrationTest.java - **/*IntTest.java - - - **/AutoconfigurationTest.java - - - - - - - json - - - - - - - + + + autoconfiguration + + + + org.apache.maven.plugins + maven-surefire-plugin + + + integration-test + + test + + + + **/*LiveTest.java + **/*IntegrationTest.java + **/*IntTest.java + + + **/AutoconfigurationTest.java + + + + + + + json + + + + + + + - - + + com.baeldung.intro.App 8.5.11 2.4.1.Final 1.9.0 2.0.0 - 5.0.2 - 5.0.2 - 5.2.4 - 18.0 - 2.2.4 - 2.3.2 - 0.23.0 - 1.4.200 - 2.1.0 - 1.5-beta1 - 2.1 - 2.6.0 - 3.3.0 - + 5.0.2 + 5.0.2 + 5.2.4 + 18.0 + 2.2.4 + 2.3.2 + 0.23.0 + 1.4.200 + 2.1.0 + 1.5-beta1 + 2.1 + 2.6.0 + 3.3.0 + 4.10.0 + 0.2.0 + 2.8.2 + From e8c383d10f5b15bf741c7ba1e0dd9b66852317f6 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:58:05 +0530 Subject: [PATCH 02/27] area api impl --- .../controller/AreaCalculationController.java | 29 +++++++++++++++++++ .../com/baeldung/ratelimiting/dto/AreaV1.java | 20 +++++++++++++ .../dto/RectangleDimensionsV1.java | 15 ++++++++++ .../dto/TriangleDimensionsV1.java | 15 ++++++++++ 4 files changed, 79 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java new file mode 100644 index 0000000000..f3fb63ebdd --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java @@ -0,0 +1,29 @@ +package com.baeldung.ratelimiting.controller; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.ratelimiting.dto.AreaV1; +import com.baeldung.ratelimiting.dto.RectangleDimensionsV1; +import com.baeldung.ratelimiting.dto.TriangleDimensionsV1; + +@RestController +@RequestMapping(value = "/api/v1/area", consumes = MediaType.APPLICATION_JSON_VALUE) +class AreaCalculationController { + + @PostMapping(value = "/rectangle", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity rectangle(@RequestBody RectangleDimensionsV1 dimensions) { + + return ResponseEntity.ok(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth())); + } + + @PostMapping(value = "/triangle", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity triangle(@RequestBody TriangleDimensionsV1 dimensions) { + + return ResponseEntity.ok(new AreaV1("triangle", 0.5d * dimensions.getHeight() * dimensions.getBase())); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java new file mode 100644 index 0000000000..78097f55b2 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java @@ -0,0 +1,20 @@ +package com.baeldung.ratelimiting.dto; + +public class AreaV1 { + + private String shape; + private Double area; + + public AreaV1(String shape, Double area) { + this.area = area; + this.shape = shape; + } + + public Double getArea() { + return area; + } + + public String getShape() { + return shape; + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java new file mode 100644 index 0000000000..e3c17e1ba7 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java @@ -0,0 +1,15 @@ +package com.baeldung.ratelimiting.dto; + +public class RectangleDimensionsV1 { + + private double length; + private double width; + + public double getLength() { + return length; + } + + public double getWidth() { + return width; + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java new file mode 100644 index 0000000000..44c954bded --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java @@ -0,0 +1,15 @@ +package com.baeldung.ratelimiting.dto; + +public class TriangleDimensionsV1 { + + private double base; + private double height; + + public double getBase() { + return base; + } + + public double getHeight() { + return height; + } +} From 4e45b8f44ef8c0b5d0e8c6b3a18f6d07be7cf796 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:58:40 +0530 Subject: [PATCH 03/27] spring mvc using interceptor --- .../bucket4japp/Bucket4jRateLimitingApp.java | 29 +++++++++ .../bucket4japp/interceptor/PricingPlan.java | 48 ++++++++++++++ .../interceptor/RateLimitingInterceptor.java | 65 +++++++++++++++++++ .../ratelimiting/application-bucket4j.yml | 10 +++ 4 files changed, 152 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java new file mode 100644 index 0000000000..2a42448b35 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java @@ -0,0 +1,29 @@ +package com.baeldung.ratelimiting.bucket4japp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.baeldung.ratelimiting.bucket4japp.interceptor.RateLimitingInterceptor; + +@SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = { + DataSourceAutoConfiguration.class, + SecurityAutoConfiguration.class +}) +public class Bucket4jRateLimitingApp implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new RateLimitingInterceptor()) + .addPathPatterns("/api/v1/area/**"); + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + .properties("spring.config.location=classpath:ratelimiting/application-bucket4j.yml") + .run(args); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java new file mode 100644 index 0000000000..e2b3ccb6c6 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java @@ -0,0 +1,48 @@ +package com.baeldung.ratelimiting.bucket4japp.interceptor; + +import java.time.Duration; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Refill; + +enum PricingPlan { + + FREE { + + @Override + Bandwidth getLimit() { + return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); + } + }, + + BASIC { + + @Override + Bandwidth getLimit() { + return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1))); + } + }, + + PROFESSIONAL { + + @Override + Bandwidth getLimit() { + return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1))); + } + }; + + abstract Bandwidth getLimit(); + + static PricingPlan resolvePlanFromApiKey(String apiKey) { + if (apiKey == null || apiKey.isEmpty()) { + return FREE; + + } else if (apiKey.startsWith("PX001-")) { + return PROFESSIONAL; + + } else if (apiKey.startsWith("BX001-")) { + return BASIC; + } + return FREE; + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java new file mode 100644 index 0000000000..8aa8de531c --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java @@ -0,0 +1,65 @@ +package com.baeldung.ratelimiting.bucket4japp.interceptor; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.HandlerInterceptor; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.ConsumptionProbe; + +public class RateLimitingInterceptor implements HandlerInterceptor { + + private static final String HEADER_API_KEY = "X-api-key"; + private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; + private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Milliseconds"; + + private final Map cache = new ConcurrentHashMap<>(); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + String apiKey = request.getHeader(HEADER_API_KEY); + + if (apiKey == null || apiKey.isEmpty()) { + response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY); + return false; + } + + Bucket tokenBucket = cache.computeIfAbsent(apiKey, this::resolveBucket); + + ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1); + + if (probe.isConsumed()) { + + response.addHeader(HEADER_LIMIT_REMAINING, String.valueOf(probe.getRemainingTokens())); + return true; + + } else { + + long waitForRefillMilli = probe.getNanosToWaitForRefill() % 1_000_000; + + response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 + response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefillMilli)); + + return false; + } + } + + private Bucket resolveBucket(String apiKey) { + PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey); + return bucket(pricingPlan.getLimit()); + } + + private Bucket bucket(Bandwidth limit) { + return Bucket4j.builder() + .addLimit(limit) + .build(); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml new file mode 100644 index 0000000000..1fb4d2cf12 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml @@ -0,0 +1,10 @@ +server: + port: 9000 + +spring: + application: + name: bucket4j-api-rate-limiting-app + mvc: + throw-exception-if-no-handler-found: true + resources: + add-mappings: false From 1b5a64c4a49b7fc9017d487aaab79ad01713a39d Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:59:03 +0530 Subject: [PATCH 04/27] bucket4j spring boot starter --- .../Bucket4jRateLimitingApp.java | 21 ++++++++++ .../application-bucket4j-starter.yml | 41 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java new file mode 100644 index 0000000000..de2ab41bf0 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java @@ -0,0 +1,21 @@ +package com.baeldung.ratelimiting.bootstarterapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cache.annotation.EnableCaching; + +@SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = { + DataSourceAutoConfiguration.class, + SecurityAutoConfiguration.class, +}) +@EnableCaching +public class Bucket4jRateLimitingApp { + + public static void main(String[] args) { + new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + .properties("spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml") + .run(args); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml new file mode 100644 index 0000000000..1c1337c611 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml @@ -0,0 +1,41 @@ +server: + port: 9001 + +spring: + application: + name: bucket4j-starter-api-rate-limiting-app + mvc: + throw-exception-if-no-handler-found: true + resources: + add-mappings: false + cache: + cache-names: + - rate-limiting-buckets + caffeine: + spec: maximumSize=100000,expireAfterAccess=3600s + +bucket4j: + enabled: true + filters: + - cache-name: rate-limiting-buckets + url: /api/v1/area.* + http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }" + rate-limits: + - expression: "getHeader('X-api-key')" + execute-condition: "getHeader('X-api-key').startsWith('PX001-')" + bandwidths: + - capacity: 100 + time: 1 + unit: hours + - expression: "getHeader('X-api-key')" + execute-condition: "getHeader('X-api-key').startsWith('BX001-')" + bandwidths: + - capacity: 40 + time: 1 + unit: hours + - expression: "getHeader('X-api-key')" + bandwidths: + - capacity: 20 + time: 1 + unit: hours + \ No newline at end of file From 34bbce79950ca2808ee0307fc23c9c109f7b2437 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 23:19:54 +0530 Subject: [PATCH 05/27] refactor to move pricing plan logic out into a separate class --- ...tingApp.java => Bucket4jRateLimitApp.java} | 4 +-- ...tingApp.java => Bucket4jRateLimitApp.java} | 14 +++++--- ...rceptor.java => RateLimitInterceptor.java} | 32 +++++++------------ .../{interceptor => service}/PricingPlan.java | 4 +-- .../service/PricingPlanService.java | 32 +++++++++++++++++++ .../application-bucket4j-starter.yml | 7 ++-- .../ratelimiting/application-bucket4j.yml | 8 ++++- 7 files changed, 67 insertions(+), 34 deletions(-) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/{Bucket4jRateLimitingApp.java => Bucket4jRateLimitApp.java} (88%) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/{Bucket4jRateLimitingApp.java => Bucket4jRateLimitApp.java} (73%) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/{RateLimitingInterceptor.java => RateLimitInterceptor.java} (68%) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/{interceptor => service}/PricingPlan.java (86%) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java similarity index 88% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java index de2ab41bf0..f16d347f85 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java @@ -11,10 +11,10 @@ import org.springframework.cache.annotation.EnableCaching; SecurityAutoConfiguration.class, }) @EnableCaching -public class Bucket4jRateLimitingApp { +public class Bucket4jRateLimitApp { public static void main(String[] args) { - new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + new SpringApplicationBuilder(Bucket4jRateLimitApp.class) .properties("spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml") .run(args); } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java similarity index 73% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java index 2a42448b35..bb179b9b38 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java @@ -1,28 +1,34 @@ package com.baeldung.ratelimiting.bucket4japp; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Lazy; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import com.baeldung.ratelimiting.bucket4japp.interceptor.RateLimitingInterceptor; +import com.baeldung.ratelimiting.bucket4japp.interceptor.RateLimitInterceptor; @SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = { DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) -public class Bucket4jRateLimitingApp implements WebMvcConfigurer { +public class Bucket4jRateLimitApp implements WebMvcConfigurer { + + @Autowired + @Lazy + private RateLimitInterceptor interceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new RateLimitingInterceptor()) + registry.addInterceptor(interceptor) .addPathPatterns("/api/v1/area/**"); } public static void main(String[] args) { - new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + new SpringApplicationBuilder(Bucket4jRateLimitApp.class) .properties("spring.config.location=classpath:ratelimiting/application-bucket4j.yml") .run(args); } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java similarity index 68% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java index 8aa8de531c..c983251e56 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java @@ -1,38 +1,39 @@ package com.baeldung.ratelimiting.bucket4japp.interceptor; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import io.github.bucket4j.Bandwidth; +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlanService; + import io.github.bucket4j.Bucket; -import io.github.bucket4j.Bucket4j; import io.github.bucket4j.ConsumptionProbe; -public class RateLimitingInterceptor implements HandlerInterceptor { +@Component +public class RateLimitInterceptor implements HandlerInterceptor { private static final String HEADER_API_KEY = "X-api-key"; private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Milliseconds"; - private final Map cache = new ConcurrentHashMap<>(); + @Autowired + private PricingPlanService pricingPlanService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - + String apiKey = request.getHeader(HEADER_API_KEY); if (apiKey == null || apiKey.isEmpty()) { response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY); return false; } - - Bucket tokenBucket = cache.computeIfAbsent(apiKey, this::resolveBucket); + + Bucket tokenBucket = pricingPlanService.resolveBucket(apiKey); ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1); @@ -51,15 +52,4 @@ public class RateLimitingInterceptor implements HandlerInterceptor { return false; } } - - private Bucket resolveBucket(String apiKey) { - PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey); - return bucket(pricingPlan.getLimit()); - } - - private Bucket bucket(Bandwidth limit) { - return Bucket4j.builder() - .addLimit(limit) - .build(); - } } \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java similarity index 86% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index e2b3ccb6c6..85632abf0b 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -1,4 +1,4 @@ -package com.baeldung.ratelimiting.bucket4japp.interceptor; +package com.baeldung.ratelimiting.bucket4japp.service; import java.time.Duration; @@ -11,7 +11,7 @@ enum PricingPlan { @Override Bandwidth getLimit() { - return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); + return Bandwidth.classic(2, Refill.intervally(2 , Duration.ofHours(1))); } }, diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java new file mode 100644 index 0000000000..713f4a6e1a --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java @@ -0,0 +1,32 @@ +package com.baeldung.ratelimiting.bucket4japp.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Service; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; + +@Service +public class PricingPlanService { + + private final Map cache = new ConcurrentHashMap<>(); + + // @Cacheable("rate-limit-buckets") + public Bucket resolveBucket(String apiKey) { + return cache.computeIfAbsent(apiKey, this::newBucket); + } + + private Bucket newBucket(String apiKey) { + PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey); + return bucket(pricingPlan.getLimit()); + } + + private Bucket bucket(Bandwidth limit) { + return Bucket4j.builder() + .addLimit(limit) + .build(); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml index 1c1337c611..ecc9f22e0a 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml @@ -3,21 +3,21 @@ server: spring: application: - name: bucket4j-starter-api-rate-limiting-app + name: bucket4j-starter-api-rate-limit-app mvc: throw-exception-if-no-handler-found: true resources: add-mappings: false cache: cache-names: - - rate-limiting-buckets + - rate-limit-buckets caffeine: spec: maximumSize=100000,expireAfterAccess=3600s bucket4j: enabled: true filters: - - cache-name: rate-limiting-buckets + - cache-name: rate-limit-buckets url: /api/v1/area.* http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }" rate-limits: @@ -38,4 +38,3 @@ bucket4j: - capacity: 20 time: 1 unit: hours - \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml index 1fb4d2cf12..0cee593261 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml @@ -3,8 +3,14 @@ server: spring: application: - name: bucket4j-api-rate-limiting-app + name: bucket4j-api-rate-limit-app mvc: throw-exception-if-no-handler-found: true resources: add-mappings: false + cache: + cache-names: + - rate-limit-buckets + caffeine: + spec: maximumSize=100000,expireAfterAccess=3600s + \ No newline at end of file From 911f840af59281685b6c6c95ad56a4a1777c3a9d Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 23:24:55 +0530 Subject: [PATCH 06/27] some cleanup --- .../bucket4japp/service/PricingPlanService.java | 1 - .../main/resources/ratelimiting/application-bucket4j.yml | 6 ------ 2 files changed, 7 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java index 713f4a6e1a..7d8a718601 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java @@ -14,7 +14,6 @@ public class PricingPlanService { private final Map cache = new ConcurrentHashMap<>(); - // @Cacheable("rate-limit-buckets") public Bucket resolveBucket(String apiKey) { return cache.computeIfAbsent(apiKey, this::newBucket); } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml index 0cee593261..ae19622d9b 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml @@ -8,9 +8,3 @@ spring: throw-exception-if-no-handler-found: true resources: add-mappings: false - cache: - cache-names: - - rate-limit-buckets - caffeine: - spec: maximumSize=100000,expireAfterAccess=3600s - \ No newline at end of file From f304437ed8959bf3d4d3c6440f64ba9afa5e8c38 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Thu, 14 May 2020 02:26:46 +0530 Subject: [PATCH 07/27] retry after secomds --- .../bucket4japp/interceptor/RateLimitInterceptor.java | 6 +++--- .../ratelimiting/bucket4japp/service/PricingPlan.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java index c983251e56..d919214983 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java @@ -18,7 +18,7 @@ public class RateLimitInterceptor implements HandlerInterceptor { private static final String HEADER_API_KEY = "X-api-key"; private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; - private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Milliseconds"; + private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Seconds"; @Autowired private PricingPlanService pricingPlanService; @@ -44,10 +44,10 @@ public class RateLimitInterceptor implements HandlerInterceptor { } else { - long waitForRefillMilli = probe.getNanosToWaitForRefill() % 1_000_000; + long waitForRefill = probe.getNanosToWaitForRefill() % 1_000_000_000; response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 - response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefillMilli)); + response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefill)); return false; } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index 85632abf0b..e8b5513e8b 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -11,7 +11,7 @@ enum PricingPlan { @Override Bandwidth getLimit() { - return Bandwidth.classic(2, Refill.intervally(2 , Duration.ofHours(1))); + return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); } }, From faf562e8210bb9e8594c02d28585cc7354ba8c48 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sat, 16 May 2020 01:34:34 +0530 Subject: [PATCH 08/27] add basic usage as test --- .../bucket4j/Bucket4jUsageTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java new file mode 100644 index 0000000000..247e493324 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java @@ -0,0 +1,82 @@ +package com.baledung.ratelimiting.bucket4j; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; + +public class Bucket4jUsageTest { + + @Test + public void givenBucketLimit_whenExceedLimit_thenConsumeReturnsFalse() { + Refill refill = Refill.intervally(10, Duration.ofMinutes(1)); + Bandwidth limit = Bandwidth.classic(10, refill); + Bucket bucket = Bucket4j.builder() + .addLimit(limit) + .build(); + + for (int i = 1; i <= 10; i++) { + assertTrue(bucket.tryConsume(1)); + } + assertFalse(bucket.tryConsume(1)); + } + + @Test + public void givenMultipletLimits_whenExceedSmallerLimit_thenConsumeReturnsFalse() { + Bucket bucket = Bucket4j.builder() + .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1)))) + .addLimit(Bandwidth.classic(5, Refill.intervally(5, Duration.ofSeconds(20)))) + .build(); + + for (int i = 1; i <= 5; i++) { + assertTrue(bucket.tryConsume(1)); + } + assertFalse(bucket.tryConsume(1)); + } + + @Test + public void givenBucketLimit_whenThrottleRequests_thenConsumeReturnsTrue() throws InterruptedException { + Refill refill = Refill.intervally(1, Duration.ofSeconds(2)); + Bandwidth limit = Bandwidth.classic(1, refill); + Bucket bucket = Bucket4j.builder() + .addLimit(limit) + .build(); + + assertTrue(bucket.tryConsume(1)); + + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + CountDownLatch latch = new CountDownLatch(1); + + executor.schedule(new AssertTryConsume(bucket, latch), 2, TimeUnit.SECONDS); + + latch.await(); + } + + static class AssertTryConsume implements Runnable { + + private Bucket bucket; + private CountDownLatch latch; + + AssertTryConsume(Bucket bucket, CountDownLatch latch) { + this.bucket = bucket; + this.latch = latch; + } + + @Override + public void run() { + assertTrue(bucket.tryConsume(1)); + latch.countDown(); + } + } +} From 8d79b7c5d6c58a6096c697d9da976c0bb9d0413b Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sat, 16 May 2020 17:20:15 +0530 Subject: [PATCH 09/27] unit test renamed --- .../{Bucket4jUsageTest.java => Bucket4jUsageUnitTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/{Bucket4jUsageTest.java => Bucket4jUsageUnitTest.java} (98%) diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java similarity index 98% rename from spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java rename to spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java index 247e493324..e6b774034e 100644 --- a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java @@ -16,7 +16,7 @@ import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket4j; import io.github.bucket4j.Refill; -public class Bucket4jUsageTest { +public class Bucket4jUsageUnitTest { @Test public void givenBucketLimit_whenExceedLimit_thenConsumeReturnsFalse() { From 6eaf9840a0305d524822928753df6415ed2b441f Mon Sep 17 00:00:00 2001 From: mikr Date: Thu, 21 May 2020 12:08:11 +0200 Subject: [PATCH 10/27] BAEL-1524 Change http into https --- persistence-modules/spring-boot-persistence-2/README.md | 2 +- persistence-modules/spring-boot-persistence/README.MD | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/persistence-modules/spring-boot-persistence-2/README.md b/persistence-modules/spring-boot-persistence-2/README.md index e0479ffe10..392218d2bf 100644 --- a/persistence-modules/spring-boot-persistence-2/README.md +++ b/persistence-modules/spring-boot-persistence-2/README.md @@ -3,6 +3,6 @@ - [Using JDBI with Spring Boot](https://www.baeldung.com/spring-boot-jdbi) - [Configuring a Tomcat Connection Pool in Spring Boot](https://www.baeldung.com/spring-boot-tomcat-connection-pool) - [Integrating Spring Boot with HSQLDB](https://www.baeldung.com/spring-boot-hsqldb) -- [List of In-Memory Databases](http://www.baeldung.com/java-in-memory-databases) +- [List of In-Memory Databases](https://www.baeldung.com/java-in-memory-databases) - [Oracle Connection Pooling With Spring](https://www.baeldung.com/spring-oracle-connection-pooling) - More articles: [[<-- prev]](../spring-boot-persistence) diff --git a/persistence-modules/spring-boot-persistence/README.MD b/persistence-modules/spring-boot-persistence/README.MD index d6ef239448..5b9fbf7b79 100644 --- a/persistence-modules/spring-boot-persistence/README.MD +++ b/persistence-modules/spring-boot-persistence/README.MD @@ -1,8 +1,8 @@ ### Relevant Articles: -- [Spring Boot with Multiple SQL Import Files](http://www.baeldung.com/spring-boot-sql-import-files) -- [Configuring Separate Spring DataSource for Tests](http://www.baeldung.com/spring-testing-separate-data-source) -- [Quick Guide on Loading Initial Data with Spring Boot](http://www.baeldung.com/spring-boot-data-sql-and-schema-sql) +- [Spring Boot with Multiple SQL Import Files](https://www.baeldung.com/spring-boot-sql-import-files) +- [Configuring Separate Spring DataSource for Tests](https://www.baeldung.com/spring-testing-separate-data-source) +- [Quick Guide on Loading Initial Data with Spring Boot](https://www.baeldung.com/spring-boot-data-sql-and-schema-sql) - [Configuring a DataSource Programmatically in Spring Boot](https://www.baeldung.com/spring-boot-configure-data-source-programmatic) - [Resolving “Failed to Configure a DataSource” Error](https://www.baeldung.com/spring-boot-failed-to-configure-data-source) - [Hibernate Field Naming with Spring Boot](https://www.baeldung.com/hibernate-field-naming-spring-boot) From dd0a8e2d11ff2d80acb22c4848c86d6f0cae247b Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Thu, 21 May 2020 17:36:25 +0530 Subject: [PATCH 11/27] add tests --- .../interceptor/RateLimitInterceptor.java | 6 +- .../bucket4japp/service/PricingPlan.java | 42 ++++++------- ...4jBootStarterRateLimitIntegrationTest.java | 63 +++++++++++++++++++ .../Bucket4jRateLimitIntegrationTest.java | 62 ++++++++++++++++++ .../bucket4japp}/Bucket4jUsageUnitTest.java | 2 +- .../PricingPlanServiceUnitTest.java | 36 +++++++++++ 6 files changed, 184 insertions(+), 27 deletions(-) create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java rename spring-boot-modules/spring-boot-libraries/src/test/java/com/{baledung/ratelimiting/bucket4j => baeldung/ratelimiting/bucket4japp}/Bucket4jUsageUnitTest.java (98%) create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java index d919214983..8a18d6c2b5 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java @@ -5,6 +5,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -44,10 +45,11 @@ public class RateLimitInterceptor implements HandlerInterceptor { } else { - long waitForRefill = probe.getNanosToWaitForRefill() % 1_000_000_000; + long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; - response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 + response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefill)); + response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 return false; } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index e8b5513e8b..2f225a83aa 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -5,34 +5,28 @@ import java.time.Duration; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Refill; -enum PricingPlan { +public enum PricingPlan { - FREE { + FREE(20), - @Override - Bandwidth getLimit() { - return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); - } - }, + BASIC(40), - BASIC { - - @Override - Bandwidth getLimit() { - return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1))); - } - }, - - PROFESSIONAL { - - @Override - Bandwidth getLimit() { - return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1))); - } - }; - - abstract Bandwidth getLimit(); + PROFESSIONAL(100);; + private int bucketCapacity; + + private PricingPlan(int bucketCapacity) { + this.bucketCapacity = bucketCapacity; + } + + Bandwidth getLimit() { + return Bandwidth.classic(bucketCapacity, Refill.intervally(bucketCapacity, Duration.ofHours(1))); + } + + public int bucketCapacity() { + return bucketCapacity; + } + static PricingPlan resolvePlanFromApiKey(String apiKey) { if (apiKey == null || apiKey.isEmpty()) { return FREE; diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java new file mode 100644 index 0000000000..d93e61988b --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java @@ -0,0 +1,63 @@ +package com.baeldung.ratelimiting.bootstarterapp; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; + +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlan; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Bucket4jRateLimitApp.class) +@TestPropertySource(properties = "spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml") +@AutoConfigureMockMvc +public class Bucket4jBootStarterRateLimitIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void givenTriangleAreaCalculator_whenRequestsWithinRateLimit_thenAccepted() throws Exception { + + RequestBuilder request = post("/api/v1/area/triangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"height\": 8, \"base\": 10 }") + .header("X-api-key", "FX001-UBSZ5YRYQ"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Rate-Limit-Remaining")) + .andExpect(jsonPath("$.shape", equalTo("triangle"))) + .andExpect(jsonPath("$.area", equalTo(40d))); + } + } + + @Test + public void givenTriangleAreaCalculator_whenRequestRateLimitTriggered_thenRejected() throws Exception { + + RequestBuilder request = post("/api/v1/area/triangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"height\": 8, \"base\": 10 }") + .header("X-api-key", "FX001-ZBSY6YSLP"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request); // exhaust limit + } + + mockMvc.perform(request) + .andExpect(status().isTooManyRequests()) + .andExpect(jsonPath("$.message", equalTo("You have exhausted your API Request Quota"))) + .andExpect(header().exists("X-Rate-Limit-Retry-After-Seconds")); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java new file mode 100644 index 0000000000..b410b7b2c5 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java @@ -0,0 +1,62 @@ +package com.baeldung.ratelimiting.bucket4japp; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; + +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlan; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Bucket4jRateLimitApp.class) +@AutoConfigureMockMvc +public class Bucket4jRateLimitIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void givenRectangleAreaCalculator_whenRequestsWithinRateLimit_thenAccepted() throws Exception { + + RequestBuilder request = post("/api/v1/area/rectangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"length\": 12, \"width\": 10 }") + .header("X-api-key", "FX001-UBSZ5YRYQ"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Rate-Limit-Remaining")) + .andExpect(jsonPath("$.shape", equalTo("rectangle"))) + .andExpect(jsonPath("$.area", equalTo(120d))); + } + } + + @Test + public void givenReactangleAreaCalculator_whenRequestRateLimitTriggered_thenRejected() throws Exception { + + RequestBuilder request = post("/api/v1/area/rectangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"length\": 12, \"width\": 10 }") + .header("X-api-key", "FX001-ZBSY6YSLP"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request); // exhaust limit + } + + mockMvc.perform(request) + .andExpect(status().isTooManyRequests()) + .andExpect(status().reason("You have exhausted your API Request Quota")) + .andExpect(header().exists("X-Rate-Limit-Retry-After-Seconds")); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java similarity index 98% rename from spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java rename to spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java index e6b774034e..fbf63ba403 100644 --- a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java @@ -1,4 +1,4 @@ -package com.baledung.ratelimiting.bucket4j; +package com.baeldung.ratelimiting.bucket4japp; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java new file mode 100644 index 0000000000..325b898779 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java @@ -0,0 +1,36 @@ +package com.baeldung.ratelimiting.bucket4japp; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlan; +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlanService; + +import io.github.bucket4j.Bucket; + +public class PricingPlanServiceUnitTest { + + private PricingPlanService service = new PricingPlanService(); + + @Test + public void givenAPIKey_whenFreePlan_thenReturnFreePlanBucket() { + Bucket bucket = service.resolveBucket("FX001-UBSZ5YRYQ"); + + assertEquals(PricingPlan.FREE.bucketCapacity(), bucket.getAvailableTokens()); + } + + @Test + public void givenAPIKey_whenBasiclan_thenReturnBasicPlanBucket() { + Bucket bucket = service.resolveBucket("BX001-MBSZ5YRYP"); + + assertEquals(PricingPlan.BASIC.bucketCapacity(), bucket.getAvailableTokens()); + } + + @Test + public void givenAPIKey_whenProfessionalPlan_thenReturnProfessionalPlanBucket() { + Bucket bucket = service.resolveBucket("PX001-NBSZ5YRYY"); + + assertEquals(PricingPlan.PROFESSIONAL.bucketCapacity(), bucket.getAvailableTokens()); + } +} From 8ecf039b6b539ae626c93e8e4aa7b30b13b55e98 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Thu, 21 May 2020 22:53:20 +0530 Subject: [PATCH 12/27] remove unused import --- .../bucket4japp/Bucket4jRateLimitIntegrationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java index b410b7b2c5..20f57a7021 100644 --- a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java @@ -11,7 +11,6 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; From d76966dab73c71c540d56262c9d0b58a60748ab5 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sun, 24 May 2020 16:53:24 +0530 Subject: [PATCH 13/27] review comments - indentation in pom --- .../spring-boot-libraries/pom.xml | 244 +++++++++--------- .../bucket4japp/service/PricingPlan.java | 2 +- 2 files changed, 123 insertions(+), 123 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index 36b9ec17c9..da4e979c17 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -88,34 +88,34 @@ ${zxing.version} - - com.github.vladimir-bukhtoyarov - bucket4j-core - ${bucket4j.version} - - - com.giffing.bucket4j.spring.boot.starter - bucket4j-spring-boot-starter - ${bucket4j-spring-boot-starter.version} - - - org.springframework.boot - spring-boot-starter-cache - - - javax.cache - cache-api - - - com.github.ben-manes.caffeine - caffeine - ${caffeine.version} - - - com.github.ben-manes.caffeine - jcache - ${caffeine.version} - + + com.github.vladimir-bukhtoyarov + bucket4j-core + ${bucket4j.version} + + + com.giffing.bucket4j.spring.boot.starter + bucket4j-spring-boot-starter + ${bucket4j-spring-boot-starter.version} + + + org.springframework.boot + spring-boot-starter-cache + + + javax.cache + cache-api + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.github.ben-manes.caffeine + jcache + ${caffeine.version} + @@ -125,112 +125,112 @@ - - spring-boot-libraries - - - src/main/resources - true - - + + spring-boot-libraries + + + src/main/resources + true + + - + - - org.apache.maven.plugins - maven-war-plugin - + + org.apache.maven.plugins + maven-war-plugin + - - pl.project13.maven - git-commit-id-plugin - ${git-commit-id-plugin.version} - - - get-the-git-infos - - revision - - initialize - - - validate-the-git-infos - - validateRevision - - package - - - - true - ${project.build.outputDirectory}/git.properties - - + + pl.project13.maven + git-commit-id-plugin + ${git-commit-id-plugin.version} + + + get-the-git-infos + + revision + + initialize + + + validate-the-git-infos + + validateRevision + + package + + + + true + ${project.build.outputDirectory}/git.properties + + - - - autoconfiguration - - - - org.apache.maven.plugins - maven-surefire-plugin - - - integration-test - - test - - - - **/*LiveTest.java - **/*IntegrationTest.java - **/*IntTest.java - - - **/AutoconfigurationTest.java - - - - - - - json - - - - - - - + + + autoconfiguration + + + + org.apache.maven.plugins + maven-surefire-plugin + + + integration-test + + test + + + + **/*LiveTest.java + **/*IntegrationTest.java + **/*IntTest.java + + + **/AutoconfigurationTest.java + + + + + + + json + + + + + + + - - + + com.baeldung.intro.App 8.5.11 2.4.1.Final 1.9.0 2.0.0 - 5.0.2 - 5.0.2 - 5.2.4 - 18.0 - 2.2.4 - 2.3.2 - 0.23.0 - 1.4.200 - 2.1.0 - 1.5-beta1 - 2.1 - 2.6.0 - 3.3.0 - 4.10.0 - 0.2.0 - 2.8.2 - + 5.0.2 + 5.0.2 + 5.2.4 + 18.0 + 2.2.4 + 2.3.2 + 0.23.0 + 1.4.200 + 2.1.0 + 1.5-beta1 + 2.1 + 2.6.0 + 3.3.0 + 4.10.0 + 0.2.0 + 2.8.2 + - + \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index 2f225a83aa..27c30ba3a0 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -11,7 +11,7 @@ public enum PricingPlan { BASIC(40), - PROFESSIONAL(100);; + PROFESSIONAL(100); private int bucketCapacity; From b7c9e649825fee3377c07cf9cdbbec77e591e84c Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sun, 24 May 2020 16:55:06 +0530 Subject: [PATCH 14/27] review comments - indentation in pom --- spring-boot-modules/spring-boot-libraries/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index da4e979c17..4e7fd205d5 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -233,4 +233,4 @@ 2.8.2 - \ No newline at end of file + From e839798f0da0d2e598f64c5d20bba6973428ba41 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sun, 24 May 2020 16:56:57 +0530 Subject: [PATCH 15/27] review comments - indentation in pom --- spring-boot-modules/spring-boot-libraries/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index 4e7fd205d5..189eb4cf1a 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -87,7 +87,7 @@ javase ${zxing.version} - + com.github.vladimir-bukhtoyarov bucket4j-core From 3c455c5c56e13f3905834785d9a89c8cdf09dcc8 Mon Sep 17 00:00:00 2001 From: Krzysztof Woyke Date: Mon, 25 May 2020 13:03:25 +0200 Subject: [PATCH 16/27] JAVA-1635: Remove unnecessary dependencies and overridden spring-boot.version --- .../spring-boot-persistence-h2/pom.xml | 12 +++++------- .../src/main/resources/application.properties | 3 +-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/persistence-modules/spring-boot-persistence-h2/pom.xml b/persistence-modules/spring-boot-persistence-h2/pom.xml index 777bc6cb2d..7070b5e674 100644 --- a/persistence-modules/spring-boot-persistence-h2/pom.xml +++ b/persistence-modules/spring-boot-persistence-h2/pom.xml @@ -35,23 +35,21 @@ ${lombok.version} compile - - org.hibernate - hibernate-core - ${hibernate.version} - com.vladmihalcea db-util ${db-util.version} + + net.bytebuddy + byte-buddy + ${byte-buddy.version} + com.baeldung.h2db.demo.server.SpringBootApp - 2.0.4.RELEASE - 5.3.11.Final 1.0.4 diff --git a/persistence-modules/spring-boot-persistence-h2/src/main/resources/application.properties b/persistence-modules/spring-boot-persistence-h2/src/main/resources/application.properties index ed1ffc63c3..0466eaac79 100644 --- a/persistence-modules/spring-boot-persistence-h2/src/main/resources/application.properties +++ b/persistence-modules/spring-boot-persistence-h2/src/main/resources/application.properties @@ -8,5 +8,4 @@ spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.validator.apply_to_ddl=false #spring.jpa.properties.hibernate.check_nullability=true spring.h2.console.enabled=true -spring.h2.console.path=/h2-console -debug=true \ No newline at end of file +spring.h2.console.path=/h2-console \ No newline at end of file From 7b53c3cb55b110470ae9e4528aef3a165fa1a873 Mon Sep 17 00:00:00 2001 From: Anshul Bansal Date: Tue, 26 May 2020 18:58:17 +0300 Subject: [PATCH 17/27] BAEL-3996_Spring_Security_with_Auth0 (#9300) * BAEL-3996 - Spring Security with Auth0 * BAEL-3996 - Spring Security with Auth0 code fixes * BAEL-3996 - Spring Security with Auth0 * BAEL-3996 - removed spaces * BAEL-3996 - RequestMapping is updated to GetMapping --- spring-security-modules/pom.xml | 1 + .../spring-security-auth0/pom.xml | 75 ++++++++++++ .../java/com/baeldung/auth0/Application.java | 13 ++ .../java/com/baeldung/auth0/AuthConfig.java | 114 ++++++++++++++++++ .../auth0/controller/AuthController.java | 77 ++++++++++++ .../auth0/controller/HomeController.java | 37 ++++++ .../auth0/controller/LogoutController.java | 35 ++++++ .../auth0/controller/UserController.java | 57 +++++++++ .../baeldung/auth0/service/ApiService.java | 44 +++++++ .../src/main/resources/application.properties | 7 ++ 10 files changed, 460 insertions(+) create mode 100644 spring-security-modules/spring-security-auth0/pom.xml create mode 100644 spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/Application.java create mode 100644 spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/AuthConfig.java create mode 100644 spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/AuthController.java create mode 100644 spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/HomeController.java create mode 100755 spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/LogoutController.java create mode 100644 spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/UserController.java create mode 100644 spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/service/ApiService.java create mode 100644 spring-security-modules/spring-security-auth0/src/main/resources/application.properties diff --git a/spring-security-modules/pom.xml b/spring-security-modules/pom.xml index 7ce33dd3e3..60a662781f 100644 --- a/spring-security-modules/pom.xml +++ b/spring-security-modules/pom.xml @@ -15,6 +15,7 @@ spring-security-acl + spring-security-auth0 spring-security-angular/server spring-security-cache-control spring-security-core diff --git a/spring-security-modules/spring-security-auth0/pom.xml b/spring-security-modules/spring-security-auth0/pom.xml new file mode 100644 index 0000000000..0bd879a40b --- /dev/null +++ b/spring-security-modules/spring-security-auth0/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + spring-security-auth0 + 1.0-SNAPSHOT + spring-security-auth0 + war + + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../../parent-boot-2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-core + + + org.springframework.security + spring-security-oauth2-resource-server + + + com.auth0 + mvc-auth-commons + ${mvc-auth-commons.version} + + + org.json + json + ${json.version} + + + + + spring-security-auth0 + + + src/main/resources + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + repackage + + + + + + + + + 20190722 + 1.2.0 + + \ No newline at end of file diff --git a/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/Application.java b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/Application.java new file mode 100644 index 0000000000..42f8d946b5 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/Application.java @@ -0,0 +1,13 @@ +package com.baeldung.auth0; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/AuthConfig.java b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/AuthConfig.java new file mode 100644 index 0000000000..69cf8b3071 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/AuthConfig.java @@ -0,0 +1,114 @@ +package com.baeldung.auth0; + +import java.io.UnsupportedEncodingException; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import com.auth0.AuthenticationController; +import com.baeldung.auth0.controller.LogoutController; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.JwkProviderBuilder; + +@Configuration +@EnableWebSecurity +public class AuthConfig extends WebSecurityConfigurerAdapter { + + @Value(value = "${com.auth0.domain}") + private String domain; + + @Value(value = "${com.auth0.clientId}") + private String clientId; + + @Value(value = "${com.auth0.clientSecret}") + private String clientSecret; + + @Value(value = "${com.auth0.managementApi.clientId}") + private String managementApiClientId; + + @Value(value = "${com.auth0.managementApi.clientSecret}") + private String managementApiClientSecret; + + @Value(value = "${com.auth0.managementApi.grantType}") + private String grantType; + + @Bean + public LogoutSuccessHandler logoutSuccessHandler() { + return new LogoutController(); + } + + @Bean + public AuthenticationController authenticationController() throws UnsupportedEncodingException { + JwkProvider jwkProvider = new JwkProviderBuilder(domain).build(); + return AuthenticationController.newBuilder(domain, clientId, clientSecret) + .withJwkProvider(jwkProvider) + .build(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable(); + http + .authorizeRequests() + .antMatchers("/callback", "/login", "/").permitAll() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/login") + .and() + .logout().logoutSuccessHandler(logoutSuccessHandler()).permitAll(); + } + + public String getDomain() { + return domain; + } + + public String getClientId() { + return clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public String getManagementApiClientId() { + return managementApiClientId; + } + + public String getManagementApiClientSecret() { + return managementApiClientSecret; + } + + public String getGrantType() { + return grantType; + } + + public String getUserInfoUrl() { + return "https://" + getDomain() + "/userinfo"; + } + + public String getUsersUrl() { + return "https://" + getDomain() + "/api/v2/users"; + } + + public String getUsersByEmailUrl() { + return "https://" + getDomain() + "/api/v2/users-by-email?email="; + } + + public String getLogoutUrl() { + return "https://" + getDomain() +"/v2/logout"; + } + + public String getContextPath(HttpServletRequest request) { + String path = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort(); + return path; + } +} diff --git a/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/AuthController.java b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/AuthController.java new file mode 100644 index 0000000000..48d09db155 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/AuthController.java @@ -0,0 +1,77 @@ +package com.baeldung.auth0.controller; + +import java.io.IOException; +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.client.RestTemplate; + +import com.auth0.AuthenticationController; +import com.auth0.IdentityVerificationException; +import com.auth0.Tokens; +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.baeldung.auth0.AuthConfig; + +@Controller +public class AuthController { + + @Autowired + private AuthenticationController authenticationController; + + @Autowired + private AuthConfig config; + + private static final String AUTH0_TOKEN_URL = "https://dev-example.auth0.com/oauth/token"; + + @GetMapping(value = "/login") + protected void login(HttpServletRequest request, HttpServletResponse response) throws IOException { + String redirectUri = config.getContextPath(request) + "/callback"; + String authorizeUrl = authenticationController.buildAuthorizeUrl(request, response, redirectUri) + .withScope("openid email") + .build(); + response.sendRedirect(authorizeUrl); + } + + @GetMapping(value="/callback") + public void callback(HttpServletRequest request, HttpServletResponse response) throws IOException, IdentityVerificationException { + Tokens tokens = authenticationController.handle(request, response); + + DecodedJWT jwt = JWT.decode(tokens.getIdToken()); + TestingAuthenticationToken authToken2 = new TestingAuthenticationToken(jwt.getSubject(), jwt.getToken()); + authToken2.setAuthenticated(true); + + SecurityContextHolder.getContext().setAuthentication(authToken2); + response.sendRedirect(config.getContextPath(request) + "/"); + } + + public String getManagementApiToken() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + JSONObject requestBody = new JSONObject(); + requestBody.put("client_id", config.getManagementApiClientId()); + requestBody.put("client_secret", config.getManagementApiClientSecret()); + requestBody.put("audience", "https://dev-example.auth0.com/api/v2/"); + requestBody.put("grant_type", config.getGrantType()); + + HttpEntity request = new HttpEntity(requestBody.toString(), headers); + + RestTemplate restTemplate = new RestTemplate(); + HashMap result = restTemplate.postForObject(AUTH0_TOKEN_URL, request, HashMap.class); + + return result.get("access_token"); + } + +} diff --git a/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/HomeController.java b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/HomeController.java new file mode 100644 index 0000000000..8a4e650846 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/HomeController.java @@ -0,0 +1,37 @@ +package com.baeldung.auth0.controller; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; + +@Controller +public class HomeController { + + @GetMapping(value = "/") + @ResponseBody + public String home(HttpServletRequest request, HttpServletResponse response, final Authentication authentication) throws IOException { + + if (authentication!= null && authentication instanceof TestingAuthenticationToken) { + TestingAuthenticationToken token = (TestingAuthenticationToken) authentication; + + DecodedJWT jwt = JWT.decode(token.getCredentials().toString()); + String email = jwt.getClaims().get("email").asString(); + + return "Welcome, " + email + "!"; + } else { + response.sendRedirect("http://localhost:8080/login"); + return null; + } + } + +} diff --git a/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/LogoutController.java b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/LogoutController.java new file mode 100755 index 0000000000..d508fe2c44 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/LogoutController.java @@ -0,0 +1,35 @@ +package com.baeldung.auth0.controller; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Controller; + +import com.baeldung.auth0.AuthConfig; + +@Controller +public class LogoutController implements LogoutSuccessHandler { + + @Autowired + private AuthConfig config; + + @Override + public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) { + if (req.getSession() != null) { + req.getSession().invalidate(); + } + String returnTo = config.getContextPath(req); + String logoutUrl = config.getLogoutUrl() + "?client_id=" + config.getClientId() + "&returnTo=" +returnTo; + try { + res.sendRedirect(logoutUrl); + } catch(IOException e){ + e.printStackTrace(); + } + } + +} diff --git a/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/UserController.java b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/UserController.java new file mode 100644 index 0000000000..86601a06d3 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/controller/UserController.java @@ -0,0 +1,57 @@ +package com.baeldung.auth0.controller; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.auth0.IdentityVerificationException; +import com.baeldung.auth0.AuthConfig; +import com.baeldung.auth0.service.ApiService; + +@Controller +public class UserController { + + @Autowired + private ApiService apiService; + + @Autowired + private AuthConfig config; + + @GetMapping(value="/users") + @ResponseBody + public ResponseEntity users(HttpServletRequest request, HttpServletResponse response) throws IOException, IdentityVerificationException { + ResponseEntity result = apiService.getCall(config.getUsersUrl()); + return result; + } + + @GetMapping(value = "/userByEmail") + @ResponseBody + public ResponseEntity userByEmail(HttpServletResponse response, @RequestParam String email) { + ResponseEntity result = apiService.getCall(config.getUsersByEmailUrl()+email); + return result; + } + + @GetMapping(value = "/createUser") + @ResponseBody + public ResponseEntity createUser(HttpServletResponse response) { + JSONObject request = new JSONObject(); + request.put("email", "norman.lewis@email.com"); + request.put("given_name", "Norman"); + request.put("family_name", "Lewis"); + request.put("connection", "Username-Password-Authentication"); + request.put("password", "Pa33w0rd"); + + ResponseEntity result = apiService.postCall(config.getUsersUrl(), request.toString()); + return result; + } + +} diff --git a/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/service/ApiService.java b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/service/ApiService.java new file mode 100644 index 0000000000..0d8263ae19 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/java/com/baeldung/auth0/service/ApiService.java @@ -0,0 +1,44 @@ +package com.baeldung.auth0.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.baeldung.auth0.controller.AuthController; + +@Service +public class ApiService { + + @Autowired + private AuthController controller; + + public ResponseEntity getCall(String url) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer "+controller.getManagementApiToken()); + + HttpEntity entity = new HttpEntity(headers); + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity result = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + return result; + } + + public ResponseEntity postCall(String url, String requestBody) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer "+controller.getManagementApiToken()); + + HttpEntity request = new HttpEntity(requestBody, headers); + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity result = restTemplate.postForEntity(url, request, String.class); + + return result; + } + +} diff --git a/spring-security-modules/spring-security-auth0/src/main/resources/application.properties b/spring-security-modules/spring-security-auth0/src/main/resources/application.properties new file mode 100644 index 0000000000..45492c5c00 --- /dev/null +++ b/spring-security-modules/spring-security-auth0/src/main/resources/application.properties @@ -0,0 +1,7 @@ +com.auth0.domain: dev-example.auth0.com +com.auth0.clientId: exampleClientId +com.auth0.clientSecret: exampleClientSecret + +com.auth0.managementApi.clientId: exampleManagementApiClientId +com.auth0.managementApi.clientSecret: exampleManagementApiClientSecret +com.auth0.managementApi.grantType: client_credentials \ No newline at end of file From 771b415b8708bc6282b16b71cadb9706a88a9276 Mon Sep 17 00:00:00 2001 From: Krzysztof Woyke Date: Wed, 27 May 2020 13:48:16 +0200 Subject: [PATCH 18/27] JAVA-1641: Remove overriden spring-boot.version property --- spring-boot-modules/spring-boot-mvc-birt/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-mvc-birt/pom.xml b/spring-boot-modules/spring-boot-mvc-birt/pom.xml index f65b851f30..0e8e231a84 100644 --- a/spring-boot-modules/spring-boot-mvc-birt/pom.xml +++ b/spring-boot-modules/spring-boot-mvc-birt/pom.xml @@ -75,7 +75,6 @@ - 2.1.1.RELEASE com.baeldung.birt.engine.ReportEngineApplication 1.8 1.8 From 9dc7483313d0bd04a61e755c30218f1d1aaff14e Mon Sep 17 00:00:00 2001 From: kwoyke Date: Wed, 27 May 2020 17:27:11 +0200 Subject: [PATCH 19/27] BAEL-3448: Downgrade logback version & fix config (#9354) --- jee-kotlin/pom.xml | 3 ++- jee-kotlin/src/main/resources/META-INF/persistence.xml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jee-kotlin/pom.xml b/jee-kotlin/pom.xml index 9191885bd4..14c47a205e 100644 --- a/jee-kotlin/pom.xml +++ b/jee-kotlin/pom.xml @@ -278,9 +278,10 @@ 2.0.1.Final 1.0.0.Alpha4 + 1.1.7 + 3.8.0.Final 3.1.3 - diff --git a/jee-kotlin/src/main/resources/META-INF/persistence.xml b/jee-kotlin/src/main/resources/META-INF/persistence.xml index daac86868b..0093792810 100644 --- a/jee-kotlin/src/main/resources/META-INF/persistence.xml +++ b/jee-kotlin/src/main/resources/META-INF/persistence.xml @@ -7,7 +7,7 @@ java:jboss/datasources/ExampleDS - com.enpy.entity.Student + com.baeldung.jeekotlin.entity.Student From 75b741954514f58b1e1359b331d58648fdd08147 Mon Sep 17 00:00:00 2001 From: kwoyke Date: Wed, 27 May 2020 17:33:29 +0200 Subject: [PATCH 20/27] KTLN-149: Get rid of the invalid buildString example (#9373) --- .../com/baeldung/stringcomparison/StringComparisonTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-kotlin-modules/core-kotlin-strings/src/test/kotlin/com/baeldung/stringcomparison/StringComparisonTest.kt b/core-kotlin-modules/core-kotlin-strings/src/test/kotlin/com/baeldung/stringcomparison/StringComparisonTest.kt index 9528f62df5..49ff798faa 100644 --- a/core-kotlin-modules/core-kotlin-strings/src/test/kotlin/com/baeldung/stringcomparison/StringComparisonTest.kt +++ b/core-kotlin-modules/core-kotlin-strings/src/test/kotlin/com/baeldung/stringcomparison/StringComparisonTest.kt @@ -19,9 +19,9 @@ class StringComparisonUnitTest { fun `compare using referential equals operator`() { val first = "kotlin" val second = "kotlin" - val copyOfFirst = buildString { "kotlin" } + val third = String("kotlin".toCharArray()) assertTrue { first === second } - assertFalse { first === copyOfFirst } + assertFalse { first === third } } @Test From 35822806087c704f6c3d3e7d743247285032b5d1 Mon Sep 17 00:00:00 2001 From: Ali Dehghani Date: Wed, 27 May 2020 22:25:08 +0430 Subject: [PATCH 21/27] Fixed the Datasource Assertion Issue --- .../SpringBootTomcatConnectionPoolIntegrationTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/persistence-modules/spring-boot-persistence-2/src/test/java/com/baeldung/tomcatconnectionpool/test/application/SpringBootTomcatConnectionPoolIntegrationTest.java b/persistence-modules/spring-boot-persistence-2/src/test/java/com/baeldung/tomcatconnectionpool/test/application/SpringBootTomcatConnectionPoolIntegrationTest.java index 5400d76fbe..4422c27150 100644 --- a/persistence-modules/spring-boot-persistence-2/src/test/java/com/baeldung/tomcatconnectionpool/test/application/SpringBootTomcatConnectionPoolIntegrationTest.java +++ b/persistence-modules/spring-boot-persistence-2/src/test/java/com/baeldung/tomcatconnectionpool/test/application/SpringBootTomcatConnectionPoolIntegrationTest.java @@ -4,6 +4,7 @@ import javax.sql.DataSource; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import com.baeldung.tomcatconnectionpool.application.SpringBootConsoleApplication; @@ -13,6 +14,7 @@ import org.springframework.boot.test.context.SpringBootTest; @RunWith(SpringRunner.class) @SpringBootTest(classes = {SpringBootConsoleApplication.class}) +@TestPropertySource(properties = "spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource") public class SpringBootTomcatConnectionPoolIntegrationTest { @Autowired From cd293d0cb634752400ec6398ba168707c006451c Mon Sep 17 00:00:00 2001 From: kwoyke Date: Wed, 27 May 2020 22:16:44 +0200 Subject: [PATCH 22/27] BAEL-3448: Change default http port (#9380) --- jee-kotlin/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/jee-kotlin/pom.xml b/jee-kotlin/pom.xml index 14c47a205e..45d5d8ece1 100644 --- a/jee-kotlin/pom.xml +++ b/jee-kotlin/pom.xml @@ -221,6 +221,7 @@ org.jboss.logmanager.LogManager ${project.basedir}/target/wildfly-${wildfly.version} + 8756 ${project.basedir}/target/wildfly-${wildfly.version}/modules false From 547a57c3c836f5293784999d71b2d0ab734d6c65 Mon Sep 17 00:00:00 2001 From: andrebrowne <42154231+andrebrowne@users.noreply.github.com> Date: Wed, 27 May 2020 20:42:38 -0400 Subject: [PATCH 23/27] BAEL-3341 Rename test dependent on database --- ...ateTypesIntegrationTest.java => HibernateTypesLiveTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename persistence-modules/hibernate-libraries/src/test/java/com/baeldung/hibernate/types/{HibernateTypesIntegrationTest.java => HibernateTypesLiveTest.java} (99%) diff --git a/persistence-modules/hibernate-libraries/src/test/java/com/baeldung/hibernate/types/HibernateTypesIntegrationTest.java b/persistence-modules/hibernate-libraries/src/test/java/com/baeldung/hibernate/types/HibernateTypesLiveTest.java similarity index 99% rename from persistence-modules/hibernate-libraries/src/test/java/com/baeldung/hibernate/types/HibernateTypesIntegrationTest.java rename to persistence-modules/hibernate-libraries/src/test/java/com/baeldung/hibernate/types/HibernateTypesLiveTest.java index 9d7479e77d..4b551386ad 100644 --- a/persistence-modules/hibernate-libraries/src/test/java/com/baeldung/hibernate/types/HibernateTypesIntegrationTest.java +++ b/persistence-modules/hibernate-libraries/src/test/java/com/baeldung/hibernate/types/HibernateTypesLiveTest.java @@ -13,7 +13,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest -public class HibernateTypesIntegrationTest { +public class HibernateTypesLiveTest { @Autowired AlbumRepository albumRepository; From cde01486d750e118829b6e26de68758f858fd60c Mon Sep 17 00:00:00 2001 From: Ali Dehghani Date: Thu, 28 May 2020 06:41:53 +0430 Subject: [PATCH 24/27] Changing to a More Stable Currency! --- .../currencies/CurrenciesControllerIntegrationTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-thymeleaf-3/src/test/java/com/baeldung/thymeleaf/currencies/CurrenciesControllerIntegrationTest.java b/spring-thymeleaf-3/src/test/java/com/baeldung/thymeleaf/currencies/CurrenciesControllerIntegrationTest.java index 02bf8a9ee0..c1e3cf7458 100644 --- a/spring-thymeleaf-3/src/test/java/com/baeldung/thymeleaf/currencies/CurrenciesControllerIntegrationTest.java +++ b/spring-thymeleaf-3/src/test/java/com/baeldung/thymeleaf/currencies/CurrenciesControllerIntegrationTest.java @@ -27,7 +27,7 @@ public class CurrenciesControllerIntegrationTest { .header("Accept-Language", "es-ES") .param("amount", "10032.5")) .andExpect(status().isOk()) - .andExpect(content().string(containsString("10.032,50 €"))); + .andExpect(content().string(containsString("10.032,50"))); } @Test @@ -42,10 +42,10 @@ public class CurrenciesControllerIntegrationTest { @Test public void whenCallCurrencyWithRomanianLocaleWithArrays_ThenReturnLocaleCurrencies() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/currency") - .header("Accept-Language", "ro-RO") + .header("Accept-Language", "en-GB") .param("amountList", "10", "20", "30")) .andExpect(status().isOk()) - .andExpect(content().string(containsString("10,00 RON, 20,00 RON, 30,00 RON"))); + .andExpect(content().string(containsString("£10.00, £20.00, £30.00"))); } @Test From 6cd7bfba80b1ec9927099c93f2c85155fff17580 Mon Sep 17 00:00:00 2001 From: vatsalgosar Date: Thu, 28 May 2020 22:07:23 +0530 Subject: [PATCH 25/27] BAEL-3987 (#9139) * BAEL-3987 - Cast an int to an enum value * refactoring * code refactored * BAEL-3987 - Updated code snippets * bAEL-3987 - Fixed indentation --- .../com/baeldung/inttoenum/PizzaStatus.java | 36 +++++++++++++++++++ .../baeldung/inttoenum/IntToEnumUnitTest.java | 27 ++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 core-java-modules/core-java-lang-2/src/main/java/com/baeldung/inttoenum/PizzaStatus.java create mode 100644 core-java-modules/core-java-lang-2/src/test/java/com/baeldung/inttoenum/IntToEnumUnitTest.java diff --git a/core-java-modules/core-java-lang-2/src/main/java/com/baeldung/inttoenum/PizzaStatus.java b/core-java-modules/core-java-lang-2/src/main/java/com/baeldung/inttoenum/PizzaStatus.java new file mode 100644 index 0000000000..8d7c626521 --- /dev/null +++ b/core-java-modules/core-java-lang-2/src/main/java/com/baeldung/inttoenum/PizzaStatus.java @@ -0,0 +1,36 @@ +package com.baeldung.inttoenum; + +import java.util.HashMap; +import java.util.Map; + +public enum PizzaStatus { + ORDERED(5), + READY(2), + DELIVERED(0); + + private int timeToDelivery; + + PizzaStatus(int timeToDelivery) { + this.timeToDelivery = timeToDelivery; + } + + public int getTimeToDelivery() { + return timeToDelivery; + } + + private static Map timeToDeliveryToEnumValuesMapping = new HashMap<>(); + + static { + PizzaStatus[] pizzaStatuses = PizzaStatus.values(); + for (int pizzaStatusIndex = 0; pizzaStatusIndex < pizzaStatuses.length; pizzaStatusIndex++) { + timeToDeliveryToEnumValuesMapping.put( + pizzaStatuses[pizzaStatusIndex].getTimeToDelivery(), + pizzaStatuses[pizzaStatusIndex] + ); + } + } + + public static PizzaStatus castIntToEnum(int timeToDelivery) { + return timeToDeliveryToEnumValuesMapping.get(timeToDelivery); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-lang-2/src/test/java/com/baeldung/inttoenum/IntToEnumUnitTest.java b/core-java-modules/core-java-lang-2/src/test/java/com/baeldung/inttoenum/IntToEnumUnitTest.java new file mode 100644 index 0000000000..876c230827 --- /dev/null +++ b/core-java-modules/core-java-lang-2/src/test/java/com/baeldung/inttoenum/IntToEnumUnitTest.java @@ -0,0 +1,27 @@ +package com.baeldung.inttoenum; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IntToEnumUnitTest { + + @Test + public void whenIntToEnumUsingValuesMethod_thenReturnEnumObject() { + int timeToDeliveryForOrderedPizzaStatus = 5; + PizzaStatus[] pizzaStatuses = PizzaStatus.values(); + PizzaStatus pizzaOrderedStatus = null; + for (int pizzaStatusIndex = 0; pizzaStatusIndex < pizzaStatuses.length; pizzaStatusIndex++) { + if (pizzaStatuses[pizzaStatusIndex].getTimeToDelivery() == timeToDeliveryForOrderedPizzaStatus) { + pizzaOrderedStatus = pizzaStatuses[pizzaStatusIndex]; + } + } + assertEquals(pizzaOrderedStatus, PizzaStatus.ORDERED); + } + + @Test + public void whenIntToEnumUsingMap_thenReturnEnumObject() { + int timeToDeliveryForOrderedPizzaStatus = 5; + assertEquals(PizzaStatus.castIntToEnum(timeToDeliveryForOrderedPizzaStatus), PizzaStatus.ORDERED); + } +} \ No newline at end of file From 135c3160ac0016ecb0244a9c20b6f065fcab632a Mon Sep 17 00:00:00 2001 From: Mark Thomas Date: Thu, 28 May 2020 15:34:00 -0500 Subject: [PATCH 26/27] BAEL-3768 - spatialguru.net@gmail.com (#9385) * Add implementation of BeanPostProcessor and BeanFactoryPostProcessor for ticket * Continue processing beans if annotation not found on one or more * Add integration test * Simplify code --- spring-core-4/pom.xml | 24 ++++- .../postprocessor/GlobalEventBus.java | 39 +++++++++ ...GuavaEventBusBeanFactoryPostProcessor.java | 63 ++++++++++++++ .../GuavaEventBusBeanPostProcessor.java | 87 +++++++++++++++++++ .../baeldung/postprocessor/StockTrade.java | 34 ++++++++ .../postprocessor/StockTradeListener.java | 7 ++ .../postprocessor/StockTradePublisher.java | 36 ++++++++ .../baeldung/postprocessor/Subscriber.java | 21 +++++ .../PostProcessorConfiguration.java | 23 +++++ .../StockTradeIntegrationTest.java | 46 ++++++++++ 10 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 spring-core-4/src/main/java/com/baeldung/postprocessor/GlobalEventBus.java create mode 100644 spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanFactoryPostProcessor.java create mode 100644 spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanPostProcessor.java create mode 100644 spring-core-4/src/main/java/com/baeldung/postprocessor/StockTrade.java create mode 100644 spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradeListener.java create mode 100644 spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradePublisher.java create mode 100644 spring-core-4/src/main/java/com/baeldung/postprocessor/Subscriber.java create mode 100644 spring-core-4/src/test/java/com/baeldung/postprocessor/PostProcessorConfiguration.java create mode 100644 spring-core-4/src/test/java/com/baeldung/postprocessor/StockTradeIntegrationTest.java diff --git a/spring-core-4/pom.xml b/spring-core-4/pom.xml index 53f7ca6912..fbec5ea9eb 100644 --- a/spring-core-4/pom.xml +++ b/spring-core-4/pom.xml @@ -24,6 +24,16 @@ spring-core ${spring.version} + + org.springframework + spring-expression + ${spring.version} + + + com.google.guava + guava + 28.2-jre + org.springframework spring-test @@ -42,6 +52,18 @@ ${junit-jupiter.version} test + + org.awaitility + awaitility + 4.0.2 + test + + + org.assertj + assertj-core + 2.9.1 + test + @@ -60,4 +82,4 @@ 2.2.2.RELEASE - \ No newline at end of file + diff --git a/spring-core-4/src/main/java/com/baeldung/postprocessor/GlobalEventBus.java b/spring-core-4/src/main/java/com/baeldung/postprocessor/GlobalEventBus.java new file mode 100644 index 0000000000..8b95ea7c6f --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/postprocessor/GlobalEventBus.java @@ -0,0 +1,39 @@ +package com.baeldung.postprocessor; + +import com.google.common.eventbus.AsyncEventBus; +import com.google.common.eventbus.EventBus; + +import java.util.concurrent.Executors; + +@SuppressWarnings("ALL") +public final class GlobalEventBus { + + public static final String GLOBAL_EVENT_BUS_EXPRESSION = "T(com.baeldung.postprocessor.GlobalEventBus).getEventBus()"; + + private static final String IDENTIFIER = "global-event-bus"; + + private static final GlobalEventBus GLOBAL_EVENT_BUS = new GlobalEventBus(); + + private final EventBus eventBus = new AsyncEventBus(IDENTIFIER, Executors.newCachedThreadPool()); + + private GlobalEventBus() { + } + + public static GlobalEventBus getInstance() { + return GlobalEventBus.GLOBAL_EVENT_BUS; + } + + public static EventBus getEventBus() { + return GlobalEventBus.GLOBAL_EVENT_BUS.eventBus; + } + + public static void subscribe(Object obj) { + getEventBus().register(obj); + } + public static void unsubscribe(Object obj) { + getEventBus().unregister(obj); + } + public static void post(Object event) { + getEventBus().post(event); + } +} diff --git a/spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanFactoryPostProcessor.java b/spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanFactoryPostProcessor.java new file mode 100644 index 0000000000..fba31fde6a --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanFactoryPostProcessor.java @@ -0,0 +1,63 @@ +package com.baeldung.postprocessor; + +import com.google.common.eventbus.EventBus; + +import java.util.Iterator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +@SuppressWarnings("ALL") +public class GuavaEventBusBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final SpelExpressionParser expressionParser = new SpelExpressionParser(); + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + for (Iterator names = beanFactory.getBeanNamesIterator(); names.hasNext(); ) { + Object proxy = this.getTargetObject(beanFactory.getBean(names.next())); + final Subscriber annotation = AnnotationUtils.getAnnotation(proxy.getClass(), Subscriber.class); + if (annotation == null) + continue; + this.logger.info("{}: processing bean of type {} during initialization", this.getClass().getSimpleName(), + proxy.getClass().getName()); + final String annotationValue = annotation.value(); + try { + final Expression expression = this.expressionParser.parseExpression(annotationValue); + final Object value = expression.getValue(); + if (!(value instanceof EventBus)) { + this.logger.error("{}: expression {} did not evaluate to an instance of EventBus for bean of type {}", + this.getClass().getSimpleName(), annotationValue, proxy.getClass().getSimpleName()); + return; + } + final EventBus eventBus = (EventBus)value; + eventBus.register(proxy); + } catch (ExpressionException ex) { + this.logger.error("{}: unable to parse/evaluate expression {} for bean of type {}", this.getClass().getSimpleName(), + annotationValue, proxy.getClass().getName()); + } + } + } + + private Object getTargetObject(Object proxy) throws BeansException { + if (AopUtils.isJdkDynamicProxy(proxy)) { + try { + return ((Advised)proxy).getTargetSource().getTarget(); + } catch (Exception e) { + throw new FatalBeanException("Error getting target of JDK proxy", e); + } + } + return proxy; + } +} diff --git a/spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanPostProcessor.java b/spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanPostProcessor.java new file mode 100644 index 0000000000..677c839444 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/postprocessor/GuavaEventBusBeanPostProcessor.java @@ -0,0 +1,87 @@ +package com.baeldung.postprocessor; + +import com.google.common.eventbus.EventBus; + +import java.util.function.BiConsumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * A {@link DestructionAwareBeanPostProcessor} which registers/un-registers subscribers to a Guava {@link EventBus}. The class must + * be annotated with {@link Subscriber} and each subscribing method must be annotated with + * {@link com.google.common.eventbus.Subscribe}. + */ +@SuppressWarnings("ALL") +public class GuavaEventBusBeanPostProcessor implements DestructionAwareBeanPostProcessor { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final SpelExpressionParser expressionParser = new SpelExpressionParser(); + + @Override + public void postProcessBeforeDestruction(final Object bean, final String beanName) throws BeansException { + this.process(bean, EventBus::unregister, "destruction"); + } + + @Override + public boolean requiresDestruction(Object bean) { + return true; + } + + @Override + public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException { + this.process(bean, EventBus::register, "initialization"); + return bean; + } + + private void process(final Object bean, final BiConsumer consumer, final String action) { + Object proxy = this.getTargetObject(bean); + final Subscriber annotation = AnnotationUtils.getAnnotation(proxy.getClass(), Subscriber.class); + if (annotation == null) + return; + this.logger.info("{}: processing bean of type {} during {}", this.getClass().getSimpleName(), proxy.getClass().getName(), + action); + final String annotationValue = annotation.value(); + try { + final Expression expression = this.expressionParser.parseExpression(annotationValue); + final Object value = expression.getValue(); + if (!(value instanceof EventBus)) { + this.logger.error("{}: expression {} did not evaluate to an instance of EventBus for bean of type {}", + this.getClass().getSimpleName(), annotationValue, proxy.getClass().getSimpleName()); + return; + } + final EventBus eventBus = (EventBus)value; + consumer.accept(eventBus, proxy); + } catch (ExpressionException ex) { + this.logger.error("{}: unable to parse/evaluate expression {} for bean of type {}", this.getClass().getSimpleName(), + annotationValue, proxy.getClass().getName()); + } + } + + private Object getTargetObject(Object proxy) throws BeansException { + if (AopUtils.isJdkDynamicProxy(proxy)) { + try { + return ((Advised)proxy).getTargetSource().getTarget(); + } catch (Exception e) { + throw new FatalBeanException("Error getting target of JDK proxy", e); + } + } + return proxy; + } +} + + diff --git a/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTrade.java b/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTrade.java new file mode 100644 index 0000000000..7711cf7101 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTrade.java @@ -0,0 +1,34 @@ +package com.baeldung.postprocessor; + +import java.util.Date; + +public class StockTrade { + + private final String symbol; + private final int quantity; + private final double price; + private final Date tradeDate; + + public StockTrade(String symbol, int quantity, double price, Date tradeDate) { + this.symbol = symbol; + this.quantity = quantity; + this.price = price; + this.tradeDate = tradeDate; + } + + public String getSymbol() { + return this.symbol; + } + + public int getQuantity() { + return this.quantity; + } + + public double getPrice() { + return this.price; + } + + public Date getTradeDate() { + return this.tradeDate; + } +} diff --git a/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradeListener.java b/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradeListener.java new file mode 100644 index 0000000000..bf34d66f24 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradeListener.java @@ -0,0 +1,7 @@ +package com.baeldung.postprocessor; + +@FunctionalInterface +public interface StockTradeListener { + + void stockTradePublished(StockTrade trade); +} diff --git a/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradePublisher.java b/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradePublisher.java new file mode 100644 index 0000000000..bf339872d9 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/postprocessor/StockTradePublisher.java @@ -0,0 +1,36 @@ +package com.baeldung.postprocessor; + +import com.google.common.eventbus.AllowConcurrentEvents; +import com.google.common.eventbus.Subscribe; + +import java.util.HashSet; +import java.util.Set; + +@Subscriber +public class StockTradePublisher { + + private final Set stockTradeListeners = new HashSet<>(); + + public void addStockTradeListener(StockTradeListener listener) { + synchronized (this.stockTradeListeners) { + this.stockTradeListeners.add(listener); + } + } + + public void removeStockTradeListener(StockTradeListener listener) { + synchronized (this.stockTradeListeners) { + this.stockTradeListeners.remove(listener); + } + } + + @Subscribe + @AllowConcurrentEvents + private void handleNewStockTradeEvent(StockTrade trade) { + // publish to DB, send to PubNub, whatever you want here + final Set listeners; + synchronized (this.stockTradeListeners) { + listeners = new HashSet<>(this.stockTradeListeners); + } + listeners.forEach(li -> li.stockTradePublished(trade)); + } +} diff --git a/spring-core-4/src/main/java/com/baeldung/postprocessor/Subscriber.java b/spring-core-4/src/main/java/com/baeldung/postprocessor/Subscriber.java new file mode 100644 index 0000000000..bef38333d6 --- /dev/null +++ b/spring-core-4/src/main/java/com/baeldung/postprocessor/Subscriber.java @@ -0,0 +1,21 @@ +package com.baeldung.postprocessor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation which indicates which Guava {@link com.google.common.eventbus.EventBus} a Spring bean wishes to subscribe to. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Subscriber { + + /** + * A SpEL expression which selects the {@link com.google.common.eventbus.EventBus}. + */ + String value() default GlobalEventBus.GLOBAL_EVENT_BUS_EXPRESSION; +} diff --git a/spring-core-4/src/test/java/com/baeldung/postprocessor/PostProcessorConfiguration.java b/spring-core-4/src/test/java/com/baeldung/postprocessor/PostProcessorConfiguration.java new file mode 100644 index 0000000000..b28e36663a --- /dev/null +++ b/spring-core-4/src/test/java/com/baeldung/postprocessor/PostProcessorConfiguration.java @@ -0,0 +1,23 @@ +package com.baeldung.postprocessor; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class PostProcessorConfiguration { + + @Bean + public GlobalEventBus eventBus() { + return GlobalEventBus.getInstance(); + } + + @Bean + public GuavaEventBusBeanPostProcessor eventBusBeanPostProcessor() { + return new GuavaEventBusBeanPostProcessor(); + } + + @Bean + public StockTradePublisher stockTradePublisher() { + return new StockTradePublisher(); + } +} diff --git a/spring-core-4/src/test/java/com/baeldung/postprocessor/StockTradeIntegrationTest.java b/spring-core-4/src/test/java/com/baeldung/postprocessor/StockTradeIntegrationTest.java new file mode 100644 index 0000000000..ae3cd968dc --- /dev/null +++ b/spring-core-4/src/test/java/com/baeldung/postprocessor/StockTradeIntegrationTest.java @@ -0,0 +1,46 @@ +package com.baeldung.postprocessor; + +import java.time.Duration; +import java.util.Date; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {PostProcessorConfiguration.class}) +public class StockTradeIntegrationTest { + + @Autowired + private StockTradePublisher stockTradePublisher; + + @Test + public void givenValidConfig_whenTradePublished_thenTradeReceived() { + Date tradeDate = new Date(); + StockTrade stockTrade = new StockTrade("AMZN", 100, 2483.52d, tradeDate); + AtomicBoolean assertionsPassed = new AtomicBoolean(false); + StockTradeListener listener = trade -> assertionsPassed.set(this.verifyExact(stockTrade, trade)); + this.stockTradePublisher.addStockTradeListener(listener); + try { + GlobalEventBus.post(stockTrade); + await().atMost(Duration.ofSeconds(2L)) + .untilAsserted(() -> assertThat(assertionsPassed.get()).isTrue()); + } finally { + this.stockTradePublisher.removeStockTradeListener(listener); + } + } + + private boolean verifyExact(StockTrade stockTrade, StockTrade trade) { + return Objects.equals(stockTrade.getSymbol(), trade.getSymbol()) + && Objects.equals(stockTrade.getTradeDate(), trade.getTradeDate()) + && stockTrade.getQuantity() == trade.getQuantity() + && stockTrade.getPrice() == trade.getPrice(); + } +} From 6076d4da3b11817b868c6ea7d5769df9a5fa97c7 Mon Sep 17 00:00:00 2001 From: Carlos Cavero Date: Fri, 29 May 2020 00:02:11 +0200 Subject: [PATCH 27/27] BAEL-3756-YAML-for-Spring-DevOps (#9368) * Add modifications to include the configuration for YAML and DevOps * Clean the Dockerfile * Modify the name of testing environment and include YAML tests --- .../spring-boot-properties/.dockerignore | 13 ++++++++++ .../spring-boot-properties/Dockerfile | 10 ++++++++ .../spring-boot-properties/pom.xml | 3 ++- .../src/main/resources/application.yml | 19 +++++++++++++- .../baeldung/yaml/YAMLDevIntegrationTest.java | 25 +++++++++++++++++++ .../baeldung/yaml/YAMLIntegrationTest.java | 24 ++++++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 spring-boot-modules/spring-boot-properties/.dockerignore create mode 100644 spring-boot-modules/spring-boot-properties/Dockerfile create mode 100644 spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLDevIntegrationTest.java create mode 100644 spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLIntegrationTest.java diff --git a/spring-boot-modules/spring-boot-properties/.dockerignore b/spring-boot-modules/spring-boot-properties/.dockerignore new file mode 100644 index 0000000000..df36044e46 --- /dev/null +++ b/spring-boot-modules/spring-boot-properties/.dockerignore @@ -0,0 +1,13 @@ +# Logs +logs +*.log + +# Git +.git +.cache + +# Classes +**/*.class + +# Ignore md files +*.md diff --git a/spring-boot-modules/spring-boot-properties/Dockerfile b/spring-boot-modules/spring-boot-properties/Dockerfile new file mode 100644 index 0000000000..d6bd2a95ae --- /dev/null +++ b/spring-boot-modules/spring-boot-properties/Dockerfile @@ -0,0 +1,10 @@ +FROM maven:3.6.0-jdk-11 +WORKDIR /code/spring-boot-modules/spring-boot-properties/ +COPY ./spring-boot-modules/spring-boot-properties/pom.xml . +COPY ./spring-boot-modules/spring-boot-properties/src ./src +COPY ./parent-boot-2/pom.xml /code/parent-boot-2/pom.xml +COPY ./pom.xml /code/pom.xml +COPY ./custom-pmd-0.0.1.jar /code/custom-pmd-0.0.1.jar +COPY ./baeldung-pmd-rules.xml /code/baeldung-pmd-rules.xml +RUN mvn dependency:resolve +CMD ["mvn", "spring-boot:run"] \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-properties/pom.xml b/spring-boot-modules/spring-boot-properties/pom.xml index ef9c084f4c..98d328bd19 100644 --- a/spring-boot-modules/spring-boot-properties/pom.xml +++ b/spring-boot-modules/spring-boot-properties/pom.xml @@ -128,7 +128,8 @@ 4.4.11 @ 2.2.4.RELEASE - com.baeldung.buildproperties.Application + + com.baeldung.yaml.MyApplication diff --git a/spring-boot-modules/spring-boot-properties/src/main/resources/application.yml b/spring-boot-modules/spring-boot-properties/src/main/resources/application.yml index 6fc6f67cd0..4914ff15f7 100644 --- a/spring-boot-modules/spring-boot-properties/src/main/resources/application.yml +++ b/spring-boot-modules/spring-boot-properties/src/main/resources/application.yml @@ -1,7 +1,14 @@ +spring: + profiles: + active: + - test + +--- + spring: profiles: test name: test-YAML -environment: test +environment: testing servers: - www.abc.test.com - www.xyz.test.com @@ -15,3 +22,13 @@ environment: production servers: - www.abc.com - www.xyz.com + +--- + +spring: + profiles: dev +name: ${DEV_NAME:dev-YAML} +environment: development +servers: + - www.abc.dev.com + - www.xyz.dev.com diff --git a/spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLDevIntegrationTest.java b/spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLDevIntegrationTest.java new file mode 100644 index 0000000000..8dfc4c2208 --- /dev/null +++ b/spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLDevIntegrationTest.java @@ -0,0 +1,25 @@ +package com.baeldung.yaml; + +import static org.junit.Assert.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = MyApplication.class) +@TestPropertySource(properties = {"spring.profiles.active = dev"}) +class YAMLDevIntegrationTest { + + @Autowired + private YAMLConfig config; + + @Test + void whenProfileTest_thenNameTesting() { + assertTrue("development".equalsIgnoreCase(config.getEnvironment())); + assertTrue("dev-YAML".equalsIgnoreCase(config.getName())); + } +} diff --git a/spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLIntegrationTest.java b/spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLIntegrationTest.java new file mode 100644 index 0000000000..090d5c592e --- /dev/null +++ b/spring-boot-modules/spring-boot-properties/src/test/java/com/baeldung/yaml/YAMLIntegrationTest.java @@ -0,0 +1,24 @@ +package com.baeldung.yaml; + +import static org.junit.Assert.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = MyApplication.class) +class YAMLIntegrationTest { + + @Autowired + private YAMLConfig config; + + @Test + void whenProfileTest_thenNameTesting() { + assertTrue("testing".equalsIgnoreCase(config.getEnvironment())); + assertTrue("test-YAML".equalsIgnoreCase(config.getName())); + } +}