From 417e7e03d4fbe381e083bc2be09d94a10c473dfd Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 15 Oct 2020 15:52:58 +0200 Subject: [PATCH] Introduce RouterFunction attributes This commit introduces support for router function attributes, a way to associate meta-data with a route. Closes: gh-25938 --- .../ChangePathPatternParserVisitor.java | 5 + .../function/server/RouterFunction.java | 41 +++++ .../server/RouterFunctionBuilder.java | 30 ++++ .../function/server/RouterFunctions.java | 83 +++++++++ .../function/server/ToStringVisitor.java | 5 + .../server/AttributesTestVisitor.java | 73 ++++++++ .../function/server/CustomRouteBuilder.java | 157 ++++++++++++++++++ .../server/RouterFunctionBuilderTests.java | 18 ++ .../function/server/RouterFunctionTests.java | 21 +++ .../ChangePathPatternParserVisitor.java | 5 + .../web/servlet/function/RouterFunction.java | 41 +++++ .../function/RouterFunctionBuilder.java | 30 ++++ .../web/servlet/function/RouterFunctions.java | 84 ++++++++++ .../web/servlet/function/ToStringVisitor.java | 5 + .../function/AttributesTestVisitor.java | 72 ++++++++ .../function/RouterFunctionBuilderTests.java | 19 +++ .../servlet/function/RouterFunctionTests.java | 23 +++ 17 files changed, 712 insertions(+) create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/server/AttributesTestVisitor.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/server/CustomRouteBuilder.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/function/AttributesTestVisitor.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java index a6cba32fc56..10f34a3e765 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.function.server; +import java.util.Map; import java.util.function.Function; import reactor.core.publisher.Mono; @@ -60,6 +61,10 @@ class ChangePathPatternParserVisitor implements RouterFunctions.Visitor { public void resources(Function> lookupFunction) { } + @Override + public void attributes(Map attributes) { + } + @Override public void unknown(RouterFunction routerFunction) { } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunction.java index ba1482639ce..d57ff1924a2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunction.java @@ -16,8 +16,14 @@ package org.springframework.web.reactive.function.server; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + import reactor.core.publisher.Mono; +import org.springframework.util.Assert; + /** * Represents a function that routes to a {@linkplain HandlerFunction handler function}. * @@ -115,4 +121,39 @@ public interface RouterFunction { visitor.unknown(this); } + /** + * Return a new routing function with the given attribute. + * @param name the attribute name + * @param value the attribute value + * @return a function that has the specified attributes + * @since 5.3 + */ + default RouterFunction withAttribute(String name, Object value) { + Assert.hasLength(name, "Name must not be empty"); + Assert.notNull(value, "Value must not be null"); + + Map attributes = new LinkedHashMap<>(); + attributes.put(name, value); + return new RouterFunctions.AttributesRouterFunction<>(this, attributes); + } + + /** + * Return a new routing function with attributes manipulated with the given consumer. + *

The map provided to the consumer is "live", so that the consumer can be used + * to {@linkplain Map#put(Object, Object) overwrite} existing attributes, + * {@linkplain Map#remove(Object) remove} attributes, or use any of the other + * {@link Map} methods. + * @param attributesConsumer a function that consumes the attributes map + * @return this builder + * @since 5.3 + */ + default RouterFunction withAttributes(Consumer> attributesConsumer) { + Assert.notNull(attributesConsumer, "AttributesConsumer must not be null"); + + Map attributes = new LinkedHashMap<>(); + attributesConsumer.accept(attributes); + return new RouterFunctions.AttributesRouterFunction<>(this, attributes); + } + + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java index 8fd0cfb616e..f54ae924a01 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java @@ -18,6 +18,7 @@ package org.springframework.web.reactive.function.server; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -330,6 +331,35 @@ class RouterFunctionBuilder implements RouterFunctions.Builder { return this; } + @Override + public RouterFunctions.Builder withAttribute(String name, Object value) { + Assert.hasLength(name, "Name must not be empty"); + Assert.notNull(value, "Value must not be null"); + + if (this.routerFunctions.isEmpty()) { + throw new IllegalStateException("attributes can only be called after any other method (GET, path, etc.)"); + } + int lastIdx = this.routerFunctions.size() - 1; + RouterFunction attributed = this.routerFunctions.get(lastIdx) + .withAttribute(name, value); + this.routerFunctions.set(lastIdx, attributed); + return this; + } + + @Override + public RouterFunctions.Builder withAttributes(Consumer> attributesConsumer) { + Assert.notNull(attributesConsumer, "AttributesConsumer must not be null"); + + if (this.routerFunctions.isEmpty()) { + throw new IllegalStateException("attributes can only be called after any other method (GET, path, etc.)"); + } + int lastIdx = this.routerFunctions.size() - 1; + RouterFunction attributed = this.routerFunctions.get(lastIdx) + .withAttributes(attributesConsumer); + this.routerFunctions.set(lastIdx, attributed); + return this; + } + @Override public RouterFunction build() { if (this.routerFunctions.isEmpty()) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index f9f1d92bcf1..64d1e3ed9b6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -16,6 +16,8 @@ package org.springframework.web.reactive.function.server; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -858,6 +860,27 @@ public abstract class RouterFunctions { Builder onError(Class exceptionType, BiFunction> responseProvider); + /** + * Add an attribute with the given name and value to the last route built with this builder. + * @param name the attribute name + * @param value the attribute value + * @return this builder + * @since 5.3 + */ + Builder withAttribute(String name, Object value); + + /** + * Manipulate the attributes of the last route built with the given consumer. + *

The map provided to the consumer is "live", so that the consumer can be used + * to {@linkplain Map#put(Object, Object) overwrite} existing attributes, + * {@linkplain Map#remove(Object) remove} attributes, or use any of the other + * {@link Map} methods. + * @param attributesConsumer a function that consumes the attributes map + * @return this builder + * @since 5.3 + */ + Builder withAttributes(Consumer> attributesConsumer); + /** * Builds the {@code RouterFunction}. All created routes are * {@linkplain RouterFunction#and(RouterFunction) composed} with one another, and filters @@ -902,6 +925,14 @@ public abstract class RouterFunctions { */ void resources(Function> lookupFunction); + /** + * Receive notification of a router function with attributes. The + * given attributes apply to the router notification that follows this one. + * @param attributes the attributes that apply to the following router + * @since 5.3 + */ + void attributes(Map attributes); + /** * Receive notification of an unknown router function. This method is called for router * functions that were not created via the various {@link RouterFunctions} methods. @@ -1126,6 +1157,58 @@ public abstract class RouterFunctions { } + static final class AttributesRouterFunction extends AbstractRouterFunction { + + private final RouterFunction delegate; + + private final Map attributes; + + public AttributesRouterFunction(RouterFunction delegate, Map attributes) { + this.delegate = delegate; + this.attributes = initAttributes(attributes); + } + + private static Map initAttributes(Map attributes) { + if (attributes.isEmpty()) { + return Collections.emptyMap(); + } + else { + return Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); + } + } + + @Override + public Mono> route(ServerRequest request) { + return this.delegate.route(request); + } + + @Override + public void accept(Visitor visitor) { + visitor.attributes(this.attributes); + this.delegate.accept(visitor); + } + + @Override + public RouterFunction withAttribute(String name, Object value) { + Assert.hasLength(name, "Name must not be empty"); + Assert.notNull(value, "Value must not be null"); + + Map attributes = new LinkedHashMap<>(this.attributes); + attributes.put(name, value); + return new AttributesRouterFunction<>(this.delegate, attributes); + } + + @Override + public RouterFunction withAttributes(Consumer> attributesConsumer) { + Assert.notNull(attributesConsumer, "AttributesConsumer must not be null"); + + Map attributes = new LinkedHashMap<>(this.attributes); + attributesConsumer.accept(attributes); + return new AttributesRouterFunction<>(this.delegate, attributes); + } + } + + private static class HandlerStrategiesResponseContext implements ServerResponse.Context { private final HandlerStrategies strategies; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ToStringVisitor.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ToStringVisitor.java index dcfb74e5691..3adb2723580 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ToStringVisitor.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ToStringVisitor.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.function.server; +import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -69,6 +70,10 @@ class ToStringVisitor implements RouterFunctions.Visitor, RequestPredicates.Visi this.builder.append(lookupFunction).append('\n'); } + @Override + public void attributes(Map attributes) { + } + @Override public void unknown(RouterFunction routerFunction) { indent(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/AttributesTestVisitor.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/AttributesTestVisitor.java new file mode 100644 index 00000000000..7a97e8c3631 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/AttributesTestVisitor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2020 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.web.reactive.function.server; + +import java.util.Map; +import java.util.function.Function; + +import reactor.core.publisher.Mono; + +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Arjen Poutsma + */ +class AttributesTestVisitor implements RouterFunctions.Visitor { + + @Nullable + private Map attributes; + + private int visitCount; + + public int visitCount() { + return this.visitCount; + } + + @Override + public void startNested(RequestPredicate predicate) { + } + + @Override + public void endNested(RequestPredicate predicate) { + } + + @Override + public void route(RequestPredicate predicate, HandlerFunction handlerFunction) { + assertThat(this.attributes).isNotNull(); + this.attributes = null; + } + + @Override + public void resources(Function> lookupFunction) { + } + + @Override + public void attributes(Map attributes) { + assertThat(attributes).containsExactly(entry("foo", "bar"), entry("baz", "qux")); + this.attributes = attributes; + this.visitCount++; + } + + @Override + public void unknown(RouterFunction routerFunction) { + + } +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/CustomRouteBuilder.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/CustomRouteBuilder.java new file mode 100644 index 00000000000..f6f43611df1 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/CustomRouteBuilder.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2020 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.web.reactive.function.server; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import reactor.core.publisher.Mono; + +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +/** + * @author Arjen Poutsma + */ +public class CustomRouteBuilder { + + public static final String OPERATION_ATTRIBUTE = CustomRouteBuilder.class.getName() + ".operation"; + + private final RouterFunctions.Builder delegate = RouterFunctions.route(); + + + private CustomRouteBuilder() { + } + + public static CustomRouteBuilder route() { + return new CustomRouteBuilder(); + } + + public CustomRouteBuilder GET(String pattern, HandlerFunction handlerFunction, + Consumer operationsConsumer) { + + OperationBuilder builder = new OperationBuilder(); + operationsConsumer.accept(builder); + + this.delegate.GET(pattern, handlerFunction) + .withAttribute(OPERATION_ATTRIBUTE, builder.operation); + + return this; + } + + public RouterFunction build() { + return this.delegate.build(); + } + + public static void main(String[] args) { + RouterFunction routerFunction = + route() + .GET("/foo", request -> ServerResponse.ok().build(), ops -> ops + .parameter("key1", "My key1 description") + .parameter("key1", "My key1 description") + .response(200, "This is normal response description") + .response(404, "This is response description") + ) + .build(); + + AttributesVisitor visitor = new AttributesVisitor(); + routerFunction.accept(visitor); + } + + + public static class OperationBuilder { + + private final Operation operation = new Operation(); + + public OperationBuilder parameter(String name, String description) { + this.operation.parameter(name, description); + return this; + } + + public OperationBuilder response(int statusCode, String description) { + this.operation.response(statusCode, description); + return this; + } + + } + + + static class Operation { + + private final Map parameters = new LinkedHashMap<>(); + + private final Map responses = new LinkedHashMap<>(); + + public void parameter(String name, String description) { + this.parameters.put(name, description); + } + + public void response(int status, String description) { + this.responses.put(status, description); + } + + @Override + public String toString() { + return "parameters=" + parameters + + ", responses=" + responses; + } + } + + static class AttributesVisitor implements RouterFunctions.Visitor { + + @Nullable + private Map attributes; + + @Override + public void attributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public void route(RequestPredicate predicate, HandlerFunction handlerFunction) { + System.out.printf("Route predicate %s->%s%nhas attributes %s", predicate, handlerFunction, this.attributes); + this.attributes = null; + } + + @Override + public void startNested(RequestPredicate predicate) { + // TODO + } + + @Override + public void endNested(RequestPredicate predicate) { + // TODO + + } + + @Override + public void resources(Function> lookupFunction) { + // TODO + + } + + @Override + public void unknown(RouterFunction routerFunction) { + // TODO + + } + } + + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java index 25d40bbd9fa..12b4b051103 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java @@ -232,5 +232,23 @@ public class RouterFunctionBuilderTests { } + @Test + public void attributes() { + RouterFunction route = RouterFunctions.route() + .GET("/atts/1", request -> ServerResponse.ok().build()) + .withAttribute("foo", "bar") + .withAttribute("baz", "qux") + .GET("/atts/2", request -> ServerResponse.ok().build()) + .withAttributes(atts -> { + atts.put("foo", "bar"); + atts.put("baz", "qux"); + }) + .build(); + + AttributesTestVisitor visitor = new AttributesTestVisitor(); + route.accept(visitor); + assertThat(visitor.visitCount()).isEqualTo(2); + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionTests.java index 286a8a8a12b..4fcd6dbf1c7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionTests.java @@ -26,6 +26,7 @@ import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRe import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; /** * @author Arjen Poutsma @@ -126,9 +127,29 @@ public class RouterFunctionTests { .verify(); } + @Test + public void attributes() { + RouterFunction route = RouterFunctions.route( + GET("/atts/1"), request -> ServerResponse.ok().build()) + .withAttribute("foo", "bar") + .withAttribute("baz", "qux") + .and(RouterFunctions.route(GET("/atts/2"), request -> ServerResponse.ok().build()) + .withAttributes(atts -> { + atts.put("foo", "bar"); + atts.put("baz", "qux"); + })); + + AttributesTestVisitor visitor = new AttributesTestVisitor(); + route.accept(visitor); + assertThat(visitor.visitCount()).isEqualTo(2); + } + + private Mono handlerMethod(ServerRequest request) { return ServerResponse.ok().bodyValue("42"); } + + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java index 3a360f2fd55..264a30bf679 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java @@ -16,6 +16,7 @@ package org.springframework.web.servlet.function; +import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -59,6 +60,10 @@ class ChangePathPatternParserVisitor implements RouterFunctions.Visitor { public void resources(Function> lookupFunction) { } + @Override + public void attributes(Map attributes) { + } + @Override public void unknown(RouterFunction routerFunction) { } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunction.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunction.java index 0ac7333c830..e2c79964a11 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunction.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunction.java @@ -16,7 +16,12 @@ package org.springframework.web.servlet.function; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; + +import org.springframework.util.Assert; /** * Represents a function that routes to a {@linkplain HandlerFunction handler function}. @@ -114,4 +119,40 @@ public interface RouterFunction { default void accept(RouterFunctions.Visitor visitor) { visitor.unknown(this); } + + /** + * Return a new routing function with the given attribute. + * @param name the attribute name + * @param value the attribute value + * @return a function that has the specified attributes + * @since 5.3 + */ + default RouterFunction withAttribute(String name, Object value) { + Assert.hasLength(name, "Name must not be empty"); + Assert.notNull(value, "Value must not be null"); + + Map attributes = new LinkedHashMap<>(); + attributes.put(name, value); + return new RouterFunctions.AttributesRouterFunction<>(this, attributes); + } + + /** + * Return a new routing function with attributes manipulated with the given consumer. + *

The map provided to the consumer is "live", so that the consumer can be used + * to {@linkplain Map#put(Object, Object) overwrite} existing attributes, + * {@linkplain Map#remove(Object) remove} attributes, or use any of the other + * {@link Map} methods. + * @param attributesConsumer a function that consumes the attributes map + * @return this builder + * @since 5.3 + */ + default RouterFunction withAttributes(Consumer> attributesConsumer) { + Assert.notNull(attributesConsumer, "AttributesConsumer must not be null"); + + Map attributes = new LinkedHashMap<>(); + attributesConsumer.accept(attributes); + return new RouterFunctions.AttributesRouterFunction<>(this, attributes); + } + + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java index 77d1813cd6e..e8730910a14 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java @@ -18,6 +18,7 @@ package org.springframework.web.servlet.function; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -323,6 +324,35 @@ class RouterFunctionBuilder implements RouterFunctions.Builder { return onError(exceptionType::isInstance, responseProvider); } + @Override + public RouterFunctions.Builder withAttribute(String name, Object value) { + Assert.hasLength(name, "Name must not be empty"); + Assert.notNull(value, "Value must not be null"); + + if (this.routerFunctions.isEmpty()) { + throw new IllegalStateException("attributes can only be called after any other method (GET, path, etc.)"); + } + int lastIdx = this.routerFunctions.size() - 1; + RouterFunction attributed = this.routerFunctions.get(lastIdx) + .withAttribute(name, value); + this.routerFunctions.set(lastIdx, attributed); + return this; + } + + @Override + public RouterFunctions.Builder withAttributes(Consumer> attributesConsumer) { + Assert.notNull(attributesConsumer, "AttributesConsumer must not be null"); + + if (this.routerFunctions.isEmpty()) { + throw new IllegalStateException("attributes can only be called after any other method (GET, path, etc.)"); + } + int lastIdx = this.routerFunctions.size() - 1; + RouterFunction attributed = this.routerFunctions.get(lastIdx) + .withAttributes(attributesConsumer); + this.routerFunctions.set(lastIdx, attributed); + return this; + } + @Override public RouterFunction build() { if (this.routerFunctions.isEmpty()) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java index 07a005c9c53..745bce13ae4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java @@ -16,6 +16,9 @@ package org.springframework.web.servlet.function; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -769,6 +772,27 @@ public abstract class RouterFunctions { Builder onError(Class exceptionType, BiFunction responseProvider); + /** + * Add an attribute with the given name and value to the last route built with this builder. + * @param name the attribute name + * @param value the attribute value + * @return this builder + * @since 5.3 + */ + Builder withAttribute(String name, Object value); + + /** + * Manipulate the attributes of the last route built with the given consumer. + *

The map provided to the consumer is "live", so that the consumer can be used + * to {@linkplain Map#put(Object, Object) overwrite} existing attributes, + * {@linkplain Map#remove(Object) remove} attributes, or use any of the other + * {@link Map} methods. + * @param attributesConsumer a function that consumes the attributes map + * @return this builder + * @since 5.3 + */ + Builder withAttributes(Consumer> attributesConsumer); + /** * Builds the {@code RouterFunction}. All created routes are * {@linkplain RouterFunction#and(RouterFunction) composed} with one another, and filters @@ -813,6 +837,14 @@ public abstract class RouterFunctions { */ void resources(Function> lookupFunction); + /** + * Receive notification of a router function with attributes. The + * given attributes apply to the router notification that follows this one. + * @param attributes the attributes that apply to the following router + * @since 5.3 + */ + void attributes(Map attributes); + /** * Receive notification of an unknown router function. This method is called for router * functions that were not created via the various {@link RouterFunctions} methods. @@ -1044,4 +1076,56 @@ public abstract class RouterFunctions { } + static final class AttributesRouterFunction extends AbstractRouterFunction { + + private final RouterFunction delegate; + + private final Map attributes; + + public AttributesRouterFunction(RouterFunction delegate, Map attributes) { + this.delegate = delegate; + this.attributes = initAttributes(attributes); + } + + private static Map initAttributes(Map attributes) { + if (attributes.isEmpty()) { + return Collections.emptyMap(); + } + else { + return Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); + } + } + + @Override + public Optional> route(ServerRequest request) { + return this.delegate.route(request); + } + + @Override + public void accept(Visitor visitor) { + visitor.attributes(this.attributes); + this.delegate.accept(visitor); + } + + @Override + public RouterFunction withAttribute(String name, Object value) { + Assert.hasLength(name, "Name must not be empty"); + Assert.notNull(value, "Value must not be null"); + + Map attributes = new LinkedHashMap<>(this.attributes); + attributes.put(name, value); + return new AttributesRouterFunction<>(this.delegate, attributes); + } + + @Override + public RouterFunction withAttributes(Consumer> attributesConsumer) { + Assert.notNull(attributesConsumer, "AttributesConsumer must not be null"); + + Map attributes = new LinkedHashMap<>(this.attributes); + attributesConsumer.accept(attributes); + return new AttributesRouterFunction<>(this.delegate, attributes); + } + } + + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ToStringVisitor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ToStringVisitor.java index 31e8c4ed3eb..dbbe2548048 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ToStringVisitor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ToStringVisitor.java @@ -16,6 +16,7 @@ package org.springframework.web.servlet.function; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -68,6 +69,10 @@ class ToStringVisitor implements RouterFunctions.Visitor, RequestPredicates.Visi this.builder.append(lookupFunction).append('\n'); } + @Override + public void attributes(Map attributes) { + } + @Override public void unknown(RouterFunction routerFunction) { indent(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/AttributesTestVisitor.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/AttributesTestVisitor.java new file mode 100644 index 00000000000..84e031518b4 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/AttributesTestVisitor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2020 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.web.servlet.function; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Arjen Poutsma + */ +class AttributesTestVisitor implements RouterFunctions.Visitor { + + @Nullable + private Map attributes; + + private int visitCount; + + public int visitCount() { + return this.visitCount; + } + + @Override + public void startNested(RequestPredicate predicate) { + } + + @Override + public void endNested(RequestPredicate predicate) { + } + + @Override + public void route(RequestPredicate predicate, HandlerFunction handlerFunction) { + assertThat(this.attributes).isNotNull(); + this.attributes = null; + } + + @Override + public void resources(Function> lookupFunction) { + } + + @Override + public void attributes(Map attributes) { + assertThat(attributes).containsExactly(entry("foo", "bar"), entry("baz", "qux")); + this.attributes = attributes; + this.visitCount++; + } + + @Override + public void unknown(RouterFunction routerFunction) { + + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java index 7c3a7b856f1..13f81baba9c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java @@ -214,4 +214,23 @@ class RouterFunctionBuilderTests { PathPatternsTestUtils.initRequest(httpMethod, null, requestUri, true, consumer), emptyList()); } + @Test + public void attributes() { + RouterFunction route = RouterFunctions.route() + .GET("/atts/1", request -> ServerResponse.ok().build()) + .withAttribute("foo", "bar") + .withAttribute("baz", "qux") + .GET("/atts/2", request -> ServerResponse.ok().build()) + .withAttributes(atts -> { + atts.put("foo", "bar"); + atts.put("baz", "qux"); + }) + .build(); + + AttributesTestVisitor visitor = new AttributesTestVisitor(); + route.accept(visitor); + assertThat(visitor.visitCount()).isEqualTo(2); + } + + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java index 6a1345663bd..e3cba1677f9 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.servlet.handler.PathPatternsTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.web.servlet.function.RequestPredicates.GET; /** * @author Arjen Poutsma @@ -109,7 +110,29 @@ class RouterFunctionTests { } + @Test + public void attributes() { + RouterFunction route = RouterFunctions.route( + GET("/atts/1"), request -> ServerResponse.ok().build()) + .withAttribute("foo", "bar") + .withAttribute("baz", "qux") + .and(RouterFunctions.route(GET("/atts/2"), request -> ServerResponse.ok().build()) + .withAttributes(atts -> { + atts.put("foo", "bar"); + atts.put("baz", "qux"); + })); + + AttributesTestVisitor visitor = new AttributesTestVisitor(); + route.accept(visitor); + assertThat(visitor.visitCount()).isEqualTo(2); + } + + + private ServerResponse handlerMethod(ServerRequest request) { return ServerResponse.ok().body("42"); } + + + }