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 <polifr@gmail.com>

* Fix predicate method

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Factory fixes and junit test coverage

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Fix on predicate check for tests

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Regexp management via predicate and configuration extension

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Validation enforcing - tryout

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Checkstyle formatting fix

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Deletion of QueryParamRoutePredicateFactory class and test

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Update in QueryRoutePredicateFactory creation with Predicate

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Unit test update

Signed-off-by: Francesco Poli <polifr@gmail.com>

* Update QueryRoutePredicateFactoryPredicateTests.java

Fix copyright header comment

Signed-off-by: Francesco Poli <polifr@gmail.com>

---------

Signed-off-by: Francesco Poli <polifr@gmail.com>
This commit is contained in:
Francesco Poli
2025-03-03 21:13:45 +01:00
committed by GitHub
parent 0affb76a1a
commit ed9a24a72e
6 changed files with 198 additions and 5 deletions

View File

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

View File

@@ -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<Qu
*/
public static final String REGEXP_KEY = "regexp";
/**
* Predicate key.
*/
public static final String PREDICATE_KEY = "predicate";
public QueryRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> 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<Qu
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
if (!StringUtils.hasText(config.regexp)) {
if (!StringUtils.hasText(config.regexp) && config.predicate == null) {
// check existence of header
return exchange.getRequest().getQueryParams().containsKey(config.param);
}
@@ -63,8 +69,13 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory<Qu
if (values == null) {
return false;
}
Predicate<String> 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<Qu
private String regexp;
private Predicate<String> predicate;
public String getParam() {
return param;
return this.param;
}
public Config setParam(String param) {
@@ -100,7 +113,7 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory<Qu
}
public String getRegexp() {
return regexp;
return this.regexp;
}
public Config setRegexp(String regexp) {
@@ -108,6 +121,26 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory<Qu
return this;
}
public Predicate<String> getPredicate() {
return this.predicate;
}
public Config setPredicate(Predicate<String> 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 <code>false</code> 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);
}
}
}

View File

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

View File

@@ -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 <code>predicate</code> 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<ServerWebExchange> 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<ServerWebExchange> 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<String> queryParamPredicate() {
return p -> p == null ? false : p.length() > PARAM_LENGTH;
}
}
}

View File

@@ -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 <code>regex</code> parameter.
*
* @see QueryRoutePredicateFactory
*/
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
@ExtendWith(OutputCaptureExtension.class)

View File

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