This commit is contained in:
Jonathan Cook 2020-05-29 09:52:21 +02:00
commit 4505e5c240
55 changed files with 1605 additions and 26 deletions

View File

@ -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<Integer, PizzaStatus> 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);
}
}

View File

@ -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);
}
}

View File

@ -19,9 +19,9 @@ class StringComparisonUnitTest {
fun `compare using referential equals operator`() { fun `compare using referential equals operator`() {
val first = "kotlin" val first = "kotlin"
val second = "kotlin" val second = "kotlin"
val copyOfFirst = buildString { "kotlin" } val third = String("kotlin".toCharArray())
assertTrue { first === second } assertTrue { first === second }
assertFalse { first === copyOfFirst } assertFalse { first === third }
} }
@Test @Test

View File

@ -221,6 +221,7 @@
<systemPropertyVariables> <systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<jboss.home>${project.basedir}/target/wildfly-${wildfly.version}</jboss.home> <jboss.home>${project.basedir}/target/wildfly-${wildfly.version}</jboss.home>
<jboss.http.port>8756</jboss.http.port>
<module.path>${project.basedir}/target/wildfly-${wildfly.version}/modules</module.path> <module.path>${project.basedir}/target/wildfly-${wildfly.version}/modules</module.path>
</systemPropertyVariables> </systemPropertyVariables>
<redirectTestOutputToFile>false</redirectTestOutputToFile> <redirectTestOutputToFile>false</redirectTestOutputToFile>
@ -278,9 +279,10 @@
<arquillian-drone-bom.version>2.0.1.Final</arquillian-drone-bom.version> <arquillian-drone-bom.version>2.0.1.Final</arquillian-drone-bom.version>
<arquillian-rest-client.version>1.0.0.Alpha4</arquillian-rest-client.version> <arquillian-rest-client.version>1.0.0.Alpha4</arquillian-rest-client.version>
<logback.version>1.1.7</logback.version>
<resteasy.version>3.8.0.Final</resteasy.version> <resteasy.version>3.8.0.Final</resteasy.version>
<shrinkwrap.version>3.1.3</shrinkwrap.version> <shrinkwrap.version>3.1.3</shrinkwrap.version>
</properties> </properties>
</project> </project>

View File

@ -7,7 +7,7 @@
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source> <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<class>com.enpy.entity.Student</class> <class>com.baeldung.jeekotlin.entity.Student</class>
<properties> <properties>
<property name="hibernate.hbm2ddl.auto" value="create"/> <property name="hibernate.hbm2ddl.auto" value="create"/>

View File

@ -13,7 +13,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest @SpringBootTest
public class HibernateTypesIntegrationTest { public class HibernateTypesLiveTest {
@Autowired @Autowired
AlbumRepository albumRepository; AlbumRepository albumRepository;

View File

@ -3,6 +3,6 @@
- [Using JDBI with Spring Boot](https://www.baeldung.com/spring-boot-jdbi) - [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) - [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) - [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) - [Oracle Connection Pooling With Spring](https://www.baeldung.com/spring-oracle-connection-pooling)
- More articles: [[<-- prev]](../spring-boot-persistence) - More articles: [[<-- prev]](../spring-boot-persistence)

View File

@ -4,6 +4,7 @@ import javax.sql.DataSource;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import com.baeldung.tomcatconnectionpool.application.SpringBootConsoleApplication; import com.baeldung.tomcatconnectionpool.application.SpringBootConsoleApplication;
@ -13,6 +14,7 @@ import org.springframework.boot.test.context.SpringBootTest;
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest(classes = {SpringBootConsoleApplication.class}) @SpringBootTest(classes = {SpringBootConsoleApplication.class})
@TestPropertySource(properties = "spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource")
public class SpringBootTomcatConnectionPoolIntegrationTest { public class SpringBootTomcatConnectionPoolIntegrationTest {
@Autowired @Autowired

View File

@ -35,23 +35,21 @@
<version>${lombok.version}</version> <version>${lombok.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.vladmihalcea</groupId> <groupId>com.vladmihalcea</groupId>
<artifactId>db-util</artifactId> <artifactId>db-util</artifactId>
<version>${db-util.version}</version> <version>${db-util.version}</version>
</dependency> </dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte-buddy.version}</version>
</dependency>
</dependencies> </dependencies>
<properties> <properties>
<!-- The main class to start by executing java -jar --> <!-- The main class to start by executing java -jar -->
<start-class>com.baeldung.h2db.demo.server.SpringBootApp</start-class> <start-class>com.baeldung.h2db.demo.server.SpringBootApp</start-class>
<spring-boot.version>2.0.4.RELEASE</spring-boot.version>
<hibernate.version>5.3.11.Final</hibernate.version>
<db-util.version>1.0.4</db-util.version> <db-util.version>1.0.4</db-util.version>
</properties> </properties>

View File

@ -9,4 +9,3 @@ spring.jpa.properties.hibernate.validator.apply_to_ddl=false
#spring.jpa.properties.hibernate.check_nullability=true #spring.jpa.properties.hibernate.check_nullability=true
spring.h2.console.enabled=true spring.h2.console.enabled=true
spring.h2.console.path=/h2-console spring.h2.console.path=/h2-console
debug=true

View File

@ -1,8 +1,8 @@
### Relevant Articles: ### Relevant Articles:
- [Spring Boot with Multiple SQL Import Files](http://www.baeldung.com/spring-boot-sql-import-files) - [Spring Boot with Multiple SQL Import Files](https://www.baeldung.com/spring-boot-sql-import-files)
- [Configuring Separate Spring DataSource for Tests](http://www.baeldung.com/spring-testing-separate-data-source) - [Configuring Separate Spring DataSource for Tests](https://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) - [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) - [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) - [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) - [Hibernate Field Naming with Spring Boot](https://www.baeldung.com/hibernate-field-naming-spring-boot)

View File

@ -87,7 +87,35 @@
<artifactId>javase</artifactId> <artifactId>javase</artifactId>
<version>${zxing.version}</version> <version>${zxing.version}</version>
</dependency> </dependency>
<!-- Bucket4j -->
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>${bucket4j.version}</version>
</dependency>
<dependency>
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
<artifactId>bucket4j-spring-boot-starter</artifactId>
<version>${bucket4j-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>jcache</artifactId>
<version>${caffeine.version}</version>
</dependency>
</dependencies> </dependencies>
<repositories> <repositories>
@ -200,6 +228,9 @@
<barcode4j.version>2.1</barcode4j.version> <barcode4j.version>2.1</barcode4j.version>
<qrgen.version>2.6.0</qrgen.version> <qrgen.version>2.6.0</qrgen.version>
<zxing.version>3.3.0</zxing.version> <zxing.version>3.3.0</zxing.version>
<bucket4j.version>4.10.0</bucket4j.version>
<bucket4j-spring-boot-starter.version>0.2.0</bucket4j-spring-boot-starter.version>
<caffeine.version>2.8.2</caffeine.version>
</properties> </properties>
</project> </project>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<String, Bucket> 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();
}
}

View File

@ -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<AreaV1> rectangle(@RequestBody RectangleDimensionsV1 dimensions) {
return ResponseEntity.ok(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth()));
}
@PostMapping(value = "/triangle", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<AreaV1> triangle(@RequestBody TriangleDimensionsV1 dimensions) {
return ResponseEntity.ok(new AreaV1("triangle", 0.5d * dimensions.getHeight() * dimensions.getBase()));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}

View File

@ -75,7 +75,6 @@
</build> </build>
<properties> <properties>
<spring-boot.version>2.1.1.RELEASE</spring-boot.version>
<start-class>com.baeldung.birt.engine.ReportEngineApplication</start-class> <start-class>com.baeldung.birt.engine.ReportEngineApplication</start-class>
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>

View File

@ -0,0 +1,13 @@
# Logs
logs
*.log
# Git
.git
.cache
# Classes
**/*.class
# Ignore md files
*.md

View File

@ -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"]

View File

@ -128,7 +128,8 @@
<httpcore.version>4.4.11</httpcore.version> <httpcore.version>4.4.11</httpcore.version>
<resource.delimiter>@</resource.delimiter> <resource.delimiter>@</resource.delimiter>
<configuration-processor.version>2.2.4.RELEASE</configuration-processor.version> <configuration-processor.version>2.2.4.RELEASE</configuration-processor.version>
<start-class>com.baeldung.buildproperties.Application</start-class> <!-- <start-class>com.baeldung.buildproperties.Application</start-class> -->
<start-class>com.baeldung.yaml.MyApplication</start-class>
</properties> </properties>
</project> </project>

View File

@ -1,7 +1,14 @@
spring:
profiles:
active:
- test
---
spring: spring:
profiles: test profiles: test
name: test-YAML name: test-YAML
environment: test environment: testing
servers: servers:
- www.abc.test.com - www.abc.test.com
- www.xyz.test.com - www.xyz.test.com
@ -15,3 +22,13 @@ environment: production
servers: servers:
- www.abc.com - www.abc.com
- www.xyz.com - www.xyz.com
---
spring:
profiles: dev
name: ${DEV_NAME:dev-YAML}
environment: development
servers:
- www.abc.dev.com
- www.xyz.dev.com

View File

@ -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()));
}
}

View File

@ -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()));
}
}

View File

@ -24,6 +24,16 @@
<artifactId>spring-core</artifactId> <artifactId>spring-core</artifactId>
<version>${spring.version}</version> <version>${spring.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId> <artifactId>spring-test</artifactId>
@ -42,6 +52,18 @@
<version>${junit-jupiter.version}</version> <version>${junit-jupiter.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>2.9.1</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -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);
}
}

View File

@ -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<String> 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;
}
}

View File

@ -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<EventBus, Object> 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
package com.baeldung.postprocessor;
@FunctionalInterface
public interface StockTradeListener {
void stockTradePublished(StockTrade trade);
}

View File

@ -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<StockTradeListener> 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<StockTradeListener> listeners;
synchronized (this.stockTradeListeners) {
listeners = new HashSet<>(this.stockTradeListeners);
}
listeners.forEach(li -> li.stockTradePublished(trade));
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -15,6 +15,7 @@
<modules> <modules>
<module>spring-security-acl</module> <module>spring-security-acl</module>
<module>spring-security-auth0</module>
<module>spring-security-angular/server</module> <module>spring-security-angular/server</module>
<module>spring-security-cache-control</module> <module>spring-security-cache-control</module>
<module>spring-security-core</module> <module>spring-security-core</module>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-auth0</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-security-auth0</name>
<packaging>war</packaging>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-2</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>mvc-auth-commons</artifactId>
<version>${mvc-auth-commons.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
</dependencies>
<build>
<finalName>spring-security-auth0</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<json.version>20190722</json.version>
<mvc-auth-commons.version>1.2.0</mvc-auth-commons.version>
</properties>
</project>

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<String> request = new HttpEntity<String>(requestBody.toString(), headers);
RestTemplate restTemplate = new RestTemplate();
HashMap<String, String> result = restTemplate.postForObject(AUTH0_TOKEN_URL, request, HashMap.class);
return result.get("access_token");
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<String> users(HttpServletRequest request, HttpServletResponse response) throws IOException, IdentityVerificationException {
ResponseEntity<String> result = apiService.getCall(config.getUsersUrl());
return result;
}
@GetMapping(value = "/userByEmail")
@ResponseBody
public ResponseEntity<String> userByEmail(HttpServletResponse response, @RequestParam String email) {
ResponseEntity<String> result = apiService.getCall(config.getUsersByEmailUrl()+email);
return result;
}
@GetMapping(value = "/createUser")
@ResponseBody
public ResponseEntity<String> 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<String> result = apiService.postCall(config.getUsersUrl(), request.toString());
return result;
}
}

View File

@ -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<String> getCall(String url) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer "+controller.getManagementApiToken());
HttpEntity<String> entity = new HttpEntity<String>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> result = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
return result;
}
public ResponseEntity<String> postCall(String url, String requestBody) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer "+controller.getManagementApiToken());
HttpEntity<String> request = new HttpEntity<String>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> result = restTemplate.postForEntity(url, request, String.class);
return result;
}
}

View File

@ -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

View File

@ -27,7 +27,7 @@ public class CurrenciesControllerIntegrationTest {
.header("Accept-Language", "es-ES") .header("Accept-Language", "es-ES")
.param("amount", "10032.5")) .param("amount", "10032.5"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("10.032,50 €"))); .andExpect(content().string(containsString("10.032,50")));
} }
@Test @Test
@ -42,10 +42,10 @@ public class CurrenciesControllerIntegrationTest {
@Test @Test
public void whenCallCurrencyWithRomanianLocaleWithArrays_ThenReturnLocaleCurrencies() throws Exception { public void whenCallCurrencyWithRomanianLocaleWithArrays_ThenReturnLocaleCurrencies() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/currency") mockMvc.perform(MockMvcRequestBuilders.get("/currency")
.header("Accept-Language", "ro-RO") .header("Accept-Language", "en-GB")
.param("amountList", "10", "20", "30")) .param("amountList", "10", "20", "30"))
.andExpect(status().isOk()) .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 @Test