diff --git a/examples/ehcache/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/controller/TestController.java b/examples/ehcache/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/controller/TestController.java index aabac8db..a7359385 100644 --- a/examples/ehcache/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/controller/TestController.java +++ b/examples/ehcache/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/controller/TestController.java @@ -2,12 +2,17 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import com.giffing.bucket4j.spring.boot.starter.utils.Bucket4JUtils; +import jakarta.validation.Valid; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import com.giffing.bucket4j.spring.boot.starter.config.cache.CacheManager; @@ -75,10 +80,38 @@ public ResponseEntity hello() { return ResponseEntity.ok("Hello World"); } + + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @param bindingResult the result of the Jakarta validation + * @return + */ @PostMapping("filters/{filterId}") - public ResponseEntity updateConfig(@PathVariable String filterId, @RequestBody Bucket4JConfiguration filter) { - configCacheManager.setValue(filterId, filter); + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody @Valid Bucket4JConfiguration newConfig, + BindingResult bindingResult) { + + //validate that there are no errors by the Jakarta validation + if (bindingResult.hasErrors()) { + List errors = bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + return ResponseEntity.ok().build(); } + private record ValidationErrorResponse(String message, List errors) {} } diff --git a/examples/gateway/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/TestController.java b/examples/gateway/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/TestController.java index b962a93f..5d8359cd 100644 --- a/examples/gateway/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/TestController.java +++ b/examples/gateway/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/TestController.java @@ -1,24 +1,64 @@ package com.giffing.bucket4j.spring.boot.starter.examples.gateway; +import com.giffing.bucket4j.spring.boot.starter.utils.Bucket4JUtils; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import com.giffing.bucket4j.spring.boot.starter.config.cache.CacheManager; import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JConfiguration; +import java.util.List; +import java.util.Set; + @RestController @RequestMapping("/") public class TestController { + @Autowired + Validator validator; + private final CacheManager configCacheManager; public TestController(CacheManager configCacheManager){ this.configCacheManager = configCacheManager; } + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @return + */ @PostMapping("filters/{filterId}") - public ResponseEntity updateConfig(@PathVariable String filterId, @RequestBody Bucket4JConfiguration filter){ - configCacheManager.setValue(filterId, filter); + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody Bucket4JConfiguration newConfig) { + + //validate that there are no errors by the Jakarta validation + Set> violations = validator.validate(newConfig); + if (!violations.isEmpty()) { + List errors = violations.stream().map(ConstraintViolation::getMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + return ResponseEntity.ok().build(); } + + private record ValidationErrorResponse(String message, List errors) {} } diff --git a/examples/gateway/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/GatewaySampleApplicationTest.java b/examples/gateway/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/GatewaySampleApplicationTest.java index e0a45242..4f8248ad 100644 --- a/examples/gateway/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/GatewaySampleApplicationTest.java +++ b/examples/gateway/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/gateway/GatewaySampleApplicationTest.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.stream.IntStream; +import com.giffing.bucket4j.spring.boot.starter.context.ExecutePredicateDefinition; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; diff --git a/examples/hazelcast/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/hazelcast/TestController.java b/examples/hazelcast/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/hazelcast/TestController.java index 71a3e477..ffbdc766 100644 --- a/examples/hazelcast/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/hazelcast/TestController.java +++ b/examples/hazelcast/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/hazelcast/TestController.java @@ -1,11 +1,17 @@ package com.giffing.bucket4j.spring.boot.starter.examples.hazelcast; +import com.giffing.bucket4j.spring.boot.starter.utils.Bucket4JUtils; +import jakarta.validation.Valid; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import com.giffing.bucket4j.spring.boot.starter.config.cache.CacheManager; import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JConfiguration; +import java.util.List; + @RestController @RequestMapping("/") public class TestController { @@ -21,11 +27,40 @@ public ResponseEntity helloWorld() { return ResponseEntity.ok().body("Hello World"); } + + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @param bindingResult the result of the Jakarta validation + * @return + */ @PostMapping("filters/{filterId}") - public ResponseEntity updateConfig(@PathVariable String filterId, @RequestBody Bucket4JConfiguration filter) { - configCacheManager.setValue(filterId, filter); + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody @Valid Bucket4JConfiguration newConfig, + BindingResult bindingResult) { + + //validate that there are no errors by the Jakarta validation + if (bindingResult.hasErrors()) { + List errors = bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + return ResponseEntity.ok().build(); } + private record ValidationErrorResponse(String message, List errors) {} + } diff --git a/examples/redis-jedis/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java b/examples/redis-jedis/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java index fa278e7e..8bdcf7f2 100644 --- a/examples/redis-jedis/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java +++ b/examples/redis-jedis/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java @@ -1,11 +1,17 @@ package com.giffing.bucket4j.spring.boot.starter; +import com.giffing.bucket4j.spring.boot.starter.utils.Bucket4JUtils; +import jakarta.validation.Valid; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import com.giffing.bucket4j.spring.boot.starter.config.cache.CacheManager; import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JConfiguration; +import java.util.List; + @RestController public class TestController { @@ -25,9 +31,38 @@ public ResponseEntity world() { return ResponseEntity.ok("Hello World"); } + + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @param bindingResult the result of the Jakarta validation + * @return + */ @PostMapping("filters/{filterId}") - public ResponseEntity updateConfig(@PathVariable String filterId, @RequestBody Bucket4JConfiguration filter){ - configCacheManager.setValue(filterId, filter); + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody @Valid Bucket4JConfiguration newConfig, + BindingResult bindingResult) { + + //validate that there are no errors by the Jakarta validation + if (bindingResult.hasErrors()) { + List errors = bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + return ResponseEntity.ok().build(); } + + private record ValidationErrorResponse(String message, List errors) {} } diff --git a/examples/redis-lettuce/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java b/examples/redis-lettuce/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java index 94abc8f1..fb1ea5ca 100644 --- a/examples/redis-lettuce/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java +++ b/examples/redis-lettuce/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java @@ -1,14 +1,27 @@ package com.giffing.bucket4j.spring.boot.starter; +import com.giffing.bucket4j.spring.boot.starter.utils.Bucket4JUtils; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import com.giffing.bucket4j.spring.boot.starter.config.cache.CacheManager; import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JConfiguration; +import java.util.List; +import java.util.Set; + @RestController public class TestController { + @Autowired + Validator validator; + private final CacheManager configCacheManager; public TestController(CacheManager configCacheManager){ @@ -25,9 +38,37 @@ public ResponseEntity world() { return ResponseEntity.ok("Hello World"); } + + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @return + */ @PostMapping("filters/{filterId}") - public ResponseEntity updateConfig(@PathVariable String filterId, @RequestBody Bucket4JConfiguration filter){ - configCacheManager.setValue(filterId, filter); + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody Bucket4JConfiguration newConfig) { + + //validate that there are no errors by the Jakarta validation + Set> violations = validator.validate(newConfig); + if (!violations.isEmpty()) { + List errors = violations.stream().map(ConstraintViolation::getMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + return ResponseEntity.ok().build(); } + + private record ValidationErrorResponse(String message, List errors) {} } diff --git a/examples/redis-redisson/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java b/examples/redis-redisson/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java index 5d8ca7a7..171d51dc 100644 --- a/examples/redis-redisson/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java +++ b/examples/redis-redisson/src/main/java/com/giffing/bucket4j/spring/boot/starter/TestController.java @@ -25,9 +25,38 @@ public ResponseEntity world() { return ResponseEntity.ok("Hello World"); } + + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @param bindingResult the result of the Jakarta validation + * @return + */ @PostMapping("filters/{filterId}") - public ResponseEntity updateConfig(@PathVariable String filterId, @RequestBody Bucket4JConfiguration filter){ - configCacheManager.setValue(filterId, filter); + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody @Valid Bucket4JConfiguration newConfig, + BindingResult bindingResult) { + + //validate that there are no errors by the Jakarta validation + if (bindingResult.hasErrors()) { + List errors = bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + return ResponseEntity.ok().build(); } + + private record ValidationErrorResponse(String message, List errors) {} } diff --git a/examples/webflux-infinispan/pom.xml b/examples/webflux-infinispan/pom.xml index 73a6a364..72bba14d 100644 --- a/examples/webflux-infinispan/pom.xml +++ b/examples/webflux-infinispan/pom.xml @@ -15,6 +15,11 @@ + + io.micrometer + micrometer-registry-prometheus + 1.12.1 + org.springframework.boot spring-boot-starter-webflux diff --git a/examples/webflux-infinispan/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java b/examples/webflux-infinispan/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java index b2932fc5..32e5384f 100644 --- a/examples/webflux-infinispan/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java +++ b/examples/webflux-infinispan/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java @@ -1,16 +1,35 @@ package com.giffing.bucket4j.spring.boot.starter.examples.webflux; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import com.giffing.bucket4j.spring.boot.starter.config.cache.CacheManager; +import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JConfiguration; +import com.giffing.bucket4j.spring.boot.starter.utils.Bucket4JUtils; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; +import java.util.List; +import java.util.Set; + @RestController @RequestMapping public class MyController { + @Autowired + Validator validator; + + private final CacheManager configCacheManager; + + public MyController(CacheManager configCacheManager) { + this.configCacheManager = configCacheManager; + } + @GetMapping("/hello") public Mono hello( @RequestParam(defaultValue = "World") String name) { @@ -28,5 +47,38 @@ public Mono world( .just(s + ", " + name + "!\n") ); } - + + + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @return + */ + @PostMapping("filters/{filterId}") + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody Bucket4JConfiguration newConfig) { + + //validate that there are no errors by the Jakarta validation + Set> violations = validator.validate(newConfig); + if (!violations.isEmpty()) { + List errors = violations.stream().map(ConstraintViolation::getMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + + return ResponseEntity.ok().build(); + } + + private record ValidationErrorResponse(String message, List errors) {} } \ No newline at end of file diff --git a/examples/webflux-infinispan/src/main/resources/application.yml b/examples/webflux-infinispan/src/main/resources/application.yml index 2d955366..3e1aa7eb 100644 --- a/examples/webflux-infinispan/src/main/resources/application.yml +++ b/examples/webflux-infinispan/src/main/resources/application.yml @@ -15,8 +15,10 @@ infinispan: config-xml: infinispan.xml bucket4j: enabled: true + filter-config-caching-enabled: true filters: - - cache-name: buckets + - id: filter1 + cache-name: buckets filter-method: webflux url: .* http-content-type: application/json;charset=UTF-8 diff --git a/examples/webflux-infinispan/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/WebfluxInfinispanRateLimitTest.java b/examples/webflux-infinispan/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/WebfluxInfinispanRateLimitTest.java index 27b3036d..cd8f425b 100644 --- a/examples/webflux-infinispan/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/WebfluxInfinispanRateLimitTest.java +++ b/examples/webflux-infinispan/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/WebfluxInfinispanRateLimitTest.java @@ -3,8 +3,10 @@ import java.util.Collections; import java.util.stream.IntStream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JBootProperties; +import com.giffing.bucket4j.spring.boot.starter.context.properties.Bucket4JConfiguration; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationContext; @@ -14,13 +16,19 @@ @SpringBootTest @ActiveProfiles("webflux-infinispan") // Like this +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class WebfluxInfinispanRateLimitTest { @Autowired ApplicationContext context; + @Autowired + Bucket4JBootProperties properties; + WebTestClient rest; + private final ObjectMapper objectMapper = new ObjectMapper(); + @BeforeEach public void setup() { this.rest = WebTestClient @@ -30,6 +38,7 @@ public void setup() { } @Test + @Order(1) void helloTest() throws Exception { String url = "/hello"; IntStream.rangeClosed(1, 5) @@ -44,6 +53,7 @@ void helloTest() throws Exception { @Test + @Order(1) void worldTest() throws Exception { String url = "/world"; IntStream.rangeClosed(1, 10) @@ -56,7 +66,34 @@ void worldTest() throws Exception { blockedWebRequestDueToRateLimit(url); } - + + + @Test + @Order(2) + void replaceConfigTest() throws Exception { + String filterEndpoint = "/world"; + int newFilterCapacity = 1000; + + //get the /world filter + Bucket4JConfiguration filter = properties.getFilters().stream().filter(x -> filterEndpoint.matches(x.getUrl())).findFirst().orElse(null); + assert filter != null; + + //update the first (and only) bandwidth capacity of the first (and only) rate limit of the Filter configuration + Bucket4JConfiguration clone = objectMapper.readValue(objectMapper.writeValueAsString(filter),Bucket4JConfiguration.class); + clone.setMajorVersion(clone.getMajorVersion() + 1); + clone.getRateLimits().get(0).getBandwidths().get(0).setCapacity(newFilterCapacity); + + //update the filter cache + String url = "/filters/".concat(clone.getId()); + rest.post().uri(url).bodyValue(clone).exchange().expectStatus().isOk(); + + //Short sleep to allow the cacheUpdateListeners to update the filter configuration + Thread.sleep(100); + + //validate that the new capacity is applied to requests + successfulWebRequest(filterEndpoint, newFilterCapacity-1); + } + private void successfulWebRequest(String url, Integer remainingTries) { rest .get() diff --git a/examples/webflux-infinispan/src/test/resources/application-webflux-infinispan.yml b/examples/webflux-infinispan/src/test/resources/application-webflux-infinispan.yml index f06370e6..c376bbf3 100644 --- a/examples/webflux-infinispan/src/test/resources/application-webflux-infinispan.yml +++ b/examples/webflux-infinispan/src/test/resources/application-webflux-infinispan.yml @@ -6,8 +6,10 @@ infinispan: config-xml: infinispan.xml bucket4j: enabled: true + filter-config-caching-enabled: true filters: - - cache-name: buckets_test + - id: filter1 + cache-name: buckets_test filter-method: webflux url: ^(/hello).* rate-limits: @@ -15,7 +17,8 @@ bucket4j: - capacity: 5 time: 10 unit: seconds - - cache-name: buckets_test + - id: filter2 + cache-name: buckets_test filter-method: webflux url: ^(/world).* rate-limits: diff --git a/examples/webflux/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java b/examples/webflux/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java index f8526904..57319d5c 100644 --- a/examples/webflux/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java +++ b/examples/webflux/src/main/java/com/giffing/bucket4j/spring/boot/starter/examples/webflux/MyController.java @@ -1,6 +1,13 @@ package com.giffing.bucket4j.spring.boot.starter.examples.webflux; +import com.giffing.bucket4j.spring.boot.starter.utils.Bucket4JUtils; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import com.giffing.bucket4j.spring.boot.starter.config.cache.CacheManager; @@ -8,10 +15,16 @@ import reactor.core.publisher.Mono; +import java.util.List; +import java.util.Set; + @RestController @RequestMapping public class MyController { + @Autowired + Validator validator; + private final CacheManager configCacheManager; public MyController(CacheManager configCacheManager) { @@ -36,9 +49,37 @@ public Mono world( ); } + + /** + * Example of how a filter configuration can be updated during runtime + * @param filterId id of the filter to update + * @param newConfig the new filter configuration + * @return + */ @PostMapping("filters/{filterId}") - public ResponseEntity updateConfig(@PathVariable String filterId, @RequestBody Bucket4JConfiguration filter) { - configCacheManager.setValue(filterId, filter); + public ResponseEntity updateConfig( + @PathVariable String filterId, + @RequestBody Bucket4JConfiguration newConfig) { + + //validate that there are no errors by the Jakarta validation + Set> violations = validator.validate(newConfig); + if (!violations.isEmpty()) { + List errors = violations.stream().map(ConstraintViolation::getMessage).toList(); + return ResponseEntity.badRequest().body(new ValidationErrorResponse("Configuration validation failed", errors)); + } + + //retrieve the old config and validate that it can be replaced by the new config + Bucket4JConfiguration oldConfig = configCacheManager.getValue(filterId); + ResponseEntity validationResponse = Bucket4JUtils.validateConfigurationUpdate(oldConfig, newConfig); + if (validationResponse != null) { + return validationResponse; + } + + //insert the new config into the cache, so it will trigger the cacheUpdateListeners + configCacheManager.setValue(filterId, newConfig); + return ResponseEntity.ok().build(); } + + private record ValidationErrorResponse(String message, List errors) {} } \ No newline at end of file