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 all 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 @@ -206,6 +206,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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand All @@ -41,21 +42,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 +70,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,8 +103,10 @@ public static class Config {

private String regexp;

private Predicate<String> predicate;

public String getParam() {
return param;
return this.param;
}

public Config setParam(String param) {
Expand All @@ -102,14 +115,34 @@ public Config setParam(String param) {
}

public String getRegexp() {
return regexp;
return this.regexp;
}

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

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,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 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
Expand Down Expand Up @@ -287,7 +299,7 @@ public BooleanSpec xForwardedRemoteAddr(String... addrs) {
*/
public BooleanSpec weight(String group, int weight) {
return asyncPredicate(getBean(WeightRoutePredicateFactory.class)
.applyAsync(c -> c.setGroup(group).setRouteId(routeBuilder.getId()).setWeight(weight)));
.applyAsync(c -> c.setGroup(group).setRouteId(this.routeBuilder.getId()).setWeight(weight)));
}

public BooleanSpec cloudFoundryRouteService() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright 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;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,19 @@
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)
public class QueryRoutePredicateFactoryTests extends BaseWebClientTests {

@Test
public void noQueryParamWorks(CapturedOutput output) {
testClient.get()
this.testClient.get()
.uri("/get")
.exchange()
.expectStatus()
Expand All @@ -57,7 +62,7 @@ public void noQueryParamWorks(CapturedOutput output) {

@Test
public void queryParamWorks() {
testClient.get()
this.testClient.get()
.uri("/get?foo=bar")
.exchange()
.expectStatus()
Expand All @@ -68,7 +73,7 @@ public void queryParamWorks() {

@Test
public void emptyQueryParamWorks(CapturedOutput output) {
testClient.get()
this.testClient.get()
.uri("/get?foo")
.exchange()
.expectStatus()
Expand Down Expand Up @@ -98,7 +103,8 @@ public static class TestConfig {
@Bean
RouteLocator queryRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("foo_query_param", r -> r.query("foo", "bar").filters(f -> f.prefixPath("/httpbin")).uri(uri))
.route("foo_query_param",
r -> r.query("foo", "bar").filters(f -> f.prefixPath("/httpbin")).uri(this.uri))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
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,
Expand Down