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 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 diff --git a/jee-kotlin/pom.xml b/jee-kotlin/pom.xml index 9191885bd4..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 @@ -278,9 +279,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 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; 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-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 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 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) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index 090967d8a8..189eb4cf1a 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} + @@ -200,6 +228,9 @@ 2.1 2.6.0 3.3.0 + 4.10.0 + 0.2.0 + 2.8.2 diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java new file mode 100644 index 0000000000..f16d347f85 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.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 Bucket4jRateLimitApp { + + public static void main(String[] args) { + 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/Bucket4jRateLimitApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java new file mode 100644 index 0000000000..bb179b9b38 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java @@ -0,0 +1,35 @@ +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.RateLimitInterceptor; + +@SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = { + DataSourceAutoConfiguration.class, + SecurityAutoConfiguration.class +}) +public class Bucket4jRateLimitApp implements WebMvcConfigurer { + + @Autowired + @Lazy + private RateLimitInterceptor interceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(interceptor) + .addPathPatterns("/api/v1/area/**"); + } + + public static void main(String[] args) { + 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/RateLimitInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java new file mode 100644 index 0000000000..8a18d6c2b5 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java @@ -0,0 +1,57 @@ +package com.baeldung.ratelimiting.bucket4japp.interceptor; + +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.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlanService; + +import io.github.bucket4j.Bucket; +import io.github.bucket4j.ConsumptionProbe; + +@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-Seconds"; + + @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 = pricingPlanService.resolveBucket(apiKey); + + ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1); + + if (probe.isConsumed()) { + + response.addHeader(HEADER_LIMIT_REMAINING, String.valueOf(probe.getRemainingTokens())); + return true; + + } else { + + long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; + + 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; + } + } +} \ 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 new file mode 100644 index 0000000000..27c30ba3a0 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -0,0 +1,42 @@ +package com.baeldung.ratelimiting.bucket4japp.service; + +import java.time.Duration; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Refill; + +public enum PricingPlan { + + FREE(20), + + BASIC(40), + + 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; + + } 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/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..7d8a718601 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java @@ -0,0 +1,31 @@ +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<>(); + + 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/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; + } +} 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..ecc9f22e0a --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml @@ -0,0 +1,40 @@ +server: + port: 9001 + +spring: + application: + name: bucket4j-starter-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 + +bucket4j: + enabled: true + filters: + - 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: + - 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 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..ae19622d9b --- /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-limit-app + mvc: + throw-exception-if-no-handler-found: true + resources: + add-mappings: false 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..20f57a7021 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java @@ -0,0 +1,61 @@ +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.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/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java new file mode 100644 index 0000000000..fbf63ba403 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java @@ -0,0 +1,82 @@ +package com.baeldung.ratelimiting.bucket4japp; + +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 Bucket4jUsageUnitTest { + + @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(); + } + } +} 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()); + } +} 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 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())); + } +} 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(); + } +} 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 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