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 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9619d0b
Creation of QueryParamRoutePredicateFactory
polifr Jul 21, 2024
22db8b4
Fix predicate method
polifr Jul 23, 2024
fe522b7
Merge branch 'main' into param-route-predicate
polifr Jul 23, 2024
97d9d7f
Factory fixes and junit test coverage
polifr Jul 23, 2024
0bc5ceb
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Oct 1, 2024
7a61e97
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Oct 3, 2024
15a1af6
Fix on predicate check for tests
polifr Oct 3, 2024
b229a0e
Regexp management via predicate and configuration extension
polifr Oct 3, 2024
b4abeb8
Validation enforcing - tryout
polifr Oct 7, 2024
6757b56
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Oct 7, 2024
28e28fc
Checkstyle formatting fix
polifr Oct 15, 2024
660b99b
Deletion of QueryParamRoutePredicateFactory class and test
polifr Oct 15, 2024
085d304
Update in QueryRoutePredicateFactory creation with Predicate
polifr Oct 15, 2024
dbdeff6
Unit test update
polifr Oct 15, 2024
aa3f958
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Oct 15, 2024
4a30a11
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Oct 17, 2024
3b0b73f
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Nov 5, 2024
8d2af03
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Nov 29, 2024
1e97332
Merge branch 'spring-cloud:main' into param-route-predicate
polifr Dec 23, 2024
5ec66fe
Update QueryRoutePredicateFactoryPredicateTests.java
polifr Dec 23, 2024
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 @@ -208,6 +208,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 @@ -40,21 +41,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 @@ -63,8 +69,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 @@ -90,8 +101,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 @@ -100,14 +113,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 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;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,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 @@ -62,7 +67,7 @@ public void noQueryParamWorks(CapturedOutput output) {

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

@Test
public void emptyQueryParamWorks(CapturedOutput output) {
testClient.get()
this.testClient.get()
.uri("/get?foo")
.exchange()
.expectStatus()
Expand Down Expand Up @@ -128,7 +133,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