Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query param route predicate - extension of QueryRoutePredicateFactory #3472

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.handler.predicate.QueryParamRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
Expand Down Expand Up @@ -206,6 +207,7 @@ public StringToZonedDateTimeConverter stringToZonedDateTimeConverter() {
* @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();
Expand Down Expand Up @@ -470,6 +472,12 @@ public PathRoutePredicateFactory pathRoutePredicateFactory() {
return new PathRoutePredicateFactory();
}

@Bean
@ConditionalOnEnabledPredicate
public QueryParamRoutePredicateFactory queryParamRoutePredicateFactory() {
return new QueryParamRoutePredicateFactory();
}

@Bean
@ConditionalOnEnabledPredicate
public QueryRoutePredicateFactory queryRoutePredicateFactory() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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.List;
import java.util.function.Predicate;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;

/**
* A predicate that checks if a query parameter value matches criteria of a given
* predicate.
*
* @author Francesco Poli
*/
public class QueryParamRoutePredicateFactory
extends AbstractRoutePredicateFactory<QueryParamRoutePredicateFactory.Config> {

public QueryParamRoutePredicateFactory() {
super(Config.class);
}

@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {

@Override
public boolean test(ServerWebExchange exchange) {
List<String> values = exchange.getRequest().getQueryParams().get(config.param);
if (values == null) {
return false;
}
for (String value : values) {
if (value != null && config.predicate.test(value)) {
return true;
}
}
return false;
}

@Override
public Object getConfig() {
return config;
}

@Override
public String toString() {
return String.format("QueryParam: param=%s", config.param);
}
};
}

/**
* {@link QueryParamRoutePredicateFactory} configuration class.
*
* @author Francesco Poli
*/
@Validated
public static class Config {

@NotEmpty
private String param;

@NotNull
private Predicate<String> predicate;

public Config setParam(String param) {
this.param = param;
return this;
}

public Config setPredicate(Predicate<String> predicate) {
this.predicate = predicate;
return this;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

import jakarta.validation.constraints.AssertTrue;
spencergibb marked this conversation as resolved.
Show resolved Hide resolved
import jakarta.validation.constraints.NotEmpty;

import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
Expand All @@ -41,21 +40,26 @@ 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
public Predicate<ServerWebExchange> apply(Config config) {
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);
}
Expand All @@ -64,8 +68,13 @@ public boolean test(ServerWebExchange exchange) {
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;
}
}
Expand All @@ -92,6 +101,8 @@ public static class Config {

private String regexp;

private Predicate<String> predicate;

public String getParam() {
return param;
}
Expand All @@ -110,6 +121,26 @@ public Config setRegexp(String regexp) {
return this;
}

public Predicate<String> getPredicate() {
return 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
spencergibb marked this conversation as resolved.
Show resolved Hide resolved
public boolean isValid() {
return !(StringUtils.hasText(regexp) && predicate != null);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.QueryParamRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
Expand Down Expand Up @@ -204,6 +205,18 @@ public <T> BooleanSpec readBody(Class<T> inClass, Predicate<T> predicate) {
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 queryParam(String param, Predicate<String> predicate) {
return asyncPredicate(getBean(QueryParamRoutePredicateFactory.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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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.QueryParamRoutePredicateFactory.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.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

/**
* Test class for {@link QueryParamRoutePredicateFactory}.
*
* @see QueryParamRoutePredicateFactory
* @author Francesco Poli
*/
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
@ExtendWith(OutputCaptureExtension.class)
public class QueryParamRoutePredicateFactoryTests 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 QueryParamRoutePredicateFactory().apply(config);
assertTrue(predicate instanceof HasConfig, "Incongruent types for predicate");
assertSame(config, ((HasConfig) predicate).getConfig(), "Incongruent config");
}

@Test
public void toStringFormat() {
Config config = new Config();
config.setParam("query_param");
Predicate<ServerWebExchange> predicate = new QueryParamRoutePredicateFactory().apply(config);
assertThat(predicate.toString()).contains("QueryParam: 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 queryParamRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("foo_query_param",
r -> r.queryParam("foo", queryParamPredicate())
.filters(f -> f.prefixPath("/httpbin"))
.uri(this.uri))
.build();
}

private Predicate<String> queryParamPredicate() {
return p -> p == null ? false : p.length() > PARAM_LENGTH;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactoryTests.class,
org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactoryTests.class,
org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactoryTests.class,
org.springframework.cloud.gateway.handler.predicate.QueryParamRoutePredicateFactoryTests.class,
org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactoryTests.class,
org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactoryIntegrationTests.class,
org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactoryTests.class,
Expand Down
Loading