Merge branch 'master' of https://github.com/eugenp/tutorials
This commit is contained in:
commit
4505e5c240
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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;
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Classes
|
||||||
|
**/*.class
|
||||||
|
|
||||||
|
# Ignore md files
|
||||||
|
*.md
|
|
@ -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"]
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.baeldung.postprocessor;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface StockTradeListener {
|
||||||
|
|
||||||
|
void stockTradePublished(StockTrade trade);
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue