diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java index 0746b831..65e979e9 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java @@ -39,6 +39,10 @@ public interface AsyncPredicate extends Function> { return new NegateAsyncPredicate<>(this); } + default AsyncPredicate not(AsyncPredicate other) { + return new NegateAsyncPredicate<>(other); + } + default AsyncPredicate or(AsyncPredicate other) { return new OrAsyncPredicate<>(this, other); } @@ -84,7 +88,7 @@ public interface AsyncPredicate extends Function> { @Override public String toString() { - return String.format("!%s", this.predicate); + return String.format("!(%s)", this.predicate); } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java index e9c3de41..7d6bdf99 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java @@ -113,6 +113,27 @@ public class BooleanSpec extends UriSpec { return new BooleanSpec(this.routeBuilder, this.builder); } + public BooleanSpec not(Function fn) { + return fn + .apply(new NotOpSpec(this.routeBuilder, this.builder, this.operator)); + } + + } + + public static class NotOpSpec extends BooleanOpSpec { + + NotOpSpec(Route.AsyncBuilder routeBuilder, RouteLocatorBuilder.Builder builder, + Operator operator) { + super(routeBuilder, builder, operator); + } + + @Override + public BooleanSpec asyncPredicate(AsyncPredicate predicate) { + AsyncPredicate negated = this.routeBuilder.getPredicate() + .not(predicate); + return super.asyncPredicate(negated); + } + } } 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 bed803cc..7826cea6 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 @@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.route.builder; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collections; +import java.util.function.Function; import java.util.function.Predicate; import org.springframework.cloud.gateway.handler.AsyncPredicate; @@ -309,4 +310,8 @@ public class PredicateSpec extends UriSpec { return predicate(exchange -> true); } + public BooleanSpec not(Function fn) { + return alwaysTrue().and().not(fn); + } + } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java index 8b02dec9..80118149 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java @@ -74,6 +74,20 @@ public class AsyncPredicateTest { right.assertTested(); } + @Test + public void negateOperatorWorks() { + TestAsyncPredicate falsePredicate = new TestAsyncPredicate<>(o -> false); + TestAsyncPredicate truePredicate = new TestAsyncPredicate<>(o -> true); + Publisher falseNot = falsePredicate.negate().apply(new Object()); + Publisher trueNot = truePredicate.negate().apply(new Object()); + + StepVerifier.create(falseNot).expectNext(true).expectComplete().verify(); + StepVerifier.create(trueNot).expectNext(false).expectComplete().verify(); + + falsePredicate.assertTested(); + truePredicate.assertTested(); + } + /** * An AsyncPredicate decorator that records if the apply method was called. */ diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java index de8f7152..1627bc5a 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java @@ -21,14 +21,21 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +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.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 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.test.context.junit4.SpringRunner; import org.springframework.util.SocketUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @@ -57,11 +64,51 @@ public class RoutePredicateHandlerMappingIntegrationTests extends BaseWebClientT .uri("/get").exchange().expectStatus().isNotFound(); } + @Test + public void andNotWorksWithMissingParameter() { + testClient.get().uri("/andnotquery").exchange().expectBody(String.class) + .isEqualTo("notsupplied"); + } + + @Test + public void andNotWorksWithParameter() { + testClient.get().uri("/andnotquery?myquery=shouldnotsee").exchange() + .expectBody(String.class).isEqualTo("hasquery"); + } + @EnableAutoConfiguration @SpringBootConfiguration @Import(DefaultTestConfig.class) + @RestController public static class TestConfig { + @Value("${test.uri:http://httpbin.org:80}") + String uri; + + @GetMapping("/httpbin/andnotquery") + String andnotquery(@RequestParam(name = "myquery", + defaultValue = "notsupplied") String myquery) { + return myquery; + } + + @GetMapping("/httpbin/hasquery") + String hasquery() { + return "hasquery"; + } + + @Bean + RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("and_not_missing_myquery", + r -> r.path("/andnotquery").and().not(p -> p.query("myquery")) + .filters(f -> f.prefixPath("/httpbin")).uri(uri)) + .route("and_not_has_myquery", + r -> r.path("/andnotquery").and().query("myquery") + .filters(f -> f.setPath("/httpbin/hasquery")) + .uri(uri)) + .build(); + } + } }