From ed9a24a72e092d39a260cb39dc4d7c3f334579f4 Mon Sep 17 00:00:00 2001 From: Francesco Poli Date: Mon, 3 Mar 2025 21:13:45 +0100 Subject: [PATCH] Query param route predicate - extension of QueryRoutePredicateFactory (#3472) * Creation of QueryParamRoutePredicateFactory A predicate that checks if a query parameter value matches criteria of a given predicate. Signed-off-by: Francesco Poli * Fix predicate method Signed-off-by: Francesco Poli * Factory fixes and junit test coverage Signed-off-by: Francesco Poli * Fix on predicate check for tests Signed-off-by: Francesco Poli * Regexp management via predicate and configuration extension Signed-off-by: Francesco Poli * Validation enforcing - tryout Signed-off-by: Francesco Poli * Checkstyle formatting fix Signed-off-by: Francesco Poli * Deletion of QueryParamRoutePredicateFactory class and test Signed-off-by: Francesco Poli * Update in QueryRoutePredicateFactory creation with Predicate Signed-off-by: Francesco Poli * Unit test update Signed-off-by: Francesco Poli * Update QueryRoutePredicateFactoryPredicateTests.java Fix copyright header comment Signed-off-by: Francesco Poli --------- Signed-off-by: Francesco Poli --- .../config/GatewayAutoConfiguration.java | 1 + .../predicate/QueryRoutePredicateFactory.java | 43 +++++- .../gateway/route/builder/PredicateSpec.java | 12 ++ ...ryRoutePredicateFactoryPredicateTests.java | 141 ++++++++++++++++++ .../QueryRoutePredicateFactoryTests.java | 5 + .../cloud/gateway/test/AdhocTestSuite.java | 1 + 6 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryPredicateTests.java diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 85767b84..72c55d60 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -210,6 +210,7 @@ public class GatewayAutoConfiguration { * @deprecated in favour of * {@link org.springframework.cloud.gateway.support.config.KeyValueConverter} */ + @Deprecated @Bean public org.springframework.cloud.gateway.support.KeyValueConverter deprecatedKeyValueConverter() { return new org.springframework.cloud.gateway.support.KeyValueConverter(); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java index 1a7fd9b1..f8c1425f 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; import java.util.function.Predicate; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import org.springframework.util.StringUtils; @@ -40,13 +41,18 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory shortcutFieldOrder() { - return Arrays.asList(PARAM_KEY, REGEXP_KEY); + return Arrays.asList(PARAM_KEY, REGEXP_KEY, PREDICATE_KEY); } @Override @@ -54,7 +60,7 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory predicate = config.predicate; + if (StringUtils.hasText(config.regexp)) { + predicate = value -> value.matches(config.regexp); + } for (String value : values) { - if (value != null && value.matches(config.regexp)) { + if (value != null && predicate.test(value)) { return true; } } @@ -90,8 +101,10 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory predicate; + public String getParam() { - return param; + return this.param; } public Config setParam(String param) { @@ -100,7 +113,7 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory getPredicate() { + return this.predicate; + } + + public Config setPredicate(Predicate predicate) { + this.predicate = predicate; + return this; + } + + /** + * Enforces the validation done on predicate configuration: {@link #regexp} and + * {@link #predicate} can't be both set at runtime. + * @return false if {@link #regexp} and {@link #predicate} are both + * set in this predicate factory configuration + */ + @AssertTrue + public boolean isValid() { + return !(StringUtils.hasText(this.regexp) && this.predicate != null); + } + } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java index 7068c68a..fb70de68 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -204,6 +204,18 @@ public class PredicateSpec extends UriSpec { getBean(ReadBodyRoutePredicateFactory.class).applyAsync(c -> c.setPredicate(inClass, predicate))); } + /** + * A predicate that checks if a query parameter value matches criteria of a given + * predicate. + * @param param the query parameter name + * @param predicate a predicate to check the value of the param + * @return a {@link BooleanSpec} to be used to add logical operators + */ + public BooleanSpec query(String param, Predicate predicate) { + return asyncPredicate( + getBean(QueryRoutePredicateFactory.class).applyAsync(c -> c.setParam(param).setPredicate(predicate))); + } + /** * A predicate that checks if a query parameter matches a regular expression. * @param param the query parameter name diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryPredicateTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryPredicateTests.java new file mode 100644 index 00000000..69765659 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryPredicateTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.handler.predicate; + +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory.Config; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.support.HasConfig; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * Test class for {@link QueryRoutePredicateFactory} for predicate parameter. + * + * @see QueryRoutePredicateFactory + */ +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +@ExtendWith(OutputCaptureExtension.class) +public class QueryRoutePredicateFactoryPredicateTests extends BaseWebClientTests { + + @Test + public void noQueryParamWorks(CapturedOutput output) { + this.testClient.get() + .uri("/get") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin"); + assertThat(output).doesNotContain("Error applying predicate for route: foo_query_param"); + } + + @Test + public void queryParamPredicateTrue() { + this.testClient.get() + .uri("/get?foo=1234567") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals(ROUTE_ID_HEADER, "foo_query_param"); + } + + @Test + public void queryParamPredicateFalse(CapturedOutput output) { + this.testClient.get() + .uri("/get?foo=123") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin"); + assertThat(output).doesNotContain("Error applying predicate for route: foo_query_param"); + } + + @Test + public void emptyQueryParamWorks(CapturedOutput output) { + this.testClient.get() + .uri("/get?foo") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin"); + assertThat(output).doesNotContain("Error applying predicate for route: foo_query_param"); + } + + @Test + public void testConfig() { + Config config = new Config(); + config.setParam("query_param"); + Predicate predicate = new QueryRoutePredicateFactory().apply(config); + assertThat(predicate).isInstanceOf(HasConfig.class); + assertThat(config).isSameAs(((HasConfig) predicate).getConfig()); + } + + @Test + public void toStringFormat() { + Config config = new Config(); + config.setParam("query_param"); + Predicate predicate = new QueryRoutePredicateFactory().apply(config); + assertThat(predicate.toString()).contains("Query: param=query_param"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + private static final int PARAM_LENGTH = 5; + + @Value("${test.uri}") + private String uri; + + @Bean + RouteLocator queryRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("foo_query_param", + r -> r.query("foo", queryParamPredicate()).filters(f -> f.prefixPath("/httpbin")).uri(this.uri)) + .build(); + } + + private Predicate queryParamPredicate() { + return p -> p == null ? false : p.length() > PARAM_LENGTH; + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java index 4506e20f..6a4c7264 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java @@ -43,6 +43,11 @@ import org.springframework.test.annotation.DirtiesContext; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +/** + * Test class for {@link QueryRoutePredicateFactory} for regex parameter. + * + * @see QueryRoutePredicateFactory + */ @SpringBootTest(webEnvironment = RANDOM_PORT) @DirtiesContext @ExtendWith(OutputCaptureExtension.class) diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java index 5d1bf14f..38041174 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java @@ -46,6 +46,7 @@ import static org.junit.Assume.assumeThat; org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactoryTests.class, org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactoryTests.class, org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactoryTests.class, + org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactoryPredicateTests.class, org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactoryIntegrationTests.class, org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactoryTests.class, org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactoryTests.class,