diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java index a8c7b8f325..bcbd3817ea 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java @@ -21,6 +21,7 @@ import java.util.function.Predicate; import org.reactivestreams.Publisher; + import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; @@ -82,6 +83,23 @@ public void accept(Visitor visitor) { } } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultAsyncPredicate)) { + return false; + } + DefaultAsyncPredicate that = (DefaultAsyncPredicate) o; + return Objects.equals(this.delegate, that.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.delegate); + } + } class NegateAsyncPredicate implements AsyncPredicate { @@ -95,7 +113,7 @@ public NegateAsyncPredicate(AsyncPredicate predicate) { @Override public Publisher apply(T t) { - return Mono.from(predicate.apply(t)).map(b -> !b); + return Mono.from(predicate.apply(t)).map(result -> !result); } @Override @@ -103,6 +121,23 @@ public String toString() { return String.format("!(%s)", this.predicate); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof NegateAsyncPredicate)) { + return false; + } + NegateAsyncPredicate that = (NegateAsyncPredicate) o; + return Objects.equals(this.predicate, that.predicate); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.predicate); + } + } class AndAsyncPredicate implements AsyncPredicate { @@ -120,7 +155,8 @@ public AndAsyncPredicate(AsyncPredicate left, AsyncPredicate apply(T t) { - return Mono.from(left.apply(t)).flatMap(result -> !result ? Mono.just(false) : Mono.from(right.apply(t))); + return Mono.from(left.apply(t)) + .flatMap(result -> !result ? Mono.just(false) : Mono.from(right.apply(t))); } @Override @@ -134,6 +170,23 @@ public String toString() { return String.format("(%s && %s)", this.left, this.right); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AndAsyncPredicate)) { + return false; + } + AndAsyncPredicate that = (AndAsyncPredicate) o; + return Objects.equals(this.left, that.left) && Objects.equals(this.right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(this.left, this.right); + } + } class OrAsyncPredicate implements AsyncPredicate { @@ -151,7 +204,8 @@ public OrAsyncPredicate(AsyncPredicate left, AsyncPredicate apply(T t) { - return Mono.from(left.apply(t)).flatMap(result -> result ? Mono.just(true) : Mono.from(right.apply(t))); + return Mono.from(left.apply(t)) + .flatMap(result -> result ? Mono.just(true) : Mono.from(right.apply(t))); } @Override @@ -165,6 +219,23 @@ public String toString() { return String.format("(%s || %s)", this.left, this.right); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OrAsyncPredicate)) { + return false; + } + OrAsyncPredicate that = (OrAsyncPredicate) o; + return Objects.equals(this.left, that.left) && Objects.equals(this.right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(this.left, this.right); + } + } } diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/GatewayPredicate.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/GatewayPredicate.java index 84121a1909..29f638669e 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/GatewayPredicate.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/handler/predicate/GatewayPredicate.java @@ -23,6 +23,10 @@ import org.springframework.cloud.gateway.support.Visitor; import org.springframework.web.server.ServerWebExchange; +/** + * A {@link Predicate} that is specific to Spring Cloud Gateway, providing additional + * methods for composing predicates and visiting them. + */ public interface GatewayPredicate extends Predicate, HasConfig { @Override @@ -46,7 +50,6 @@ default void accept(Visitor visitor) { static GatewayPredicate wrapIfNeeded(Predicate other) { GatewayPredicate right; - if (other instanceof GatewayPredicate gatewayPredicate) { right = gatewayPredicate; } @@ -82,6 +85,23 @@ public String toString() { return this.delegate.getClass().getSimpleName(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GatewayPredicateWrapper)) { + return false; + } + GatewayPredicateWrapper that = (GatewayPredicateWrapper) o; + return Objects.equals(this.delegate, that.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.delegate); + } + } class NegateGatewayPredicate implements GatewayPredicate { @@ -94,8 +114,8 @@ public NegateGatewayPredicate(GatewayPredicate predicate) { } @Override - public boolean test(ServerWebExchange t) { - return !this.predicate.test(t); + public boolean test(ServerWebExchange exchange) { + return !this.predicate.test(exchange); } @Override @@ -108,6 +128,23 @@ public String toString() { return String.format("!%s", this.predicate); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof NegateGatewayPredicate)) { + return false; + } + NegateGatewayPredicate that = (NegateGatewayPredicate) o; + return Objects.equals(this.predicate, that.predicate); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.predicate); + } + } class AndGatewayPredicate implements GatewayPredicate { @@ -124,8 +161,8 @@ public AndGatewayPredicate(GatewayPredicate left, GatewayPredicate right) { } @Override - public boolean test(ServerWebExchange t) { - return (this.left.test(t) && this.right.test(t)); + public boolean test(ServerWebExchange exchange) { + return this.left.test(exchange) && this.right.test(exchange); } @Override @@ -139,6 +176,23 @@ public String toString() { return String.format("(%s && %s)", this.left, this.right); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AndGatewayPredicate)) { + return false; + } + AndGatewayPredicate that = (AndGatewayPredicate) o; + return Objects.equals(this.left, that.left) && Objects.equals(this.right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(this.left, this.right); + } + } class OrGatewayPredicate implements GatewayPredicate { @@ -155,8 +209,8 @@ public OrGatewayPredicate(GatewayPredicate left, GatewayPredicate right) { } @Override - public boolean test(ServerWebExchange t) { - return (this.left.test(t) || this.right.test(t)); + public boolean test(ServerWebExchange exchange) { + return this.left.test(exchange) || this.right.test(exchange); } @Override @@ -170,6 +224,23 @@ public String toString() { return String.format("(%s || %s)", this.left, this.right); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OrGatewayPredicate)) { + return false; + } + OrGatewayPredicate that = (OrGatewayPredicate) o; + return Objects.equals(this.left, that.left) && Objects.equals(this.right, that.right); + } + + @Override + public int hashCode() { + return Objects.hash(this.left, this.right); + } + } } diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateEqualsTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateEqualsTests.java new file mode 100644 index 0000000000..a63d7a4ece --- /dev/null +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateEqualsTests.java @@ -0,0 +1,261 @@ +/* + * Copyright 2013-present 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; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.gateway.handler.AsyncPredicate.AndAsyncPredicate; +import org.springframework.cloud.gateway.handler.AsyncPredicate.NegateAsyncPredicate; +import org.springframework.cloud.gateway.handler.AsyncPredicate.OrAsyncPredicate; +import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; +import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate.AndGatewayPredicate; +import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate.NegateGatewayPredicate; +import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate.OrGatewayPredicate; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.get; + +/** + * Tests for {@link AsyncPredicate} and {@link GatewayPredicate} equality contracts. + * + * Relates to: https://github.com/spring-cloud/spring-cloud-gateway/issues/3236 + */ +class AsyncPredicateEqualsTests { + + // ----------------------------------------------------------------------- + // DefaultAsyncPredicate equality + // ----------------------------------------------------------------------- + + @Test + void defaultAsyncPredicateSameInstanceIsEqual() { + AsyncPredicate predicate = buildPathPredicate("/api"); + assertThat(predicate).isEqualTo(predicate); + } + + @Test + void defaultAsyncPredicateWithSameDelegateIsEqual() { + GatewayPredicate delegate = buildGatewayPredicate("/api"); + AsyncPredicate p1 = new AsyncPredicate.DefaultAsyncPredicate<>(delegate); + AsyncPredicate p2 = new AsyncPredicate.DefaultAsyncPredicate<>(delegate); + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void defaultAsyncPredicateWithDifferentDelegateIsNotEqual() { + AsyncPredicate p1 = buildPathPredicate("/api"); + AsyncPredicate p2 = buildPathPredicate("/other"); + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void defaultAsyncPredicateIsNotEqualToNull() { + AsyncPredicate predicate = buildPathPredicate("/api"); + assertThat(predicate).isNotEqualTo(null); + } + + // ----------------------------------------------------------------------- + // NegateAsyncPredicate equality + // ----------------------------------------------------------------------- + + @Test + void negateAsyncPredicateWithSameInnerPredicateIsEqual() { + AsyncPredicate inner = buildPathPredicate("/api"); + NegateAsyncPredicate p1 = new NegateAsyncPredicate<>(inner); + NegateAsyncPredicate p2 = new NegateAsyncPredicate<>(inner); + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void negateAsyncPredicateWithDifferentInnerPredicateIsNotEqual() { + NegateAsyncPredicate p1 = new NegateAsyncPredicate<>(buildPathPredicate("/a")); + NegateAsyncPredicate p2 = new NegateAsyncPredicate<>(buildPathPredicate("/b")); + assertThat(p1).isNotEqualTo(p2); + } + + // ----------------------------------------------------------------------- + // AndAsyncPredicate equality + // ----------------------------------------------------------------------- + + @Test + void andAsyncPredicateWithSameComponentsIsEqual() { + AsyncPredicate left = buildPathPredicate("/api"); + AsyncPredicate right = buildPathPredicate("/v1"); + AndAsyncPredicate p1 = new AndAsyncPredicate<>(left, right); + AndAsyncPredicate p2 = new AndAsyncPredicate<>(left, right); + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void andAsyncPredicateWithDifferentLeftIsNotEqual() { + AsyncPredicate right = buildPathPredicate("/v1"); + AndAsyncPredicate p1 = new AndAsyncPredicate<>(buildPathPredicate("/api"), right); + AndAsyncPredicate p2 = new AndAsyncPredicate<>(buildPathPredicate("/other"), right); + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void andAsyncPredicateWithDifferentRightIsNotEqual() { + AsyncPredicate left = buildPathPredicate("/api"); + AndAsyncPredicate p1 = new AndAsyncPredicate<>(left, buildPathPredicate("/v1")); + AndAsyncPredicate p2 = new AndAsyncPredicate<>(left, buildPathPredicate("/v2")); + assertThat(p1).isNotEqualTo(p2); + } + + // ----------------------------------------------------------------------- + // OrAsyncPredicate equality + // ----------------------------------------------------------------------- + + @Test + void orAsyncPredicateWithSameComponentsIsEqual() { + AsyncPredicate left = buildPathPredicate("/api"); + AsyncPredicate right = buildPathPredicate("/v1"); + OrAsyncPredicate p1 = new OrAsyncPredicate<>(left, right); + OrAsyncPredicate p2 = new OrAsyncPredicate<>(left, right); + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void orAsyncPredicateWithDifferentComponentsIsNotEqual() { + AsyncPredicate left = buildPathPredicate("/api"); + OrAsyncPredicate p1 = new OrAsyncPredicate<>(left, buildPathPredicate("/v1")); + OrAsyncPredicate p2 = new OrAsyncPredicate<>(left, buildPathPredicate("/v2")); + assertThat(p1).isNotEqualTo(p2); + } + + // ----------------------------------------------------------------------- + // GatewayPredicate inner class equality + // ----------------------------------------------------------------------- + + @Test + void negateGatewayPredicateWithSameInnerIsEqual() { + GatewayPredicate inner = buildGatewayPredicate("/api"); + NegateGatewayPredicate p1 = new NegateGatewayPredicate(inner); + NegateGatewayPredicate p2 = new NegateGatewayPredicate(inner); + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void andGatewayPredicateWithSameComponentsIsEqual() { + GatewayPredicate left = buildGatewayPredicate("/api"); + GatewayPredicate right = buildGatewayPredicate("/v1"); + AndGatewayPredicate p1 = new AndGatewayPredicate(left, right); + AndGatewayPredicate p2 = new AndGatewayPredicate(left, right); + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void orGatewayPredicateWithSameComponentsIsEqual() { + GatewayPredicate left = buildGatewayPredicate("/api"); + GatewayPredicate right = buildGatewayPredicate("/v1"); + OrGatewayPredicate p1 = new OrGatewayPredicate(left, right); + OrGatewayPredicate p2 = new OrGatewayPredicate(left, right); + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + // ----------------------------------------------------------------------- + // Composite predicate (via .and() / .or() / .negate()) equality + // ----------------------------------------------------------------------- + + @Test + void compositeAndPredicateBuiltFromSamePartsIsEqual() { + GatewayPredicate left = buildGatewayPredicate("/api"); + GatewayPredicate right = buildGatewayPredicate("/v1"); + + AsyncPredicate p1 = AsyncPredicate.from(left).and(AsyncPredicate.from(right)); + AsyncPredicate p2 = AsyncPredicate.from(left).and(AsyncPredicate.from(right)); + + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void compositeOrPredicateBuiltFromSamePartsIsEqual() { + GatewayPredicate left = buildGatewayPredicate("/api"); + GatewayPredicate right = buildGatewayPredicate("/v1"); + + AsyncPredicate p1 = AsyncPredicate.from(left).or(AsyncPredicate.from(right)); + AsyncPredicate p2 = AsyncPredicate.from(left).or(AsyncPredicate.from(right)); + + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + @Test + void negatedPredicateBuiltFromSamePartIsEqual() { + GatewayPredicate inner = buildGatewayPredicate("/api"); + + AsyncPredicate p1 = AsyncPredicate.from(inner).negate(); + AsyncPredicate p2 = AsyncPredicate.from(inner).negate(); + + assertThat(p1).isEqualTo(p2); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + /** + * Creates an AsyncPredicate wrapping a simple path-matching GatewayPredicate. + * Uses a concrete GatewayPredicate so equals() can compare by config value. + */ + private AsyncPredicate buildPathPredicate(String path) { + return AsyncPredicate.from(buildGatewayPredicate(path)); + } + + /** + * Builds a simple GatewayPredicate that checks if the request path starts with the + * given prefix. Overrides equals/hashCode so two instances with the same prefix + * are considered equal — this mirrors what factory-created predicates should do. + */ + private GatewayPredicate buildGatewayPredicate(String pathPrefix) { + return new GatewayPredicate() { + @Override + public boolean test(ServerWebExchange exchange) { + return exchange.getRequest().getPath().value().startsWith(pathPrefix); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GatewayPredicate other)) return false; + // Compare by the toString representation which encodes the path + return this.toString().equals(other.toString()); + } + + @Override + public int hashCode() { + return pathPrefix.hashCode(); + } + + @Override + public String toString() { + return "PathPredicate[" + pathPrefix + "]"; + } + }; + } + +}