Introduce RenderingResponse
This commit introduces the RenderingResponse, a template rendering-specific subtype of ServerResponse that exposes model and template data.
This commit is contained in:
parent
71cdd61b33
commit
7796c4db14
|
|
@ -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<ServerRequest, Optional<Locale>> 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<ViewResolver> viewResolvers = new ArrayList<>();
|
||||
|
||||
private Function<ServerRequest, Optional<Locale>> 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<ServerRequest, Optional<Locale>> 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<ViewResolver> viewResolvers;
|
||||
|
||||
private final Function<ServerRequest, Optional<Locale>> localeResolver;
|
||||
|
||||
|
||||
public DefaultHandlerStrategies(
|
||||
List<HttpMessageReader<?>> messageReaders,
|
||||
List<HttpMessageWriter<?>> messageWriters,
|
||||
List<ViewResolver> viewResolvers) {
|
||||
List<ViewResolver> viewResolvers,
|
||||
Function<ServerRequest, Optional<Locale>> localeResolver) {
|
||||
this.messageReaders = unmodifiableCopy(messageReaders);
|
||||
this.messageWriters = unmodifiableCopy(messageWriters);
|
||||
this.viewResolvers = unmodifiableCopy(viewResolvers);
|
||||
this.localeResolver = localeResolver;
|
||||
}
|
||||
|
||||
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
|
||||
|
|
@ -164,6 +187,11 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
|
|||
public Supplier<Stream<ViewResolver>> viewResolvers() {
|
||||
return this.viewResolvers::stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<ServerRequest, Optional<Locale>> localeResolver() {
|
||||
return this.localeResolver;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, Object> model = new LinkedHashMap<String, Object>();
|
||||
|
||||
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<String, ?> 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<RenderingResponse> 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<String, Object> model;
|
||||
|
||||
public DefaultRenderingResponse(HttpStatus statusCode, HttpHeaders headers, String name,
|
||||
Map<String, Object> model) {
|
||||
super(statusCode, headers);
|
||||
this.name = name;
|
||||
this.model = unmodifiableCopy(model);
|
||||
}
|
||||
|
||||
private static <K, V> Map<K, V> unmodifiableCopy(Map<? extends K, ? extends V> m) {
|
||||
return Collections.unmodifiableMap(new LinkedHashMap<>(m));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> model() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
writeStatusAndHeaders(response);
|
||||
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
|
||||
Locale locale = resolveLocale(exchange, strategies);
|
||||
Stream<ViewResolver> 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.<ServerRequest>getAttribute(RouterFunctions.REQUEST_ATTRIBUTE)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Could not find ServerRequest in exchange attributes"));
|
||||
|
||||
return strategies.localeResolver()
|
||||
.apply(request)
|
||||
.orElse(Locale.getDefault());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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<Locale.LanguageRange> acceptLanguage() {
|
||||
return delegate().getAcceptLanguage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong contentLength() {
|
||||
long value = delegate().getContentLength();
|
||||
|
|
|
|||
|
|
@ -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<ServerResponse> 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<ServerResponse> render(String name, Map<String, ?> model) {
|
||||
Assert.hasLength(name, "'name' must not be empty");
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> model;
|
||||
|
||||
|
||||
public RenderingServerResponse(HttpStatus statusCode, HttpHeaders headers, String name,
|
||||
Map<String, Object> model) {
|
||||
|
||||
super(statusCode, headers);
|
||||
this.name = name;
|
||||
this.model = Collections.unmodifiableMap(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> 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<ViewResolver> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Stream<ViewResolver>> viewResolvers();
|
||||
|
||||
/**
|
||||
* Supply a function that resolves the locale of a given {@link ServerRequest}.
|
||||
* @return the locale resolver
|
||||
*/
|
||||
Function<ServerRequest, Optional<Locale>> 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<Stream<HttpMessageReader<?>>> messageReaders,
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters,
|
||||
Supplier<Stream<ViewResolver>> viewResolvers) {
|
||||
|
||||
return new HandlerStrategies() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return checkForNull(messageReaders);
|
||||
}
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return checkForNull(messageWriters);
|
||||
}
|
||||
@Override
|
||||
public Supplier<Stream<ViewResolver>> viewResolvers() {
|
||||
return checkForNull(viewResolvers);
|
||||
}
|
||||
private <T> Supplier<Stream<T>> checkForNull(Supplier<Stream<T>> 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<ServerRequest, Optional<Locale>> localeResolver);
|
||||
|
||||
/**
|
||||
* Builds the {@link HandlerStrategies}.
|
||||
* @return the built strategies
|
||||
|
|
|
|||
|
|
@ -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<String, Object> 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}.
|
||||
* <p><emphasis>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.</emphasis>
|
||||
* @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<String, ?> 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<RenderingResponse> build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<Charset> acceptCharset();
|
||||
|
||||
/**
|
||||
* Return the list of acceptable {@linkplain Locale.LanguageRange languages},
|
||||
* as specified by the {@code Accept-Language} header.
|
||||
*/
|
||||
List<Locale.LanguageRange> acceptLanguage();
|
||||
|
||||
/**
|
||||
* Return the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
|
|
|
|||
|
|
@ -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<Locale.LanguageRange> acceptLanguage() {
|
||||
return this.headers.acceptLanguage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong contentLength() {
|
||||
return this.headers.contentLength();
|
||||
|
|
|
|||
|
|
@ -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<RenderingResponse> 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<RenderingResponse> 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<RenderingResponse> 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<RenderingResponse> 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<String, String> model = Collections.singletonMap("foo", "bar");
|
||||
Mono<RenderingResponse> 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<String> model = Collections.singleton("bar");
|
||||
Mono<RenderingResponse> 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<String, Object> model = Collections.singletonMap("foo", "bar");
|
||||
Mono<RenderingResponse> 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<ViewResolver> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -309,59 +309,5 @@ TODO: enable when ServerEntityResponse is reintroduced
|
|||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
TODO: enable when ServerEntityResponse is reintroduced
|
||||
@Test
|
||||
public void render() throws Exception {
|
||||
Map<String, Object> model = Collections.singletonMap("foo", "bar");
|
||||
Mono<ServerResponse> 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<ViewResolver> 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<ServerResponse> result =
|
||||
ServerResponse.ok().render("name", this, Collections.emptyList(), "foo");
|
||||
Flux<Rendering> map = result.flatMap(ServerResponse::body);
|
||||
|
||||
Map<String, Object> expected = new HashMap<>(2);
|
||||
expected.put("defaultServerResponseBuilderTests", this);
|
||||
expected.put("string", "foo");
|
||||
|
||||
StepVerifier.create(map)
|
||||
.expectNextMatches(rendering -> expected.equals(rendering.model()))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
|
@ -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<Stream<ViewResolver>> viewResolvers() {
|
||||
return Stream::empty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<ServerRequest, Optional<Locale>> localeResolver() {
|
||||
return DefaultHandlerStrategiesBuilder.DEFAULT_LOCALE_RESOLVER;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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<Locale.LanguageRange> acceptLanguage() {
|
||||
return delegate().getAcceptLanguage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong contentLength() {
|
||||
return toOptionalLong(delegate().getContentLength());
|
||||
|
|
|
|||
Loading…
Reference in New Issue