From 7796c4db14a35b6d1b7a5977bda2565fd68a384b Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 16 Dec 2016 10:27:45 +0100 Subject: [PATCH] Introduce RenderingResponse This commit introduces the RenderingResponse, a template rendering-specific subtype of ServerResponse that exposes model and template data. --- .../DefaultHandlerStrategiesBuilder.java | 32 ++- .../DefaultRenderingResponseBuilder.java | 188 ++++++++++++++++++ .../function/server/DefaultServerRequest.java | 8 + .../server/DefaultServerResponseBuilder.java | 78 ++------ .../function/server/HandlerStrategies.java | 54 ++--- .../function/server/RenderingResponse.java | 136 +++++++++++++ .../server/ResourceHandlerFunction.java | 4 +- .../function/server/ServerRequest.java | 7 + .../server/support/ServerRequestWrapper.java | 6 + .../server/DefaultRenderingResponseTests.java | 137 +++++++++++++ .../DefaultServerResponseBuilderTests.java | 54 ----- .../DispatcherHandlerIntegrationTests.java | 8 + .../server/HandlerStrategiesTests.java | 16 +- .../function/server/MockServerRequest.java | 6 + 14 files changed, 571 insertions(+), 163 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java 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 57d04ca7d4..e25329994c 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,6 +19,9 @@ 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.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -52,6 +55,11 @@ import org.springframework.web.reactive.result.view.ViewResolver; */ 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 static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", DefaultHandlerStrategiesBuilder.class.getClassLoader()) && @@ -69,6 +77,8 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { private final List viewResolvers = new ArrayList<>(); + private Function> localeResolver; + public void defaultConfiguration() { messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); @@ -94,6 +104,7 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { else { messageWriter(new ServerSentEventHttpMessageWriter()); } + localeResolver(DEFAULT_LOCALE_RESOLVER); } public void applicationContext(ApplicationContext applicationContext) { @@ -123,10 +134,17 @@ 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 build() { return new DefaultHandlerStrategies(this.messageReaders, this.messageWriters, - this.viewResolvers); + this.viewResolvers, localeResolver); } private static class DefaultHandlerStrategies implements HandlerStrategies { @@ -137,13 +155,18 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { private final List viewResolvers; + private final Function> localeResolver; + + public DefaultHandlerStrategies( List> messageReaders, List> messageWriters, - List viewResolvers) { + List viewResolvers, + Function> localeResolver) { this.messageReaders = unmodifiableCopy(messageReaders); this.messageWriters = unmodifiableCopy(messageWriters); this.viewResolvers = unmodifiableCopy(viewResolvers); + this.localeResolver = localeResolver; } private static List unmodifiableCopy(List list) { @@ -164,6 +187,11 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { public Supplier> viewResolvers() { return this.viewResolvers::stream; } + + @Override + public Function> localeResolver() { + return this.localeResolver; + } } } 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 new file mode 100644 index 0000000000..ce17134f4c --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseBuilder.java @@ -0,0 +1,188 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Stream; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.Conventions; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Default {@link RenderingResponse.Builder} implementation. + * + * @author Arjen Poutsma + * @since 5.0 + */ +class DefaultRenderingResponseBuilder implements RenderingResponse.Builder { + + private final String name; + + private final HttpHeaders headers = new HttpHeaders(); + + private final Map model = new LinkedHashMap(); + + private HttpStatus status = HttpStatus.OK; + + public DefaultRenderingResponseBuilder(String name) { + this.name = name; + } + + + @Override + public RenderingResponse.Builder modelAttribute(Object attribute) { + Assert.notNull(attribute, "'value' must not be null"); + if (attribute instanceof Collection && ((Collection) attribute).isEmpty()) { + return this; + } + return modelAttribute(Conventions.getVariableName(attribute), attribute); + } + + @Override + public RenderingResponse.Builder modelAttribute(String name, Object value) { + Assert.notNull(name, "'name' must not be null"); + this.model.put(name, value); + return this; + } + + @Override + public RenderingResponse.Builder modelAttributes(Object... attributes) { + if (attributes != null) { + modelAttributes(Arrays.asList(attributes)); + } + return this; + } + + @Override + public RenderingResponse.Builder modelAttributes(Collection attributes) { + if (attributes != null) { + attributes.forEach(this::modelAttribute); + } + return this; + } + + @Override + public RenderingResponse.Builder modelAttributes(Map attributes) { + if (attributes != null) { + this.model.putAll(attributes); + } + return this; + } + + @Override + public RenderingResponse.Builder header(String headerName, String... headerValues) { + for (String headerValue : headerValues) { + this.headers.add(headerName, headerValue); + } + return this; + } + + @Override + public RenderingResponse.Builder headers(HttpHeaders headers) { + if (headers != null) { + this.headers.putAll(headers); + } + return this; + } + + @Override + public RenderingResponse.Builder status(HttpStatus status) { + Assert.notNull(status, "'status' must not be null"); + this.status = status; + return this; + } + + + @Override + public Mono build() { + return Mono.just(new DefaultRenderingResponse(this.status, this.headers, this.name, + this.model)); + } + + static class DefaultRenderingResponse extends DefaultServerResponseBuilder.AbstractServerResponse + implements RenderingResponse { + + private final String name; + + private final Map model; + + public DefaultRenderingResponse(HttpStatus statusCode, HttpHeaders headers, String name, + Map model) { + super(statusCode, headers); + this.name = name; + this.model = unmodifiableCopy(model); + } + + private static Map unmodifiableCopy(Map m) { + return Collections.unmodifiableMap(new LinkedHashMap<>(m)); + } + + + @Override + public String name() { + return this.name; + } + + @Override + public Map model() { + return this.model; + } + + @Override + public Mono writeTo(ServerWebExchange exchange, HandlerStrategies strategies) { + ServerHttpResponse response = exchange.getResponse(); + writeStatusAndHeaders(response); + MediaType contentType = exchange.getResponse().getHeaders().getContentType(); + Locale locale = resolveLocale(exchange, strategies); + Stream viewResolverStream = strategies.viewResolvers().get(); + return Flux.fromStream(viewResolverStream) + .concatMap(viewResolver -> viewResolver.resolveViewName(this.name, locale)) + .next() + .otherwiseIfEmpty(Mono.error(new IllegalArgumentException("Could not resolve view with name '" + + this.name +"'"))) + .then(view -> view.render(this.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() + .apply(request) + .orElse(Locale.getDefault()); + + } + } + + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java index e42e5648d6..3643b4141e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java @@ -21,6 +21,7 @@ import java.net.URI; import java.nio.charset.Charset; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; @@ -47,7 +48,9 @@ import org.springframework.web.server.WebSession; /** * {@code ServerRequest} implementation based on a {@link ServerWebExchange}. + * * @author Arjen Poutsma + * @since 5.0 */ class DefaultServerRequest implements ServerRequest { @@ -166,6 +169,11 @@ class DefaultServerRequest implements ServerRequest { return delegate().getAcceptCharset(); } + @Override + public List acceptLanguage() { + return delegate().getAcceptLanguage(); + } + @Override public OptionalLong contentLength() { long value = delegate().getContentLength(); 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 820cbe2e99..2f654035ad 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -21,23 +21,17 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.core.Conventions; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -46,16 +40,15 @@ import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; 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; /** * Default {@link ServerResponse.BodyBuilder} implementation. * * @author Arjen Poutsma + * @since 5.0 */ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -197,31 +190,28 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public Mono render(String name, Object... modelAttributes) { Assert.hasLength(name, "'name' must not be empty"); - return render(name, toModelMap(modelAttributes)); + + return new DefaultRenderingResponseBuilder(name) + .headers(this.headers) + .status(this.statusCode) + .modelAttributes(modelAttributes) + .build() + .map(renderingResponse -> renderingResponse); } @Override public Mono render(String name, Map model) { Assert.hasLength(name, "'name' must not be empty"); - Map modelMap = new LinkedHashMap<>(); - if (model != null) { - modelMap.putAll(model); - } - return Mono - .just(new RenderingServerResponse(this.statusCode, this.headers, name, modelMap)); + + return new DefaultRenderingResponseBuilder(name) + .headers(this.headers) + .status(this.statusCode) + .modelAttributes(model) + .build() + .map(renderingResponse -> renderingResponse); } - private Map toModelMap(Object[] modelAttributes) { - if (ObjectUtils.isEmpty(modelAttributes)) { - return null; - } - return Arrays.stream(modelAttributes) - .filter(val -> !ObjectUtils.isEmpty(val)) - .collect(Collectors.toMap(Conventions::getVariableName, val -> val)); - } - - - private static abstract class AbstractServerResponse implements ServerResponse { + static abstract class AbstractServerResponse implements ServerResponse { private final HttpStatus statusCode; @@ -312,38 +302,4 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } } - - private static final class RenderingServerResponse extends AbstractServerResponse { - - private final String name; - - private final Map model; - - - public RenderingServerResponse(HttpStatus statusCode, HttpHeaders headers, String name, - Map model) { - - super(statusCode, headers); - this.name = name; - this.model = Collections.unmodifiableMap(model); - } - - @Override - public Mono writeTo(ServerWebExchange exchange, HandlerStrategies strategies) { - ServerHttpResponse response = exchange.getResponse(); - writeStatusAndHeaders(response); - MediaType contentType = exchange.getResponse().getHeaders().getContentType(); - Locale acceptLocale = exchange.getRequest().getHeaders().getAcceptLanguageAsLocale(); - Locale locale = (acceptLocale != null ? acceptLocale : Locale.getDefault()); - Stream viewResolverStream = strategies.viewResolvers().get(); - return Flux.fromStream(viewResolverStream) - .concatMap(viewResolver -> viewResolver.resolveViewName(this.name, locale)) - .next() - .otherwiseIfEmpty(Mono.error(new IllegalArgumentException("Could not resolve view with name '" + - this.name +"'"))) - .then(view -> view.render(this.model, contentType, exchange)); - } - - } - } 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 739b7d22f8..641b144ec2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -16,6 +16,9 @@ package org.springframework.web.reactive.function.server; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -29,8 +32,7 @@ import org.springframework.web.reactive.result.view.ViewResolver; * Defines the strategies to be used for processing {@link HandlerFunction}s. An instance of * this class is immutable; instances are typically created through the mutable {@link Builder}: * either through {@link #builder()} to set up default strategies, or {@link #empty()} to start from - * scratch. Alternatively, {@code HandlerStrategies} instances can be created through - * {@link #of(Supplier, Supplier, Supplier)}. + * scratch. * * @author Arjen Poutsma * @author Juergen Hoeller @@ -63,6 +65,12 @@ public interface HandlerStrategies { */ Supplier> viewResolvers(); + /** + * Supply a function that resolves the locale of a given {@link ServerRequest}. + * @return the locale resolver + */ + Function> localeResolver(); + // Static methods @@ -88,39 +96,6 @@ public interface HandlerStrategies { return builder(applicationContext).build(); } - /** - * Return a new {@code HandlerStrategies} described by the given supplier functions. - * All provided supplier function parameters can be {@code null} to indicate an empty - * stream is to be returned. - * @param messageReaders the supplier function for {@link HttpMessageReader} instances (can be {@code null}) - * @param messageWriters the supplier function for {@link HttpMessageWriter} instances (can be {@code null}) - * @param viewResolvers the supplier function for {@link ViewResolver} instances (can be {@code null}) - * @return the new {@code HandlerStrategies} - */ - static HandlerStrategies of(Supplier>> messageReaders, - Supplier>> messageWriters, - Supplier> viewResolvers) { - - return new HandlerStrategies() { - @Override - public Supplier>> messageReaders() { - return checkForNull(messageReaders); - } - @Override - public Supplier>> messageWriters() { - return checkForNull(messageWriters); - } - @Override - public Supplier> viewResolvers() { - return checkForNull(viewResolvers); - } - private Supplier> checkForNull(Supplier> supplier) { - return supplier != null ? supplier : Stream::empty; - } - }; - } - - // Builder methods /** @@ -184,6 +159,13 @@ 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); + /** * Builds the {@link HandlerStrategies}. * @return the built strategies diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.java new file mode 100644 index 0000000000..8c803613bc --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RenderingResponse.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; + +import java.util.Collection; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * Rendering-specific subtype of {@link ServerResponse} that exposes model and template data. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public interface RenderingResponse extends ServerResponse { + + /** + * Return the name of the template to be rendered. + */ + String name(); + + /** + * Return the unmodifiable model map. + */ + Map model(); + + // Builder + + /** + * Create a builder with the given template name. + * + * @param name the name of the template to render + * @return the created builder + */ + static Builder create(String name) { + Assert.notNull(name, "'name' must not be null"); + return new DefaultRenderingResponseBuilder(name); + } + + /** + * Defines a builder that adds a body to the response. + */ + interface Builder { + + /** + * Add the supplied attribute to the model using a + * {@linkplain org.springframework.core.Conventions#getVariableName generated name}. + *

Note: Empty {@link Collection Collections} are not added to + * the model when using this method because we cannot correctly determine + * the true convention name. View code should check for {@code null} rather + * than for empty collections. + * @param attribute the model attribute value (never {@code null}) + */ + Builder modelAttribute(Object attribute); + + /** + * Add the supplied attribute value under the supplied name. + * @param name the name of the model attribute (never {@code null}) + * @param value the model attribute value (can be {@code null}) + */ + Builder modelAttribute(String name, Object value); + + /** + * Copy all attributes in the supplied array into the model, using attribute + * name generation for each element. + * @see #modelAttribute(Object) + */ + Builder modelAttributes(Object... attributes); + + /** + * Copy all attributes in the supplied {@code Collection} into the model, using attribute + * name generation for each element. + * @see #modelAttribute(Object) + */ + Builder modelAttributes(Collection attributes); + + /** + * Copy all attributes in the supplied {@code Map} into the model. + * @see #modelAttribute(String, Object) + */ + Builder modelAttributes(Map attributes); + + /** + * Add the given header value(s) under the given name. + * @param headerName the header name + * @param headerValues the header value(s) + * @return this builder + * @see HttpHeaders#add(String, String) + */ + Builder header(String headerName, String... headerValues); + + /** + * Copy the given headers into the entity's headers map. + * @param headers the existing HttpHeaders to copy from + * @return this builder + * @see HttpHeaders#add(String, String) + */ + Builder headers(HttpHeaders headers); + + /** + * Set the status. + * @param status the response status + * @return this builder + */ + Builder status(HttpStatus status); + + /** + * Build the response. + * + * @return the built response + */ + Mono build(); + + } + + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java index d6ad9b8f9d..0ad9183361 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -33,6 +33,8 @@ import org.springframework.http.HttpStatus; import org.springframework.web.reactive.function.BodyInserters; /** + * Resource-based implementation of {@link HandlerFunction}. + * * @author Arjen Poutsma * @since 5.0 */ 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 f7f6373ee9..1262e1014a 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 @@ -20,6 +20,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; @@ -182,6 +183,12 @@ public interface ServerRequest { */ List acceptCharset(); + /** + * Return the list of acceptable {@linkplain Locale.LanguageRange languages}, + * as specified by the {@code Accept-Language} header. + */ + List acceptLanguage(); + /** * Return the length of the body in bytes, as specified by the * {@code Content-Length} header. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java index 4193edc7d5..135a862e66 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/ServerRequestWrapper.java @@ -20,6 +20,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.Charset; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; @@ -167,6 +168,11 @@ public class ServerRequestWrapper implements ServerRequest { return this.headers.acceptCharset(); } + @Override + public List acceptLanguage() { + return this.headers.acceptLanguage(); + } + @Override public OptionalLong contentLength() { return this.headers.contentLength(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java new file mode 100644 index 0000000000..8f3a4ff15c --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultRenderingResponseTests.java @@ -0,0 +1,137 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Arjen Poutsma + */ +public class DefaultRenderingResponseTests { + + @Test + public void create() throws Exception { + String name = "foo"; + Mono result = RenderingResponse.create(name).build(); + StepVerifier.create(result) + .expectNextMatches(response -> name.equals(response.name())) + .expectComplete() + .verify(); + } + + @Test + public void headers() throws Exception { + HttpHeaders headers = new HttpHeaders(); + Mono result = RenderingResponse.create("foo").headers(headers).build(); + StepVerifier.create(result) + .expectNextMatches(response -> headers.equals(response.headers())) + .expectComplete() + .verify(); + + } + + @Test + public void modelAttribute() throws Exception { + Mono result = RenderingResponse.create("foo") + .modelAttribute("foo", "bar").build(); + StepVerifier.create(result) + .expectNextMatches(response -> "bar".equals(response.model().get("foo"))) + .expectComplete() + .verify(); + } + + @Test + public void modelAttributeConventions() throws Exception { + Mono result = RenderingResponse.create("foo") + .modelAttribute("bar").build(); + StepVerifier.create(result) + .expectNextMatches(response -> "bar".equals(response.model().get("string"))) + .expectComplete() + .verify(); + } + + @Test + public void modelAttributes() throws Exception { + Map model = Collections.singletonMap("foo", "bar"); + Mono result = RenderingResponse.create("foo") + .modelAttributes(model).build(); + StepVerifier.create(result) + .expectNextMatches(response -> "bar".equals(response.model().get("foo"))) + .expectComplete() + .verify(); + } + + @Test + public void modelAttributesConventions() throws Exception { + Set model = Collections.singleton("bar"); + Mono result = RenderingResponse.create("foo") + .modelAttributes(model).build(); + StepVerifier.create(result) + .expectNextMatches(response -> "bar".equals(response.model().get("string"))) + .expectComplete() + .verify(); + } + + @Test + public void render() throws Exception { + Map model = Collections.singletonMap("foo", "bar"); + Mono result = RenderingResponse.create("view").modelAttributes(model).build(); + + MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build(); + MockServerHttpResponse mockResponse = new MockServerHttpResponse(); + ServerWebExchange exchange = + new DefaultServerWebExchange(request, mockResponse, new MockWebSessionManager()); + ViewResolver viewResolver = mock(ViewResolver.class); + View view = mock(View.class); + when(viewResolver.resolveViewName("view", Locale.ENGLISH)).thenReturn(Mono.just(view)); + when(view.render(model, null, exchange)).thenReturn(Mono.empty()); + + List viewResolvers = new ArrayList<>(); + viewResolvers.add(viewResolver); + + HandlerStrategies mockConfig = mock(HandlerStrategies.class); + when(mockConfig.viewResolvers()).thenReturn(viewResolvers::stream); + + StepVerifier.create(result) + .expectNextMatches(response -> "view".equals(response.name()) && + model.equals(response.model())) + .expectComplete() + .verify(); + } + +} \ No newline at end of file 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 9b885a63e2..5cfd07ba2e 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 @@ -309,59 +309,5 @@ TODO: enable when ServerEntityResponse is reintroduced } */ -/* -TODO: enable when ServerEntityResponse is reintroduced - @Test - public void render() throws Exception { - Map model = Collections.singletonMap("foo", "bar"); - Mono result = ServerResponse.ok().render("view", model); - - - MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, URI.create("http://localhost")); - MockServerHttpResponse mockResponse = new MockServerHttpResponse(); - ServerWebExchange exchange = new DefaultServerWebExchange(request, mockResponse, new MockWebSessionManager()); - ViewResolver viewResolver = mock(ViewResolver.class); - View view = mock(View.class); - when(viewResolver.resolveViewName("view", Locale.ENGLISH)).thenReturn(Mono.just(view)); - when(view.render(model, null, exchange)).thenReturn(Mono.empty()); - - List viewResolvers = new ArrayList<>(); - viewResolvers.add(viewResolver); - - HandlerStrategies mockConfig = mock(HandlerStrategies.class); - when(mockConfig.viewResolvers()).thenReturn(viewResolvers::stream); - - StepVerifier.create(result) - .consumeNextWith(response -> { - StepVerifier.create(response.body()) - .expectNextMatches(rendering -> "view".equals(rendering.name()) - && model.equals(rendering.model())) - .expectComplete() - .verify(); - }) - .expectComplete() - .verify(); - } -*/ - -/* -TODO: enable when ServerEntityResponse is reintroduced - - @Test - public void renderObjectArray() throws Exception { - Mono result = - ServerResponse.ok().render("name", this, Collections.emptyList(), "foo"); - Flux map = result.flatMap(ServerResponse::body); - - Map expected = new HashMap<>(2); - expected.put("defaultServerResponseBuilderTests", this); - expected.put("string", "foo"); - - StepVerifier.create(map) - .expectNextMatches(rendering -> expected.equals(rendering.model())) - .expectComplete() - .verify(); - } -*/ } \ No newline at end of file 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 4d5b96ba98..56ca078577 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 @@ -17,6 +17,9 @@ package org.springframework.web.reactive.function.server; import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -136,6 +139,11 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr public Supplier> viewResolvers() { return Stream::empty; } + + @Override + public Function> localeResolver() { + return DefaultHandlerStrategiesBuilder.DEFAULT_LOCALE_RESOLVER; + } }); } 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 5dfc452860..c68079cacc 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -20,8 +20,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.junit.Test; import org.reactivestreams.Publisher; @@ -57,15 +55,15 @@ public class HandlerStrategiesTests { HttpMessageReader messageReader = new DummyMessageReader(); HttpMessageWriter messageWriter = new DummyMessageWriter(); - HandlerStrategies strategies = HandlerStrategies.of( - () -> Stream.of(messageReader), - () -> Stream.of(messageWriter), - null); + HandlerStrategies strategies = HandlerStrategies.empty() + .messageReader(messageReader) + .messageWriter(messageWriter) + .build(); - assertEquals(1L, strategies.messageReaders().get().collect(Collectors.counting()).longValue()); + assertEquals(1L, ((Long) strategies.messageReaders().get().count()).longValue()); assertEquals(Optional.of(messageReader), strategies.messageReaders().get().findFirst()); - assertEquals(1L, strategies.messageWriters().get().collect(Collectors.counting()).longValue()); + assertEquals(1L, ((Long) strategies.messageWriters().get().count()).longValue()); assertEquals(Optional.of(messageWriter), strategies.messageWriters().get().findFirst()); assertEquals(Optional.empty(), strategies.viewResolvers().get().findFirst()); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java index ed8e2c549f..a457baeff3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/MockServerRequest.java @@ -25,6 +25,7 @@ import java.time.ZonedDateTime; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; @@ -312,6 +313,11 @@ public class MockServerRequest implements ServerRequest { return delegate().getAcceptCharset(); } + @Override + public List acceptLanguage() { + return delegate().getAcceptLanguage(); + } + @Override public OptionalLong contentLength() { return toOptionalLong(delegate().getContentLength());