Merge branch 'master' of https://github.com/eugenp/tutorials
This commit is contained in:
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -19,9 +19,9 @@ class StringComparisonUnitTest {
|
||||
fun `compare using referential equals operator`() {
|
||||
val first = "kotlin"
|
||||
val second = "kotlin"
|
||||
val copyOfFirst = buildString { "kotlin" }
|
||||
val third = String("kotlin".toCharArray())
|
||||
assertTrue { first === second }
|
||||
assertFalse { first === copyOfFirst }
|
||||
assertFalse { first === third }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+3
-1
@@ -221,6 +221,7 @@
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<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>
|
||||
</systemPropertyVariables>
|
||||
<redirectTestOutputToFile>false</redirectTestOutputToFile>
|
||||
@@ -278,9 +279,10 @@
|
||||
<arquillian-drone-bom.version>2.0.1.Final</arquillian-drone-bom.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>
|
||||
<shrinkwrap.version>3.1.3</shrinkwrap.version>
|
||||
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
|
||||
|
||||
<class>com.enpy.entity.Student</class>
|
||||
<class>com.baeldung.jeekotlin.entity.Student</class>
|
||||
|
||||
<properties>
|
||||
<property name="hibernate.hbm2ddl.auto" value="create"/>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
public class HibernateTypesIntegrationTest {
|
||||
public class HibernateTypesLiveTest {
|
||||
|
||||
@Autowired
|
||||
AlbumRepository albumRepository;
|
||||
@@ -3,6 +3,6 @@
|
||||
- [Using JDBI with Spring Boot](https://www.baeldung.com/spring-boot-jdbi)
|
||||
- [Configuring a Tomcat Connection Pool in Spring Boot](https://www.baeldung.com/spring-boot-tomcat-connection-pool)
|
||||
- [Integrating Spring Boot with HSQLDB](https://www.baeldung.com/spring-boot-hsqldb)
|
||||
- [List of In-Memory Databases](http://www.baeldung.com/java-in-memory-databases)
|
||||
- [List of In-Memory Databases](https://www.baeldung.com/java-in-memory-databases)
|
||||
- [Oracle Connection Pooling With Spring](https://www.baeldung.com/spring-oracle-connection-pooling)
|
||||
- More articles: [[<-- prev]](../spring-boot-persistence)
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import javax.sql.DataSource;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import com.baeldung.tomcatconnectionpool.application.SpringBootConsoleApplication;
|
||||
@@ -13,6 +14,7 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = {SpringBootConsoleApplication.class})
|
||||
@TestPropertySource(properties = "spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource")
|
||||
public class SpringBootTomcatConnectionPoolIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -35,23 +35,21 @@
|
||||
<version>${lombok.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
<version>${hibernate.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladmihalcea</groupId>
|
||||
<artifactId>db-util</artifactId>
|
||||
<version>${db-util.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy</artifactId>
|
||||
<version>${byte-buddy.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<!-- The main class to start by executing java -jar -->
|
||||
<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>
|
||||
</properties>
|
||||
|
||||
|
||||
+1
-2
@@ -8,5 +8,4 @@ spring.jpa.properties.hibernate.format_sql=true
|
||||
spring.jpa.properties.hibernate.validator.apply_to_ddl=false
|
||||
#spring.jpa.properties.hibernate.check_nullability=true
|
||||
spring.h2.console.enabled=true
|
||||
spring.h2.console.path=/h2-console
|
||||
debug=true
|
||||
spring.h2.console.path=/h2-console
|
||||
@@ -1,8 +1,8 @@
|
||||
### Relevant Articles:
|
||||
|
||||
- [Spring Boot with Multiple SQL Import Files](http://www.baeldung.com/spring-boot-sql-import-files)
|
||||
- [Configuring Separate Spring DataSource for Tests](http://www.baeldung.com/spring-testing-separate-data-source)
|
||||
- [Quick Guide on Loading Initial Data with Spring Boot](http://www.baeldung.com/spring-boot-data-sql-and-schema-sql)
|
||||
- [Spring Boot with Multiple SQL Import Files](https://www.baeldung.com/spring-boot-sql-import-files)
|
||||
- [Configuring Separate Spring DataSource for Tests](https://www.baeldung.com/spring-testing-separate-data-source)
|
||||
- [Quick Guide on Loading Initial Data with Spring Boot](https://www.baeldung.com/spring-boot-data-sql-and-schema-sql)
|
||||
- [Configuring a DataSource Programmatically in Spring Boot](https://www.baeldung.com/spring-boot-configure-data-source-programmatic)
|
||||
- [Resolving “Failed to Configure a DataSource” Error](https://www.baeldung.com/spring-boot-failed-to-configure-data-source)
|
||||
- [Hibernate Field Naming with Spring Boot](https://www.baeldung.com/hibernate-field-naming-spring-boot)
|
||||
|
||||
@@ -87,7 +87,35 @@
|
||||
<artifactId>javase</artifactId>
|
||||
<version>${zxing.version}</version>
|
||||
</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>
|
||||
|
||||
<repositories>
|
||||
@@ -200,6 +228,9 @@
|
||||
<barcode4j.version>2.1</barcode4j.version>
|
||||
<qrgen.version>2.6.0</qrgen.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>
|
||||
|
||||
</project>
|
||||
|
||||
+21
@@ -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);
|
||||
}
|
||||
}
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
+57
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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();
|
||||
}
|
||||
}
|
||||
+29
@@ -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()));
|
||||
}
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
+40
@@ -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
|
||||
+10
@@ -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
|
||||
+63
@@ -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"));
|
||||
}
|
||||
}
|
||||
+61
@@ -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"));
|
||||
}
|
||||
}
|
||||
+82
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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>
|
||||
|
||||
<properties>
|
||||
<spring-boot.version>2.1.1.RELEASE</spring-boot.version>
|
||||
<start-class>com.baeldung.birt.engine.ReportEngineApplication</start-class>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<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"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user