diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultRouterFunctionSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultRouterFunctionSpec.java new file mode 100644 index 00000000000..2b426568770 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultRouterFunctionSpec.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.test.web.reactive.server; + +import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +/** + * Spec for setting up server-less testing against a RouterFunction. + * + * @author Arjen Poutsma + * @since 5.0 + */ +class DefaultRouterFunctionSpec extends AbstractMockServerSpec + implements WebTestClient.RouterFunctionSpec { + + private final RouterFunction routerFunction; + + private HandlerStrategies handlerStrategies = HandlerStrategies.withDefaults(); + + + DefaultRouterFunctionSpec(RouterFunction routerFunction) { + this.routerFunction = routerFunction; + } + + + @Override + public WebTestClient.RouterFunctionSpec handlerStrategies(HandlerStrategies handlerStrategies) { + if (handlerStrategies != null) { + this.handlerStrategies = handlerStrategies; + } + return this; + } + + @Override + protected WebHttpHandlerBuilder initHttpHandlerBuilder() { + WebHandler webHandler = RouterFunctions.toWebHandler(this.routerFunction, this.handlerStrategies); + return WebHttpHandlerBuilder.webHandler(webHandler); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/RouterFunctionSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/RouterFunctionSpec.java deleted file mode 100644 index 5c7dd4214c0..00000000000 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/RouterFunctionSpec.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2002-2017 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 - * - * http://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.test.web.reactive.server; - -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.HandlerAdapter; -import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter; -import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; - -/** - * Spec for setting up server-less testing against a RouterFunction. - * - * @author Rossen Stoyanchev - * @since 5.0 - */ -public class RouterFunctionSpec extends AbstractMockServerSpec { - - private final RouterFunction routerFunction; - - - RouterFunctionSpec(RouterFunction routerFunction) { - this.routerFunction = routerFunction; - } - - - @Override - protected WebHttpHandlerBuilder initHttpHandlerBuilder() { - return WebHttpHandlerBuilder.applicationContext(initApplicationContext()); - } - - private ApplicationContext initApplicationContext() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.registerBean("webHandler", DispatcherHandler.class, () -> new DispatcherHandler()); - context.registerBean(HandlerMapping.class, () -> RouterFunctions.toHandlerMapping(this.routerFunction)); - context.registerBean(HandlerAdapter.class, () -> new HandlerFunctionAdapter()); - context.registerBean(HandlerResultHandler.class, () -> new ServerResponseResultHandler()); - context.refresh(); - return context; - } - -} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index c8d28c6b816..57ef6b251cf 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -48,6 +48,7 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.server.ServerWebExchange; @@ -169,8 +170,8 @@ public interface WebTestClient { * @param routerFunction the RouterFunction to test * @return the {@link WebTestClient} builder */ - static MockServerSpec bindToRouterFunction(RouterFunction routerFunction) { - return new RouterFunctionSpec(routerFunction); + static RouterFunctionSpec bindToRouterFunction(RouterFunction routerFunction) { + return new DefaultRouterFunctionSpec(routerFunction); } /** @@ -285,6 +286,18 @@ public interface WebTestClient { } + /** + * Specification for customizing router function configuration. + */ + interface RouterFunctionSpec extends MockServerSpec { + + /** + * Configure handler strategies. + */ + RouterFunctionSpec handlerStrategies(HandlerStrategies handlerStrategies); + + } + /** * Steps for customizing the {@link WebClient} used to test with * internally delegating to a {@link WebClient.Builder}. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index 6424406f602..e1eb5d1e262 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -47,6 +47,9 @@ import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.CompositeContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; +import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler; import org.springframework.web.reactive.handler.AbstractHandlerMapping; import org.springframework.web.reactive.result.SimpleHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; @@ -81,6 +84,8 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { private PathMatchConfigurer pathMatchConfigurer; + private ViewResolverRegistry viewResolverRegistry; + private ApplicationContext applicationContext; @@ -203,6 +208,23 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { public void configurePathMatching(PathMatchConfigurer configurer) { } + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = createRouterFunctionMapping(); + mapping.setOrder(-1); // go before RequestMappingHandlerMapping + mapping.setMessageCodecConfigurer(serverCodecConfigurer()); + mapping.setCorsConfigurations(getCorsConfigurations()); + + return mapping; + } + + /** + * Override to plug a sub-class of {@link RouterFunctionMapping}. + */ + protected RouterFunctionMapping createRouterFunctionMapping() { + return new RouterFunctionMapping(); + } + /** * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped * resource handlers. To configure resource handling, override @@ -362,6 +384,11 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { return null; } + @Bean + public HandlerFunctionAdapter handlerFunctionAdapter() { + return new HandlerFunctionAdapter(); + } + @Bean public SimpleHandlerAdapter simpleHandlerAdapter() { return new SimpleHandlerAdapter(); @@ -381,15 +408,38 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Bean public ViewResolutionResultHandler viewResolutionResultHandler() { - ViewResolverRegistry registry = new ViewResolverRegistry(getApplicationContext()); - configureViewResolvers(registry); + ViewResolverRegistry registry = getViewResolverRegistry(); List resolvers = registry.getViewResolvers(); ViewResolutionResultHandler handler = new ViewResolutionResultHandler( resolvers, webFluxContentTypeResolver(), webFluxAdapterRegistry()); handler.setDefaultViews(registry.getDefaultViews()); handler.setOrder(registry.getOrder()); return handler; + } + + @Bean + public ServerResponseResultHandler serverResponseResultHandler() { + ViewResolverRegistry registry = getViewResolverRegistry(); + List resolvers = registry.getViewResolvers(); + + ServerResponseResultHandler handler = new ServerResponseResultHandler(); + handler.setMessageCodecConfigurer(serverCodecConfigurer()); + handler.setViewResolvers(resolvers); + handler.setOrder(registry.getOrder() + 1); + + return handler; + } + /** + * Callback for building the {@link ViewResolverRegistry}. This method is final, + * use {@link #configureViewResolvers} to customize view resolvers. + */ + protected final ViewResolverRegistry getViewResolverRegistry() { + if (this.viewResolverRegistry == null) { + this.viewResolverRegistry = new ViewResolverRegistry(getApplicationContext()); + configureViewResolvers(this.viewResolverRegistry); + } + return this.viewResolverRegistry; } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java index d28c14bda57..61817ee017c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilder.java @@ -203,13 +203,13 @@ class DefaultEntityResponseBuilder implements EntityResponse.Builder { } @Override - public Mono writeTo(ServerWebExchange exchange, HandlerStrategies strategies) { + public Mono writeTo(ServerWebExchange exchange, Context context) { ServerHttpResponse response = exchange.getResponse(); writeStatusAndHeaders(response); return inserter().insert(response, new BodyInserter.Context() { @Override public Supplier>> messageWriters() { - return strategies.messageWriters(); + return context.messageWriters(); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java index 939ed1fc4e6..a31a9f71097 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java @@ -19,10 +19,7 @@ package org.springframework.web.reactive.function.server; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Locale; -import java.util.Optional; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -44,18 +41,10 @@ import org.springframework.web.server.handler.ResponseStatusExceptionHandler; */ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { - static final Function> DEFAULT_LOCALE_RESOLVER = - request -> request.headers().acceptLanguage().stream() - .map(Locale.LanguageRange::getRange) - .map(Locale::forLanguageTag).findFirst(); - - private final ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create(); private final List viewResolvers = new ArrayList<>(); - private Function> localeResolver; - private final List webFilters = new ArrayList<>(); private final List exceptionHandlers = new ArrayList<>(); @@ -67,7 +56,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { public void defaultConfiguration() { this.codecConfigurer.registerDefaults(true); - localeResolver(DEFAULT_LOCALE_RESOLVER); exceptionHandler(new ResponseStatusExceptionHandler()); } @@ -94,13 +82,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { return this; } - @Override - public HandlerStrategies.Builder localeResolver(Function> localeResolver) { - Assert.notNull(localeResolver, "'localeResolver' must not be null"); - this.localeResolver = localeResolver; - return this; - } - @Override public HandlerStrategies.Builder webFilter(WebFilter filter) { Assert.notNull(filter, "'filter' must not be null"); @@ -118,8 +99,8 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { @Override public HandlerStrategies build() { return new DefaultHandlerStrategies(this.codecConfigurer.getReaders(), - this.codecConfigurer.getWriters(), this.viewResolvers, this.localeResolver, - this.webFilters, this.exceptionHandlers); + this.codecConfigurer.getWriters(), this.viewResolvers, this.webFilters, + this.exceptionHandlers); } @@ -131,8 +112,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { private final List viewResolvers; - private final Function> localeResolver; - private final List webFilters; private final List exceptionHandlers; @@ -141,14 +120,12 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { List> messageReaders, List> messageWriters, List viewResolvers, - Function> localeResolver, List webFilters, List exceptionHandlers) { this.messageReaders = unmodifiableCopy(messageReaders); this.messageWriters = unmodifiableCopy(messageWriters); this.viewResolvers = unmodifiableCopy(viewResolvers); - this.localeResolver = localeResolver; this.webFilters = unmodifiableCopy(webFilters); this.exceptionHandlers = unmodifiableCopy(exceptionHandlers); } @@ -172,11 +149,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { return this.viewResolvers::stream; } - @Override - public Supplier>> localeResolver() { - return () -> this.localeResolver; - } - @Override public Supplier> webFilters() { return this.webFilters::stream; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java index 2af7e5bfe2a..6a24d4f5db5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.stream.Stream; @@ -158,12 +159,12 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { } @Override - public Mono writeTo(ServerWebExchange exchange, HandlerStrategies strategies) { + public Mono writeTo(ServerWebExchange exchange, Context context) { ServerHttpResponse response = exchange.getResponse(); writeStatusAndHeaders(response); MediaType contentType = exchange.getResponse().getHeaders().getContentType(); - Locale locale = resolveLocale(exchange, strategies); - Stream viewResolverStream = strategies.viewResolvers().get(); + Locale locale = resolveLocale(exchange); + Stream viewResolverStream = context.viewResolvers().get(); return Flux.fromStream(viewResolverStream) .concatMap(viewResolver -> viewResolver.resolveViewName(name(), locale)) @@ -173,15 +174,9 @@ class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { .flatMap(view -> view.render(model(), contentType, exchange)); } - private Locale resolveLocale(ServerWebExchange exchange, HandlerStrategies strategies) { - ServerRequest request = - exchange.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE) - .orElseThrow(() -> new IllegalStateException( - "Could not find ServerRequest in exchange attributes")); - - return strategies.localeResolver().get() - .apply(request) - .orElse(Locale.getDefault()); + private Locale resolveLocale(ServerWebExchange exchange) { + List locales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales(); + return locales.isEmpty() ? Locale.getDefault() : locales.get(0); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java index b0482d02d75..c65468f12ce 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java @@ -169,7 +169,7 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public Mono build( - BiFunction> writeFunction) { + BiFunction> writeFunction) { Assert.notNull(writeFunction, "'writeFunction' must not be null"); return Mono.just(new WriterFunctionServerResponse(this.statusCode, this.headers, writeFunction)); @@ -277,19 +277,19 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { private static final class WriterFunctionServerResponse extends AbstractServerResponse { - private final BiFunction> writeFunction; + private final BiFunction> writeFunction; public WriterFunctionServerResponse(HttpStatus statusCode, HttpHeaders headers, - BiFunction> writeFunction) { + BiFunction> writeFunction) { super(statusCode, headers); this.writeFunction = writeFunction; } @Override - public Mono writeTo(ServerWebExchange exchange, HandlerStrategies strategies) { + public Mono writeTo(ServerWebExchange exchange, Context context) { writeStatusAndHeaders(exchange.getResponse()); - return this.writeFunction.apply(exchange, strategies); + return this.writeFunction.apply(exchange, context); } } @@ -309,13 +309,13 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } @Override - public Mono writeTo(ServerWebExchange exchange, HandlerStrategies strategies) { + public Mono writeTo(ServerWebExchange exchange, Context context) { ServerHttpResponse response = exchange.getResponse(); writeStatusAndHeaders(response); return this.inserter.insert(response, new BodyInserter.Context() { @Override public Supplier>> messageWriters() { - return strategies.messageWriters(); + return context.messageWriters(); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java index 4fc70435c22..cb24450f04c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/HandlerStrategies.java @@ -16,10 +16,7 @@ package org.springframework.web.reactive.function.server; -import java.util.Locale; -import java.util.Optional; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -40,7 +37,6 @@ import org.springframework.web.server.WebFilter; * @author Juergen Hoeller * @since 5.0 * @see RouterFunctions#toHttpHandler(RouterFunction, HandlerStrategies) - * @see RouterFunctions#toHandlerMapping(RouterFunction, HandlerStrategies) */ public interface HandlerStrategies { @@ -67,12 +63,6 @@ public interface HandlerStrategies { */ Supplier> viewResolvers(); - /** - * Supply a function that resolves the locale of a given {@link ServerRequest}. - * @return the locale resolver - */ - Supplier>> localeResolver(); - /** * Supply a {@linkplain Stream stream} of {@link WebFilter}s to be used for filtering the * request and response. @@ -147,13 +137,6 @@ public interface HandlerStrategies { */ Builder viewResolver(ViewResolver viewResolver); - /** - * Set the given function as {@link Locale} resolver for this builder. - * @param localeResolver the locale resolver to set - * @return this builder - */ - Builder localeResolver(Function> localeResolver); - /** * Add the given web filter to this builder. * @param filter the filter to add 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 8932c31e28a..0ccf687b086 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 @@ -19,17 +19,17 @@ package org.springframework.web.reactive.function.server; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import org.springframework.core.io.Resource; +import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.Assert; -import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter; -import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler; +import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -92,18 +92,7 @@ public abstract class RouterFunctions { Assert.notNull(predicate, "'predicate' must not be null"); Assert.notNull(handlerFunction, "'handlerFunction' must not be null"); - return request -> { - if (predicate.test(request)) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Predicate \"%s\" matches against \"%s\"", - predicate, request)); - } - return Mono.just(handlerFunction); - } - else { - return Mono.empty(); - } - }; + return new DefaultRouterFunction<>(predicate, handlerFunction); } /** @@ -135,17 +124,7 @@ public abstract class RouterFunctions { Assert.notNull(predicate, "'predicate' must not be null"); Assert.notNull(routerFunction, "'routerFunction' must not be null"); - return request -> predicate.nest(request) - .map(nestedRequest -> { - if (logger.isDebugEnabled()) { - logger.debug( - String.format("Nested predicate \"%s\" matches against \"%s\"", - predicate, request)); - } - return routerFunction.route(nestedRequest); - } - ) - .orElseGet(Mono::empty); + return new DefaultNestedRouterFunction<>(predicate, routerFunction); } /** @@ -219,19 +198,32 @@ public abstract class RouterFunctions { Assert.notNull(routerFunction, "RouterFunction must not be null"); Assert.notNull(strategies, "HandlerStrategies must not be null"); - WebHandler webHandler = exchange -> { + WebHandler webHandler = toWebHandler(routerFunction, strategies); + WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder.webHandler(webHandler); + strategies.webFilters().get().forEach(handlerBuilder::filter); + strategies.exceptionHandlers().get().forEach(handlerBuilder::exceptionHandler); + return handlerBuilder.build(); + } + + /** + * Convert the given {@linkplain RouterFunction router function} into a {@link WebHandler}, + * using the given strategies. + * @param routerFunction the router function to convert + * @param strategies the strategies to use + * @return a web handler that handles web request using the given router function + */ + public static WebHandler toWebHandler(RouterFunction routerFunction, HandlerStrategies strategies) { + Assert.notNull(routerFunction, "RouterFunction must not be null"); + Assert.notNull(strategies, "HandlerStrategies must not be null"); + + return exchange -> { ServerRequest request = new DefaultServerRequest(exchange, strategies.messageReaders()); addAttributes(exchange, request); return routerFunction.route(request) .defaultIfEmpty(notFound()) .flatMap(handlerFunction -> wrapException(() -> handlerFunction.handle(request))) - .flatMap(response -> wrapException(() -> response.writeTo(exchange, strategies))); + .flatMap(response -> wrapException(() -> response.writeTo(exchange, toContext(strategies)))); }; - - WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder.webHandler(webHandler); - strategies.webFilters().get().forEach(handlerBuilder::filter); - strategies.exceptionHandlers().get().forEach(handlerBuilder::exceptionHandler); - return handlerBuilder.build(); } private static Mono wrapException(Supplier> supplier) { @@ -243,39 +235,17 @@ public abstract class RouterFunctions { } } - /** - * Convert the given {@code RouterFunction} into a {@code HandlerMapping}. - * This conversion uses {@linkplain HandlerStrategies#builder() default strategies}. - *

The returned {@code HandlerMapping} can be run in a - * {@link org.springframework.web.reactive.DispatcherHandler}. - * @param routerFunction the router function to convert - * @return an handler mapping that maps HTTP request to a handler using the given router function - * @see HandlerFunctionAdapter - * @see ServerResponseResultHandler - */ - public static HandlerMapping toHandlerMapping(RouterFunction routerFunction) { - return toHandlerMapping(routerFunction, HandlerStrategies.withDefaults()); - } - - /** - * Convert the given {@linkplain RouterFunction router function} into a {@link HandlerMapping}, - * using the given strategies. - *

The returned {@code HandlerMapping} can be run in a - * {@link org.springframework.web.reactive.DispatcherHandler}. - * @param routerFunction the router function to convert - * @param strategies the strategies to use - * @return an handler mapping that maps HTTP request to a handler using the given router function - * @see HandlerFunctionAdapter - * @see ServerResponseResultHandler - */ - public static HandlerMapping toHandlerMapping(RouterFunction routerFunction, HandlerStrategies strategies) { - Assert.notNull(routerFunction, "RouterFunction must not be null"); - Assert.notNull(strategies, "HandlerStrategies must not be null"); + private static ServerResponse.Context toContext(HandlerStrategies strategies) { + return new ServerResponse.Context() { + @Override + public Supplier>> messageWriters() { + return strategies.messageWriters(); + } - return exchange -> { - ServerRequest request = new DefaultServerRequest(exchange, strategies.messageReaders()); - addAttributes(exchange, request); - return routerFunction.route(request).map(handlerFunction -> (Object)handlerFunction); + @Override + public Supplier> viewResolvers() { + return strategies.viewResolvers(); + } }; } @@ -294,4 +264,72 @@ public abstract class RouterFunctions { return (HandlerFunction) handlerFunction; } + private static class DefaultRouterFunction + implements RouterFunction { + + private final RequestPredicate predicate; + + private final HandlerFunction handlerFunction; + + public DefaultRouterFunction(RequestPredicate predicate, + HandlerFunction handlerFunction) { + this.predicate = predicate; + this.handlerFunction = handlerFunction; + } + + @Override + public Mono> route(ServerRequest request) { + if (this.predicate.test(request)) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Predicate \"%s\" matches against \"%s\"", + this.predicate, request)); + } + return Mono.just(this.handlerFunction); + } + else { + return Mono.empty(); + } + } + + @Override + public String toString() { + return String.format("%s -> %s", this.predicate, this.handlerFunction); + } + } + + private static class DefaultNestedRouterFunction + implements RouterFunction { + + private final RequestPredicate predicate; + + private final RouterFunction routerFunction; + + public DefaultNestedRouterFunction(RequestPredicate predicate, + RouterFunction routerFunction) { + this.predicate = predicate; + this.routerFunction = routerFunction; + } + + @Override + public Mono> route(ServerRequest serverRequest) { + return this.predicate.nest(serverRequest) + .map(nestedRequest -> { + if (logger.isDebugEnabled()) { + logger.debug( + String.format( + "Nested predicate \"%s\" matches against \"%s\"", + this.predicate, serverRequest)); + } + return this.routerFunction.route(nestedRequest); + } + ) + .orElseGet(Mono::empty); + } + + @Override + public String toString() { + return String.format("%s -> %s", this.predicate, this.routerFunction); + } + + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java index 3b741e85e95..51d921c774e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerRequest.java @@ -33,9 +33,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.json.Jackson2CodecSupport; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.function.BodyExtractor; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; /** @@ -175,6 +177,19 @@ public interface ServerRequest { Mono principal(); + /** + * Create a new {@code ServerRequest} based on the given {@code ServerWebExchange} and + * message readers. + * @param exchange the exchange + * @param messageReaders the message readers + * @return the created {@code ServerRequest} + */ + static ServerRequest create(ServerWebExchange exchange, + List> messageReaders) { + + return new DefaultServerRequest(exchange, messageReaders::stream); + } + /** * Represents the headers of the HTTP request. * @see ServerRequest#headers() diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java index 1857eec6369..ea31e47ba13 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java @@ -22,6 +22,8 @@ import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Supplier; +import java.util.stream.Stream; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -31,11 +33,13 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.json.Jackson2CodecSupport; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; /** @@ -64,10 +68,10 @@ public interface ServerResponse { /** * Write this response to the given web exchange. * @param exchange the web exchange to write to - * @param strategies the strategies to use when writing + * @param context the context to use when writing * @return {@code Mono} to indicate when writing is complete */ - Mono writeTo(ServerWebExchange exchange, HandlerStrategies strategies); + Mono writeTo(ServerWebExchange exchange, Context context); // Static builder methods @@ -298,7 +302,7 @@ public interface ServerResponse { * @param writeFunction the function used to write to the {@link ServerWebExchange} * @return the built response */ - Mono build(BiFunction> writeFunction); + Mono build(BiFunction> writeFunction); } @@ -385,4 +389,25 @@ public interface ServerResponse { Mono render(String name, Map model); } + /** + * Defines the context used during the {@link #writeTo(ServerWebExchange, Context)}. + */ + interface Context { + + /** + * Supply a {@linkplain Stream stream} of {@link HttpMessageWriter}s + * to be used for response body conversion. + * @return the stream of message writers + */ + Supplier>> messageWriters(); + + /** + * Supply a {@linkplain Stream stream} of {@link ViewResolver}s to be used for view name + * resolution. + * @return the stream of view resolvers + */ + Supplier> viewResolvers(); + } + + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/RouterFunctionMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/RouterFunctionMapping.java new file mode 100644 index 00000000000..968b74a4625 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/RouterFunctionMapping.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.support; + +import java.util.Collections; +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.handler.AbstractHandlerMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * {@code HandlerMapping} implementation that supports {@link RouterFunction}s. + *

If no {@link RouterFunction} is provided at + * {@linkplain #RouterFunctionMapping(RouterFunction) construction time}, this mapping will detect + * all router functions in the application context, and consult them in + * {@linkplain org.springframework.core.annotation.Order order}. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean { + + private RouterFunction routerFunction; + + private ServerCodecConfigurer messageCodecConfigurer; + + /** + * Create an empty {@code RouterFunctionMapping}. + *

If this constructor is used, this mapping will detect all {@link RouterFunction} instances + * available in the application context. + */ + public RouterFunctionMapping() { + } + + /** + * Create a {@code RouterFunctionMapping} with the given {@link RouterFunction}. + *

If this constructor is used, no application context detection will occur. + * @param routerFunction the router function to use for mapping + */ + public RouterFunctionMapping(RouterFunction routerFunction) { + this.routerFunction = routerFunction; + } + + /** + * Configure HTTP message readers to de-serialize the request body with. + *

By default this is set to {@link ServerCodecConfigurer} with defaults. + */ + public void setMessageCodecConfigurer(ServerCodecConfigurer configurer) { + this.messageCodecConfigurer = configurer; + } + + + @Override + public void afterPropertiesSet() throws Exception { + if (this.messageCodecConfigurer == null) { + this.messageCodecConfigurer = ServerCodecConfigurer.create(); + } + if (this.routerFunction == null) { + initRouterFunctions(); + } + } + + /** + * Initialized the router functions by detecting them in the application context. + */ + protected void initRouterFunctions() { + if (logger.isDebugEnabled()) { + logger.debug("Looking for router functions in application context: " + + getApplicationContext()); + } + + List> routerFunctions = routerFunctions(); + if (!CollectionUtils.isEmpty(routerFunctions) && logger.isInfoEnabled()) { + routerFunctions.forEach(routerFunction1 -> { + logger.info("Mapped " + routerFunction1); + }); + } + this.routerFunction = routerFunctions.stream() + .reduce(RouterFunction::andOther) + .orElse(null); + } + + private List> routerFunctions() { + SortedRouterFunctionsContainer container = new SortedRouterFunctionsContainer(); + getApplicationContext().getAutowireCapableBeanFactory().autowireBean(container); + + return CollectionUtils.isEmpty(container.routerFunctions) ? Collections.emptyList() : + container.routerFunctions; + } + + @Override + protected Mono getHandlerInternal(ServerWebExchange exchange) { + if (this.routerFunction != null) { + ServerRequest request = ServerRequest.create(exchange, this.messageCodecConfigurer.getReaders()); + exchange.getAttributes().put(RouterFunctions.REQUEST_ATTRIBUTE, request); + return this.routerFunction.route(request); + } + else { + return Mono.empty(); + } + } + + private static class SortedRouterFunctionsContainer { + + private List> routerFunctions; + + @Autowired(required = false) + public void setRouterFunctions(List> routerFunctions) { + this.routerFunctions = routerFunctions; + } + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerResponseResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerResponseResultHandler.java index bcfccde4215..2a4853d8654 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerResponseResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerResponseResultHandler.java @@ -16,13 +16,22 @@ package org.springframework.web.reactive.function.server.support; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + import reactor.core.publisher.Mono; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.Ordered; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; /** @@ -31,26 +40,53 @@ import org.springframework.web.server.ServerWebExchange; * @author Arjen Poutsma * @since 5.0 */ -public class ServerResponseResultHandler implements HandlerResultHandler { +public class ServerResponseResultHandler implements HandlerResultHandler, InitializingBean, + Ordered { + + private ServerCodecConfigurer messageCodecConfigurer; - private final HandlerStrategies strategies; + private List viewResolvers; + + private int order = LOWEST_PRECEDENCE; /** - * Create a {@code ServerResponseResultHandler} with default strategies. + * Configure HTTP message readers to de-serialize the request body with. + *

By default this is set to {@link ServerCodecConfigurer} with defaults. */ - public ServerResponseResultHandler() { - this(HandlerStrategies.builder().build()); + public void setMessageCodecConfigurer(ServerCodecConfigurer configurer) { + this.messageCodecConfigurer = configurer; + } + + public void setViewResolvers(List viewResolvers) { + this.viewResolvers = viewResolvers; } /** - * Create a {@code ServerResponseResultHandler} with the given strategies. + * Set the order for this result handler relative to others. + *

By default set to {@link Ordered#LOWEST_PRECEDENCE}, however see + * Javadoc of sub-classes which may change this default. + * @param order the order */ - public ServerResponseResultHandler(HandlerStrategies strategies) { - Assert.notNull(strategies, "HandlerStrategies must not be null"); - this.strategies = strategies; + public void setOrder(int order) { + this.order = order; } + @Override + public int getOrder() { + return this.order; + } + + + @Override + public void afterPropertiesSet() throws Exception { + if (this.messageCodecConfigurer == null) { + throw new IllegalArgumentException("'messageCodecConfigurer' is required"); + } + if (this.viewResolvers == null) { + this.viewResolvers = Collections.emptyList(); + } + } @Override public boolean supports(HandlerResult result) { @@ -61,7 +97,16 @@ public class ServerResponseResultHandler implements HandlerResultHandler { public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { ServerResponse response = (ServerResponse) result.getReturnValue(); Assert.state(response != null, "No ServerResponse"); - return response.writeTo(exchange, this.strategies); - } + return response.writeTo(exchange, new ServerResponse.Context() { + @Override + public Supplier>> messageWriters() { + return messageCodecConfigurer.getWriters()::stream; + } + @Override + public Supplier> viewResolvers() { + return viewResolvers::stream; + } + }); + } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java index 9556d7aa5f9..29436538cc3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java @@ -23,6 +23,8 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Supplier; +import java.util.stream.Stream; import org.junit.Test; import org.reactivestreams.Publisher; @@ -40,10 +42,12 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerWebExchange; import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.result.view.ViewResolver; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.*; @@ -190,6 +194,17 @@ public class DefaultEntityResponseBuilderTests { MockServerWebExchange exchange = MockServerHttpRequest.get("http://localhost").toExchange(); + ServerResponse.Context context = new ServerResponse.Context() { + @Override + public Supplier>> messageWriters() { + return Collections.>singletonList(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()))::stream; + } + + @Override + public Supplier> viewResolvers() { + return Collections.emptyList()::stream; + } + }; HandlerStrategies strategies = HandlerStrategies.empty() .customCodecs(configurer -> configurer.writer(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()))) .build(); @@ -200,7 +215,7 @@ public class DefaultEntityResponseBuilderTests { .expectNext(body) .expectComplete() .verify(); - response.writeTo(exchange, strategies); + response.writeTo(exchange, context); }) .expectComplete() .verify(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java index 755b7a4cc53..d3d46df0e3e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java @@ -270,9 +270,9 @@ public class DefaultServerResponseBuilderTests { ServerWebExchange exchange = mock(ServerWebExchange.class); MockServerHttpResponse response = new MockServerHttpResponse(); when(exchange.getResponse()).thenReturn(response); - HandlerStrategies strategies = mock(HandlerStrategies.class); + ServerResponse.Context context = mock(ServerResponse.Context.class); - result.flatMap(res -> res.writeTo(exchange, strategies)).block(); + result.flatMap(res -> res.writeTo(exchange, context)).block(); assertEquals(HttpStatus.CREATED, response.getStatusCode()); assertEquals("MyValue", response.getHeaders().getFirst("MyKey")); @@ -287,9 +287,9 @@ public class DefaultServerResponseBuilderTests { ServerWebExchange exchange = mock(ServerWebExchange.class); MockServerHttpResponse response = new MockServerHttpResponse(); when(exchange.getResponse()).thenReturn(response); - HandlerStrategies strategies = mock(HandlerStrategies.class); + ServerResponse.Context context = mock(ServerResponse.Context.class); - result.flatMap(res -> res.writeTo(exchange, strategies)).block(); + result.flatMap(res -> res.writeTo(exchange, context)).block(); StepVerifier.create(response.getBody()).expectComplete().verify(); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java index 381aa2dbfc0..e22405edb3a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java @@ -36,16 +36,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.HandlerAdapter; -import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.reactive.config.WebFluxConfigurationSupport; -import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter; -import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.*; -import static org.springframework.web.reactive.function.BodyInserters.fromObject; import static org.springframework.web.reactive.function.BodyInserters.fromPublisher; import static org.springframework.web.reactive.function.server.RouterFunctions.route; @@ -108,8 +102,9 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr } + @EnableWebFlux @Configuration - static class TestConfiguration extends WebFluxConfigurationSupport { + static class TestConfiguration { @Bean public PersonHandler personHandler() { @@ -122,31 +117,23 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr } @Bean - public HandlerAdapter handlerAdapter() { - return new HandlerFunctionAdapter(); + public RouterFunction> monoRouterFunction(PersonHandler personHandler) { + return route(RequestPredicates.GET("/mono"), personHandler::mono); } @Bean - public HandlerMapping handlerMapping() { - - PersonHandler personHandler = personHandler(); - return RouterFunctions.toHandlerMapping( - route(RequestPredicates.GET("/mono"), personHandler::mono) - .and(route(RequestPredicates.GET("/flux"), personHandler::flux))); + public RouterFunction fluxRouterFunction(PersonHandler personHandler) { + return route(RequestPredicates.GET("/flux"), personHandler::flux); } - @Bean - public ServerResponseResultHandler responseResultHandler() { - return new ServerResponseResultHandler(); - } } private static class PersonHandler { - public Mono mono(ServerRequest request) { + public Mono> mono(ServerRequest request) { Person person = new Person("John"); - return ServerResponse.ok().body(fromObject(person)); + return EntityResponse.fromObject(person).build(); } public Mono flux(ServerRequest request) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/HandlerStrategiesTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/HandlerStrategiesTests.java index b2010c1d88e..f34ed8b0633 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/HandlerStrategiesTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/HandlerStrategiesTests.java @@ -33,7 +33,6 @@ public class HandlerStrategiesTests { assertEquals(Optional.empty(), strategies.messageReaders().get().findFirst()); assertEquals(Optional.empty(), strategies.messageWriters().get().findFirst()); assertEquals(Optional.empty(), strategies.viewResolvers().get().findFirst()); - assertNull(strategies.localeResolver().get()); } @Test @@ -42,7 +41,6 @@ public class HandlerStrategiesTests { assertNotEquals(Optional.empty(), strategies.messageReaders().get().findFirst()); assertNotEquals(Optional.empty(), strategies.messageWriters().get().findFirst()); assertEquals(Optional.empty(), strategies.viewResolvers().get().findFirst()); - assertNotNull(strategies.localeResolver().get()); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java index 0942f65dd85..6d3edf51fc5 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/ResourceHandlerFunctionTests.java @@ -19,7 +19,10 @@ package org.springframework.web.reactive.function.server; import java.io.IOException; import java.nio.file.Files; import java.util.EnumSet; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -29,9 +32,11 @@ import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageWriter; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerWebExchange; +import org.springframework.web.reactive.result.view.ViewResolver; import static org.junit.Assert.*; @@ -44,6 +49,25 @@ public class ResourceHandlerFunctionTests { private final ResourceHandlerFunction handlerFunction = new ResourceHandlerFunction(this.resource); + private ServerResponse.Context context; + + @Before + public void createContext() { + HandlerStrategies strategies = HandlerStrategies.withDefaults(); + context = new ServerResponse.Context() { + @Override + public Supplier>> messageWriters() { + return strategies.messageWriters(); + } + + @Override + public Supplier> viewResolvers() { + return strategies.viewResolvers(); + } + }; + + } + @Test public void get() throws IOException { @@ -59,7 +83,7 @@ public class ResourceHandlerFunctionTests { assertTrue(response instanceof EntityResponse); EntityResponse entityResponse = (EntityResponse) response; assertEquals(this.resource, entityResponse.entity()); - return response.writeTo(exchange, HandlerStrategies.withDefaults()); + return response.writeTo(exchange, context); }); StepVerifier.create(result) @@ -94,7 +118,7 @@ public class ResourceHandlerFunctionTests { assertTrue(response instanceof EntityResponse); EntityResponse entityResponse = (EntityResponse) response; assertEquals(this.resource.getFilename(), entityResponse.entity().getFilename()); - return response.writeTo(exchange, HandlerStrategies.withDefaults()); + return response.writeTo(exchange, context); }); StepVerifier.create(result).expectComplete().verify(); @@ -116,7 +140,7 @@ public class ResourceHandlerFunctionTests { assertEquals(HttpStatus.OK, response.statusCode()); assertEquals(EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS), response.headers().getAllow()); - return response.writeTo(exchange, HandlerStrategies.withDefaults()); + return response.writeTo(exchange, context); }); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java index 99ec483bf8e..835069f0bf3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java @@ -199,7 +199,7 @@ public class RouterFunctionsTests { @Override public Mono writeTo(ServerWebExchange exchange, - HandlerStrategies strategies) { + Context context) { return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Not found")); } }); @@ -232,7 +232,7 @@ public class RouterFunctionsTests { @Override public Mono writeTo(ServerWebExchange exchange, - HandlerStrategies strategies) { + Context context) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not found"); } }); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/RouterFunctionMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/RouterFunctionMappingTests.java new file mode 100644 index 00000000000..4b4f2dc88a3 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/RouterFunctionMappingTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.support; + +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerWebExchange; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; + +/** + * @author Arjen Poutsma + */ +public class RouterFunctionMappingTests { + + + private List> messageReaders; + + private ServerWebExchange exchange; + + private ServerCodecConfigurer codecConfigurer; + + @Before + public void setUp() { + this.messageReaders = + Collections.singletonList(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + this.exchange = new MockServerWebExchange(MockServerHttpRequest.get("http://example.com/match").build()); + codecConfigurer = ServerCodecConfigurer.create(); + + } + + @Test + public void normal() { + HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); + RouterFunction routerFunction = request -> Mono.just(handlerFunction); + + RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction); + mapping.setMessageCodecConfigurer(this.codecConfigurer); + + Mono result = mapping.getHandler(this.exchange); + + StepVerifier.create(result) + .expectNext(handlerFunction) + .expectComplete() + .verify(); + } + + @Test + public void noMatch() { + RouterFunction routerFunction = request -> Mono.empty(); + RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction); + mapping.setMessageCodecConfigurer(this.codecConfigurer); + + Mono result = mapping.getHandler(this.exchange); + + StepVerifier.create(result) + .expectComplete() + .verify(); + } + + @Configuration + @EnableWebFlux + private static class TestConfig { + + public RouterFunction match() { + HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); + return RouterFunctions.route(RequestPredicates.GET("/match"), handlerFunction); + } + + public RouterFunction noMatch() { + HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); + RouterFunction routerFunction = request -> Mono.empty(); + return RouterFunctions.route(RequestPredicates.GET("/no-match"), handlerFunction); + } + + } +} \ No newline at end of file