Merge templating branch
This commit is contained in:
@@ -84,6 +84,7 @@ dependencies {
|
||||
compile "io.projectreactor:reactor-core:${reactorVersion}"
|
||||
compile "commons-logging:commons-logging:1.2"
|
||||
|
||||
optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker
|
||||
optional 'io.reactivex:rxjava:1.1.0'
|
||||
optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT"
|
||||
optional "com.fasterxml.jackson.core:jackson-databind:2.6.2"
|
||||
@@ -95,6 +96,7 @@ dependencies {
|
||||
optional 'io.undertow:undertow-core:1.3.5.Final'
|
||||
optional "org.eclipse.jetty:jetty-server:${jettyVersion}"
|
||||
optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}"
|
||||
optional("org.freemarker:freemarker:2.3.23")
|
||||
|
||||
provided "javax.servlet:javax.servlet-api:3.1.0"
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware {
|
||||
return resultHandler;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getResult());
|
||||
throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getReturnValue());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -32,40 +35,67 @@ public class HandlerResult {
|
||||
|
||||
private final Object handler;
|
||||
|
||||
private final Object result;
|
||||
private final Optional<Object> returnValue;
|
||||
|
||||
private final ResolvableType resultType;
|
||||
private final ResolvableType returnValueType;
|
||||
|
||||
private final ModelMap model;
|
||||
|
||||
private Function<Throwable, Mono<HandlerResult>> exceptionHandler;
|
||||
|
||||
|
||||
public HandlerResult(Object handler, Object result, ResolvableType resultType) {
|
||||
/**
|
||||
* Create a new {@code HandlerResult}.
|
||||
* @param handler the handler that handled the request
|
||||
* @param returnValue the return value from the handler possibly {@code null}
|
||||
* @param returnValueType the return value type
|
||||
* @param model the model used for request handling
|
||||
*/
|
||||
public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType, ModelMap model) {
|
||||
Assert.notNull(handler, "'handler' is required");
|
||||
Assert.notNull(handler, "'resultType' is required");
|
||||
Assert.notNull(returnValueType, "'returnValueType' is required");
|
||||
Assert.notNull(model, "'model' is required");
|
||||
this.handler = handler;
|
||||
this.result = result;
|
||||
this.resultType = resultType;
|
||||
this.returnValue = Optional.ofNullable(returnValue);
|
||||
this.returnValueType = returnValueType;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the handler that handled the request.
|
||||
*/
|
||||
public Object getHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
public Object getResult() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
public ResolvableType getResultType() {
|
||||
return this.resultType;
|
||||
/**
|
||||
* Return the value returned from the handler wrapped as {@link Optional}.
|
||||
*/
|
||||
public Optional<Object> getReturnValue() {
|
||||
return this.returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* For an async result, failures may occur later during result handling.
|
||||
* Use this property to configure an exception handler to be invoked if
|
||||
* result handling fails.
|
||||
*
|
||||
* @param function a function to map the the error to an alternative result.
|
||||
* Return the type of the value returned from the handler.
|
||||
*/
|
||||
public ResolvableType getReturnValueType() {
|
||||
return this.returnValueType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the model used during request handling with attributes that may be
|
||||
* used to render HTML templates with.
|
||||
*/
|
||||
public ModelMap getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an exception handler that may be used to produce an alternative
|
||||
* result when result handling fails. Especially for an async return value
|
||||
* errors may occur after the invocation of the handler.
|
||||
* @param function the error handler
|
||||
* @return the current instance
|
||||
*/
|
||||
public HandlerResult setExceptionHandler(Function<Throwable, Mono<HandlerResult>> function) {
|
||||
@@ -73,12 +103,20 @@ public class HandlerResult {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether there is an exception handler.
|
||||
*/
|
||||
public boolean hasExceptionHandler() {
|
||||
return (this.exceptionHandler != null);
|
||||
}
|
||||
|
||||
public Mono<HandlerResult> applyExceptionHandler(Throwable ex) {
|
||||
return (hasExceptionHandler() ? this.exceptionHandler.apply(ex) : Mono.error(ex));
|
||||
/**
|
||||
* Apply the exception handler and return the alternative result.
|
||||
* @param failure the exception
|
||||
* @return the new result or the same error if there is no exception handler
|
||||
*/
|
||||
public Mono<HandlerResult> applyExceptionHandler(Throwable failure) {
|
||||
return (hasExceptionHandler() ? this.exceptionHandler.apply(failure) : Mono.error(failure));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Contract to render {@link HandlerResult} to the HTTP response.
|
||||
*
|
||||
* <p>In contrast to an {@link org.springframework.core.codec.Encoder Encoder}
|
||||
* which is a singleton and encodes any object of a given type, a {@code View}
|
||||
* is typically selected by name and resolved using a {@link ViewResolver}
|
||||
* which may for example match it to an HTML template. Furthermore a {@code View}
|
||||
* may render based on multiple attributes contained in the model.
|
||||
*
|
||||
* <p>A {@code View} can also choose to select an attribute from the model use
|
||||
* any existing {@code Encoder} to render alternate media types.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public interface View {
|
||||
|
||||
/**
|
||||
* Return the list of media types this encoder supports.
|
||||
*/
|
||||
List<MediaType> getSupportedMediaTypes();
|
||||
|
||||
/**
|
||||
* Render the view based on the given {@link HandlerResult}. Implementations
|
||||
* can access and use the model or only a specific attribute in it.
|
||||
* @param result the result from handler execution
|
||||
* @param contentType the content type selected to render with which should
|
||||
* match one of the {@link #getSupportedMediaTypes() supported media types}.
|
||||
* @param exchange the current exchange
|
||||
* @return the output stream
|
||||
*/
|
||||
Flux<DataBuffer> render(HandlerResult result, Optional<MediaType> contentType, ServerWebExchange exchange);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Contract to resolve a view name to a {@link View} instance. The view name may
|
||||
* correspond to an HTML template or be generated dynamically.
|
||||
*
|
||||
* <p>The process of view resolution is driven through a ViewResolver-based
|
||||
* {@code HandlerResultHandler} implementation called
|
||||
* {@link org.springframework.web.reactive.view.ViewResolverResultHandler
|
||||
* ViewResolverResultHandler}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @see org.springframework.web.reactive.view.ViewResolverResultHandler
|
||||
|
||||
*/
|
||||
public interface ViewResolver {
|
||||
|
||||
/**
|
||||
* Resolve the view name to a View instance.
|
||||
* @param viewName the name of the view to resolve
|
||||
* @param locale the locale for the request
|
||||
* @return the resolved view or an empty stream
|
||||
*/
|
||||
Mono<View> resolveViewName(String viewName, Locale locale);
|
||||
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.reactive.HandlerAdapter;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
@@ -48,7 +50,8 @@ public class HttpHandlerHandlerAdapter implements HandlerAdapter {
|
||||
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
|
||||
WebHandler webHandler = (WebHandler) handler;
|
||||
Mono<Void> completion = webHandler.handle(exchange);
|
||||
return Mono.just(new HandlerResult(webHandler, completion, PUBLISHER_VOID));
|
||||
ModelMap model = new ExtendedModelMap();
|
||||
return Mono.just(new HandlerResult(webHandler, completion, PUBLISHER_VOID, model));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.reactive.handler;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -61,7 +63,7 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler
|
||||
|
||||
@Override
|
||||
public boolean supports(HandlerResult result) {
|
||||
ResolvableType type = result.getResultType();
|
||||
ResolvableType type = result.getReturnValueType();
|
||||
return (type != null && Void.TYPE.equals(type.getRawClass()) ||
|
||||
(isConvertibleToPublisher(type) && Void.class.isAssignableFrom(type.getGeneric(0).getRawClass())));
|
||||
}
|
||||
@@ -75,12 +77,14 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
|
||||
Object value = result.getResult();
|
||||
if (Void.TYPE.equals(result.getResultType().getRawClass())) {
|
||||
Optional<Object> value = result.getReturnValue();
|
||||
if (!value.isPresent() || Void.TYPE.equals(result.getReturnValueType().getRawClass())) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return (value instanceof Mono ? (Mono<Void>)value :
|
||||
Mono.from(this.conversionService.convert(value, Publisher.class)));
|
||||
if (value.get() instanceof Mono) {
|
||||
return (Mono<Void>) value.get();
|
||||
}
|
||||
return Mono.from(this.conversionService.convert(value.get(), Publisher.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.web.reactive.method;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
|
||||
@@ -34,7 +35,10 @@ public interface HandlerMethodArgumentResolver {
|
||||
* The returned {@link Mono} may produce one or zero values if the argument
|
||||
* does not resolve to any value, which will result in {@code null} passed
|
||||
* as the argument value.
|
||||
* @param parameter the method parameter
|
||||
* @param model the implicit model for request handling
|
||||
* @param exchange the current exchange
|
||||
*/
|
||||
Mono<Object> resolveArgument(MethodParameter parameter, ServerWebExchange exchange);
|
||||
Mono<Object> resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange);
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
@@ -77,17 +78,20 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||
/**
|
||||
* Invoke the method and return a Publisher for the return value.
|
||||
* @param exchange the current exchange
|
||||
* @param model the model for request handling
|
||||
* @param providedArgs optional list of argument values to check by type
|
||||
* (via {@code instanceof}) for resolving method arguments.
|
||||
* @return Publisher that produces a single HandlerResult or an error signal;
|
||||
* never throws an exception
|
||||
*/
|
||||
public Mono<HandlerResult> invokeForRequest(ServerWebExchange exchange, Object... providedArgs) {
|
||||
return resolveArguments(exchange, providedArgs).then(args -> {
|
||||
public Mono<HandlerResult> invokeForRequest(ServerWebExchange exchange, ModelMap model,
|
||||
Object... providedArgs) {
|
||||
|
||||
return resolveArguments(exchange, model, providedArgs).then(args -> {
|
||||
try {
|
||||
Object value = doInvoke(args);
|
||||
ResolvableType type = ResolvableType.forMethodParameter(getReturnType());
|
||||
HandlerResult handlerResult = new HandlerResult(this, value, type);
|
||||
HandlerResult handlerResult = new HandlerResult(this, value, type, model);
|
||||
return Mono.just(handlerResult);
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
@@ -100,7 +104,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Object[]> resolveArguments(ServerWebExchange exchange, Object... providedArgs) {
|
||||
private Mono<Object[]> resolveArguments(ServerWebExchange exchange, ModelMap model, Object... providedArgs) {
|
||||
if (ObjectUtils.isEmpty(getMethodParameters())) {
|
||||
return NO_ARGS;
|
||||
}
|
||||
@@ -121,7 +125,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||
.findFirst()
|
||||
.orElseThrow(() -> getArgError("No resolver for ", param, null));
|
||||
try {
|
||||
return resolver.resolveArgument(param, exchange)
|
||||
return resolver.resolveArgument(param, model, exchange)
|
||||
.defaultIfEmpty(NO_VALUE)
|
||||
.otherwise(ex -> Mono.error(getArgError("Error resolving ", param, ex)))
|
||||
.log("reactor.unresolved");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.method.annotation;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.reactive.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Resolver for the {@link Model} controller method argument.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ModelArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return Model.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) {
|
||||
return Mono.just(model);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.reactive.method.HandlerMethodArgumentResolver;
|
||||
@@ -58,7 +59,9 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(MethodParameter parameter, ServerWebExchange exchange) {
|
||||
public Mono<Object> resolveArgument(MethodParameter parameter, ModelMap model,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
|
||||
if (mediaType == null) {
|
||||
mediaType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
|
||||
@@ -38,6 +38,8 @@ import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.buffer.DataBufferAllocator;
|
||||
import org.springframework.core.io.buffer.NettyDataBufferAllocator;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
|
||||
@@ -64,7 +66,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
|
||||
new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false));
|
||||
|
||||
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
|
||||
new ConcurrentHashMap<Class<?>, ExceptionHandlerMethodResolver>(64);
|
||||
new ConcurrentHashMap<>(64);
|
||||
|
||||
|
||||
/**
|
||||
@@ -105,6 +107,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
|
||||
|
||||
this.argumentResolvers.add(new RequestParamArgumentResolver());
|
||||
this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, this.conversionService));
|
||||
this.argumentResolvers.add(new ModelArgumentResolver());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +121,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
|
||||
invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers);
|
||||
|
||||
return invocable.invokeForRequest(exchange)
|
||||
ModelMap model = new ExtendedModelMap();
|
||||
return invocable.invokeForRequest(exchange, model)
|
||||
.map(result -> result.setExceptionHandler(ex -> handleException(ex, handlerMethod, exchange)))
|
||||
.otherwise(ex -> handleException(ex, handlerMethod, exchange));
|
||||
}
|
||||
@@ -135,7 +138,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
|
||||
logger.debug("Invoking @ExceptionHandler method: " + invocable);
|
||||
}
|
||||
invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers());
|
||||
return invocable.invokeForRequest(exchange, ex);
|
||||
ExtendedModelMap errorModel = new ExtendedModelMap();
|
||||
return invocable.invokeForRequest(exchange, errorModel, ex);
|
||||
}
|
||||
catch (Exception invocationEx) {
|
||||
if (logger.isErrorEnabled()) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.web.reactive.method.annotation;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.reactive.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -41,7 +42,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(MethodParameter param, ServerWebExchange exchange) {
|
||||
public Mono<Object> resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) {
|
||||
RequestParam annotation = param.getParameterAnnotation(RequestParam.class);
|
||||
String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName());
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build();
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -65,7 +66,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
|
||||
|
||||
private final Map<Encoder<?>, List<MediaType>> mediaTypesByEncoder;
|
||||
|
||||
private int order = 0;
|
||||
private int order = 0; // TODO: should be MAX_VALUE
|
||||
|
||||
|
||||
public ResponseBodyResultHandler(List<Encoder<?>> encoders, ConversionService service) {
|
||||
@@ -132,23 +133,23 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
|
||||
|
||||
Object value = result.getResult();
|
||||
if (value == null) {
|
||||
Optional<Object> value = result.getReturnValue();
|
||||
if (!value.isPresent()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
Publisher<?> publisher;
|
||||
ResolvableType elementType;
|
||||
ResolvableType returnType = result.getResultType();
|
||||
ResolvableType returnType = result.getReturnValueType();
|
||||
if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) {
|
||||
publisher = this.conversionService.convert(value, Publisher.class);
|
||||
publisher = this.conversionService.convert(value.get(), Publisher.class);
|
||||
elementType = returnType.getGeneric(0);
|
||||
if (Void.class.equals(elementType.getRawClass())) {
|
||||
return (Mono<Void>)Mono.from(publisher);
|
||||
}
|
||||
}
|
||||
else {
|
||||
publisher = Mono.just(value);
|
||||
publisher = Mono.just(value.get());
|
||||
elementType = returnType;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* Abstract base class for URL-based views. Provides a consistent way of
|
||||
* holding the URL that a View wraps, in the form of a "url" bean property.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public abstract class AbstractUrlBasedView extends AbstractView implements InitializingBean {
|
||||
|
||||
private String url;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for use as a bean.
|
||||
*/
|
||||
protected AbstractUrlBasedView() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new AbstractUrlBasedView with the given URL.
|
||||
*/
|
||||
protected AbstractUrlBasedView(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the URL of the resource that this view wraps.
|
||||
* The URL must be appropriate for the concrete View implementation.
|
||||
*/
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URL of the resource that this view wraps.
|
||||
*/
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (getUrl() == null) {
|
||||
throw new IllegalArgumentException("Property 'url' is required");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the resource for the configured URL actually exists.
|
||||
* @param locale the desired Locale that we're looking for
|
||||
* @return {@code false} if the resource exists
|
||||
* {@code false} if we know that it does not exist
|
||||
* @throws Exception if the resource exists but is invalid (e.g. could not be parsed)
|
||||
*/
|
||||
public abstract boolean checkResourceExists(Locale locale) throws Exception;
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + "; URL [" + getUrl() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferAllocator;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferAllocator;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.View;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public abstract class AbstractView implements View, ApplicationContextAware {
|
||||
|
||||
/** Logger that is available to subclasses */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
private final List<MediaType> mediaTypes = new ArrayList<>(4);
|
||||
|
||||
private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
|
||||
public AbstractView() {
|
||||
this.mediaTypes.add(ViewResolverSupport.DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the supported media types for this view.
|
||||
* Default is "text/html;charset=UTF-8".
|
||||
*/
|
||||
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
|
||||
Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' is required.");
|
||||
this.mediaTypes.clear();
|
||||
if (supportedMediaTypes != null) {
|
||||
this.mediaTypes.addAll(supportedMediaTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured media types supported by this view.
|
||||
*/
|
||||
@Override
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return this.mediaTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link DataBufferAllocator} to use for write I/O.
|
||||
* <p>By default this is set to {@link DefaultDataBufferAllocator}.
|
||||
* @param bufferAllocator the allocator to use
|
||||
*/
|
||||
public void setBufferAllocator(DataBufferAllocator bufferAllocator) {
|
||||
Assert.notNull(bufferAllocator, "'bufferAllocator' is required.");
|
||||
this.bufferAllocator = bufferAllocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured buffer allocator, never {@code null}.
|
||||
*/
|
||||
public DataBufferAllocator getBufferAllocator() {
|
||||
return this.bufferAllocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the model to render.
|
||||
* @param result the result from handler execution
|
||||
* @param contentType the content type selected to render with which should
|
||||
* match one of the {@link #getSupportedMediaTypes() supported media types}.
|
||||
* @param exchange the current exchange
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Flux<DataBuffer> render(HandlerResult result, Optional<MediaType> contentType,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Rendering view with model " + result.getModel());
|
||||
}
|
||||
|
||||
if (contentType.isPresent()) {
|
||||
exchange.getResponse().getHeaders().setContentType(contentType.get());
|
||||
}
|
||||
|
||||
Map<String, Object> mergedModel = getModelAttributes(result, exchange);
|
||||
return renderInternal(mergedModel, exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the model to use for rendering.
|
||||
* <p>The default implementation creates a combined output Map that includes
|
||||
* model as well as static attributes with the former taking precedence.
|
||||
*/
|
||||
protected Map<String, Object> getModelAttributes(HandlerResult result, ServerWebExchange exchange) {
|
||||
ModelMap model = result.getModel();
|
||||
int size = (model != null ? model.size() : 0);
|
||||
|
||||
Map<String, Object> attributes = new LinkedHashMap<>(size);
|
||||
if (model != null) {
|
||||
attributes.putAll(model);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses must implement this method to actually render the view.
|
||||
* @param renderAttributes combined output Map (never {@code null}),
|
||||
* with dynamic values taking precedence over static attributes
|
||||
* @param exchange current exchange
|
||||
*/
|
||||
protected abstract Flux<DataBuffer> renderInternal(Map<String, Object> renderAttributes,
|
||||
ServerWebExchange exchange);
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.web.reactive.View;
|
||||
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.web.reactive.ViewResolver ViewResolver} that
|
||||
* allow direct resolution of symbolic view names to URLs without explicit
|
||||
* mapping definition. This is useful if symbolic names match the names of view
|
||||
* resources in a straightforward manner (i.e. the symbolic name is the unique
|
||||
* part of the resource's filename), without the need for a dedicated mapping
|
||||
* to be defined for each view.
|
||||
*
|
||||
* <p>Supports {@link AbstractUrlBasedView} subclasses like
|
||||
* {@link org.springframework.web.reactive.view.freemarker.FreeMarkerView}.
|
||||
* The view class for all views generated by this resolver can be specified
|
||||
* via the "viewClass" property.
|
||||
*
|
||||
* <p>View names can either be resource URLs themselves, or get augmented by a
|
||||
* specified prefix and/or suffix. Exporting an attribute that holds the
|
||||
* RequestContext to all views is explicitly supported.
|
||||
*
|
||||
* <p>Example: prefix="templates/", suffix=".ftl", viewname="test" ->
|
||||
* "templates/test.ftl"
|
||||
*
|
||||
* <p>As a special feature, redirect URLs can be specified via the "redirect:"
|
||||
* prefix. E.g.: "redirect:myAction" will trigger a redirect to the given
|
||||
* URL, rather than resolution as standard view name. This is typically used
|
||||
* for redirecting to a controller URL after finishing a form workflow.
|
||||
*
|
||||
* <p>Note: This class does not support localized resolution, i.e. resolving
|
||||
* a symbolic view name to different resources depending on the current locale.
|
||||
* * @author Rossen Stoyanchev
|
||||
*/
|
||||
public class UrlBasedViewResolver extends ViewResolverSupport implements InitializingBean {
|
||||
|
||||
private Class<?> viewClass;
|
||||
|
||||
private String prefix = "";
|
||||
|
||||
private String suffix = "";
|
||||
|
||||
|
||||
/**
|
||||
* Set the view class to instantiate through {@link #createUrlBasedView(String)}.
|
||||
* @param viewClass a class that is assignable to the required view class
|
||||
* which by default is AbstractUrlBasedView.
|
||||
*/
|
||||
public void setViewClass(Class<?> viewClass) {
|
||||
if (viewClass == null || !requiredViewClass().isAssignableFrom(viewClass)) {
|
||||
String name = (viewClass != null ? viewClass.getName() : null);
|
||||
throw new IllegalArgumentException("Given view class [" + name + "] " +
|
||||
"is not of type [" + requiredViewClass().getName() + "]");
|
||||
}
|
||||
this.viewClass = viewClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the view class to be used to create views.
|
||||
*/
|
||||
protected Class<?> getViewClass() {
|
||||
return this.viewClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the required type of view for this resolver.
|
||||
* This implementation returns {@link AbstractUrlBasedView}.
|
||||
* @see AbstractUrlBasedView
|
||||
*/
|
||||
protected Class<?> requiredViewClass() {
|
||||
return AbstractUrlBasedView.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the prefix that gets prepended to view names when building a URL.
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = (prefix != null ? prefix : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the prefix that gets prepended to view names when building a URL.
|
||||
*/
|
||||
protected String getPrefix() {
|
||||
return this.prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suffix that gets appended to view names when building a URL.
|
||||
*/
|
||||
public void setSuffix(String suffix) {
|
||||
this.suffix = (suffix != null ? suffix : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the suffix that gets appended to view names when building a URL.
|
||||
*/
|
||||
protected String getSuffix() {
|
||||
return this.suffix;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (getViewClass() == null) {
|
||||
throw new IllegalArgumentException("Property 'viewClass' is required");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<View> resolveViewName(String viewName, Locale locale) {
|
||||
AbstractUrlBasedView urlBasedView = createUrlBasedView(viewName);
|
||||
View view = applyLifecycleMethods(viewName, urlBasedView);
|
||||
try {
|
||||
return (urlBasedView.checkResourceExists(locale) ? Mono.just(view) : Mono.empty());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new View instance of the specified view class and configures it.
|
||||
* Does <i>not</i> perform any lookup for pre-defined View instances.
|
||||
* <p>Spring lifecycle methods as defined by the bean container do not have to
|
||||
* be called here; those will be applied by the {@code loadView} method
|
||||
* after this method returns.
|
||||
* <p>Subclasses will typically call {@code super.buildView(viewName)}
|
||||
* first, before setting further properties themselves. {@code loadView}
|
||||
* will then apply Spring lifecycle methods at the end of this process.
|
||||
* @param viewName the name of the view to build
|
||||
* @return the View instance
|
||||
*/
|
||||
protected AbstractUrlBasedView createUrlBasedView(String viewName) {
|
||||
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
|
||||
view.setSupportedMediaTypes(getSupportedMediaTypes());
|
||||
view.setBufferAllocator(getBufferAllocator());
|
||||
view.setUrl(getPrefix() + viewName + getSuffix());
|
||||
return view;
|
||||
}
|
||||
|
||||
private View applyLifecycleMethods(String viewName, AbstractView view) {
|
||||
return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.HandlerResultHandler;
|
||||
import org.springframework.web.reactive.View;
|
||||
import org.springframework.web.reactive.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
|
||||
/**
|
||||
* {@code HandlerResultHandler} that resolves a String return value from a
|
||||
* handler to a {@link View} which is then used to render the response.
|
||||
* A handler may also return a {@code View} instance and/or async variants that
|
||||
* provide a String view name or a {@code View}.
|
||||
*
|
||||
* <p>This result handler should be ordered after others that may also interpret
|
||||
* a String return value for example in combination with {@code @ResponseBody}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ViewResolverResultHandler implements HandlerResultHandler, Ordered {
|
||||
|
||||
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private int order = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
public ViewResolverResultHandler(List<ViewResolver> resolvers, ConversionService service) {
|
||||
Assert.notEmpty(resolvers, "At least one ViewResolver is required.");
|
||||
Assert.notNull(service, "'conversionService' is required.");
|
||||
this.viewResolvers.addAll(resolvers);
|
||||
this.conversionService = service;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a read-only list of view resolvers.
|
||||
*/
|
||||
public List<ViewResolver> getViewResolvers() {
|
||||
return Collections.unmodifiableList(this.viewResolvers);
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
|
||||
// TODO: @ModelAttribute return value, declared Object return value (either String or View)
|
||||
|
||||
@Override
|
||||
public boolean supports(HandlerResult result) {
|
||||
Class<?> clazz = result.getReturnValueType().getRawClass();
|
||||
if (isViewNameOrViewReference(clazz)) {
|
||||
return true;
|
||||
}
|
||||
if (this.conversionService.canConvert(clazz, Mono.class)) {
|
||||
clazz = result.getReturnValueType().getGeneric(0).getRawClass();
|
||||
return isViewNameOrViewReference(clazz);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isViewNameOrViewReference(Class<?> clazz) {
|
||||
return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
|
||||
|
||||
Mono<?> returnValueMono;
|
||||
if (this.conversionService.canConvert(result.getReturnValueType().getRawClass(), Mono.class)) {
|
||||
returnValueMono = this.conversionService.convert(result.getReturnValue().get(), Mono.class);
|
||||
}
|
||||
else if (result.getReturnValue().isPresent()) {
|
||||
returnValueMono = Mono.just(result.getReturnValue().get());
|
||||
}
|
||||
else {
|
||||
Optional<String> viewName = getDefaultViewName(result, exchange);
|
||||
if (viewName.isPresent()) {
|
||||
returnValueMono = Mono.just(viewName.get());
|
||||
}
|
||||
else {
|
||||
returnValueMono = Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " +
|
||||
"neither returned a view name nor a View object"));
|
||||
}
|
||||
}
|
||||
|
||||
return returnValueMono.then(returnValue -> {
|
||||
if (returnValue instanceof View) {
|
||||
Flux<DataBuffer> body = ((View) returnValue).render(result, Optional.empty(), exchange);
|
||||
return exchange.getResponse().setBody(body);
|
||||
}
|
||||
else if (returnValue instanceof CharSequence) {
|
||||
String viewName = returnValue.toString();
|
||||
Locale locale = Locale.getDefault(); // TODO
|
||||
return Flux.fromIterable(getViewResolvers())
|
||||
.concatMap(resolver -> resolver.resolveViewName(viewName, locale))
|
||||
.next()
|
||||
.then(view -> {
|
||||
Flux<DataBuffer> body = view.render(result, Optional.empty(), exchange);
|
||||
return exchange.getResponse().setBody(body);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Should not happen
|
||||
return Mono.error(new IllegalStateException(
|
||||
"Unexpected return value: " + returnValue.getClass()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Optional<String> getDefaultViewName(HandlerResult result, ServerWebExchange exchange) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.buffer.DataBufferAllocator;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferAllocator;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.ViewResolver;
|
||||
|
||||
/**
|
||||
* Base class for {@code ViewResolver} implementations with shared properties.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.3
|
||||
*/
|
||||
public abstract class ViewResolverSupport implements ViewResolver, ApplicationContextAware, Ordered {
|
||||
|
||||
public static final MediaType DEFAULT_CONTENT_TYPE = MediaType.parseMediaType("text/html;charset=UTF-8");
|
||||
|
||||
|
||||
private List<MediaType> mediaTypes = new ArrayList<>(4);
|
||||
|
||||
private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private int order = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
public ViewResolverSupport() {
|
||||
this.mediaTypes.add(DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the supported media types for this view.
|
||||
* Default is "text/html;charset=UTF-8".
|
||||
*/
|
||||
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
|
||||
Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' is required.");
|
||||
this.mediaTypes.clear();
|
||||
if (supportedMediaTypes != null) {
|
||||
this.mediaTypes.addAll(supportedMediaTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured media types supported by this view.
|
||||
*/
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return this.mediaTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link DataBufferAllocator} to use for write I/O.
|
||||
* <p>By default this is set to {@link DefaultDataBufferAllocator}.
|
||||
* @param bufferAllocator the allocator to use
|
||||
*/
|
||||
public void setBufferAllocator(DataBufferAllocator bufferAllocator) {
|
||||
Assert.notNull(bufferAllocator, "'bufferAllocator' is required.");
|
||||
this.bufferAllocator = bufferAllocator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured buffer allocator, never {@code null}.
|
||||
*/
|
||||
public DataBufferAllocator getBufferAllocator() {
|
||||
return this.bufferAllocator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order in which this {@link ViewResolver}
|
||||
* is evaluated.
|
||||
*/
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the order in which this {@link ViewResolver} is evaluated.
|
||||
*/
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view.freemarker;
|
||||
|
||||
import freemarker.template.Configuration;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by objects that configure and manage a
|
||||
* FreeMarker Configuration object in a web environment. Detected and
|
||||
* used by {@link FreeMarkerView}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public interface FreeMarkerConfig {
|
||||
|
||||
/**
|
||||
* Return the FreeMarker Configuration object for the current
|
||||
* web application context.
|
||||
* <p>A FreeMarker Configuration object may be used to set FreeMarker
|
||||
* properties and shared objects, and allows to retrieve templates.
|
||||
* @return the FreeMarker Configuration
|
||||
*/
|
||||
Configuration getConfiguration();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view.freemarker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import freemarker.cache.ClassTemplateLoader;
|
||||
import freemarker.cache.TemplateLoader;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.TemplateException;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory;
|
||||
|
||||
/**
|
||||
* Configures FreeMarker for web usage via the "configLocation" and/or
|
||||
* "freemarkerSettings" and/or "templateLoaderPath" properties.
|
||||
* The simplest way to use this class is to specify just a "templateLoaderPath"
|
||||
* (e.g. "classpath:templates"); you do not need any further configuration then.
|
||||
*
|
||||
* <p>This bean must be included in the application context of any application
|
||||
* using {@link FreeMarkerView}. It exists purely to configure FreeMarker.
|
||||
* It is not meant to be referenced by application components but just internally
|
||||
* by {@code FreeMarkerView}. Implements {@link FreeMarkerConfig} to be found by
|
||||
* {@code FreeMarkerView} without depending on the bean name the configurer.
|
||||
*
|
||||
* <p>Note that you can also refer to a pre-configured FreeMarker Configuration
|
||||
* instance via the "configuration" property. This allows to share a FreeMarker
|
||||
* Configuration for web and email usage for example.
|
||||
*
|
||||
* <p>TODO: macros
|
||||
*
|
||||
* <p>This configurer registers a template loader for this package, allowing to
|
||||
* reference the "spring.ftl" macro library contained in this package:
|
||||
*
|
||||
* <pre class="code">
|
||||
* <#import "/spring.ftl" as spring/>
|
||||
* <@spring.bind "person.age"/>
|
||||
* age is ${spring.status.value}</pre>
|
||||
*
|
||||
* Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory
|
||||
implements FreeMarkerConfig, InitializingBean, ResourceLoaderAware {
|
||||
|
||||
private Configuration configuration;
|
||||
|
||||
|
||||
public FreeMarkerConfigurer() {
|
||||
setDefaultEncoding("UTF-8");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a pre-configured Configuration to use for the FreeMarker web config,
|
||||
* e.g. a shared one for web and email usage. If this is not set,
|
||||
* FreeMarkerConfigurationFactory's properties (inherited by this class)
|
||||
* have to be specified.
|
||||
*/
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize FreeMarkerConfigurationFactory's Configuration
|
||||
* if not overridden by a pre-configured FreeMarker Configuation.
|
||||
* <p>Sets up a ClassTemplateLoader to use for loading Spring macros.
|
||||
* @see #createConfiguration
|
||||
* @see #setConfiguration
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws IOException, TemplateException {
|
||||
if (this.configuration == null) {
|
||||
this.configuration = createConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation registers an additional ClassTemplateLoader
|
||||
* for the Spring-provided macros, added to the end of the list.
|
||||
*/
|
||||
@Override
|
||||
protected void postProcessTemplateLoaders(List<TemplateLoader> templateLoaders) {
|
||||
templateLoaders.add(new ClassTemplateLoader(FreeMarkerConfigurer.class, ""));
|
||||
logger.info("ClassTemplateLoader for Spring macros added to FreeMarker configuration");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the Configuration object wrapped by this bean.
|
||||
*/
|
||||
@Override
|
||||
public Configuration getConfiguration() {
|
||||
return this.configuration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view.freemarker;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import freemarker.core.ParseException;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.DefaultObjectWrapperBuilder;
|
||||
import freemarker.template.ObjectWrapper;
|
||||
import freemarker.template.SimpleHash;
|
||||
import freemarker.template.Template;
|
||||
import freemarker.template.Version;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.web.reactive.view.AbstractUrlBasedView;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@code View} implementation that uses the FreeMarker template engine.
|
||||
*
|
||||
* <p>Depends on a single {@link FreeMarkerConfig} object such as
|
||||
* {@link FreeMarkerConfigurer} being accessible in the application context.
|
||||
* Alternatively set the FreeMarker configuration can be set directly on this
|
||||
* class via {@link #setConfiguration}.
|
||||
*
|
||||
* <p>The {@link #setUrl(String) url} property is the location of the FreeMarker
|
||||
* template relative to the FreeMarkerConfigurer's
|
||||
* {@link FreeMarkerConfigurer#setTemplateLoaderPath templateLoaderPath}.
|
||||
*
|
||||
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class FreeMarkerView extends AbstractUrlBasedView {
|
||||
|
||||
private Configuration configuration;
|
||||
|
||||
private String encoding;
|
||||
|
||||
|
||||
/**
|
||||
* Set the FreeMarker Configuration to be used by this view.
|
||||
* <p>Typically this property is not set directly. Instead a single
|
||||
* {@link FreeMarkerConfig} is expected in the Spring application context
|
||||
* which is used to obtain the FreeMarker configuration.
|
||||
*/
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FreeMarker configuration used by this view.
|
||||
*/
|
||||
protected Configuration getConfiguration() {
|
||||
return this.configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the encoding of the FreeMarker template file.
|
||||
* <p>By default {@link FreeMarkerConfigurer} sets the default encoding in
|
||||
* the FreeMarker configuration to "UTF-8". It's recommended to specify the
|
||||
* encoding in the FreeMarker Configuration rather than per template if all
|
||||
* your templates share a common encoding.
|
||||
*/
|
||||
public void setEncoding(String encoding) {
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the encoding for the FreeMarker template.
|
||||
*/
|
||||
protected String getEncoding() {
|
||||
return this.encoding;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
super.afterPropertiesSet();
|
||||
if (getConfiguration() == null) {
|
||||
FreeMarkerConfig config = autodetectConfiguration();
|
||||
setConfiguration(config.getConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext.
|
||||
* @return the Configuration instance to use for FreeMarkerViews
|
||||
* @throws BeansException if no Configuration instance could be found
|
||||
* @see #setConfiguration
|
||||
*/
|
||||
protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
|
||||
try {
|
||||
return BeanFactoryUtils.beanOfTypeIncludingAncestors(
|
||||
getApplicationContext(), FreeMarkerConfig.class, true, false);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
throw new ApplicationContextException(
|
||||
"Must define a single FreeMarkerConfig bean in this web application context " +
|
||||
"(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
|
||||
"This bean may be given any name.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check that the FreeMarker template used for this view exists and is valid.
|
||||
* <p>Can be overridden to customize the behavior, for example in case of
|
||||
* multiple templates to be rendered into a single view.
|
||||
*/
|
||||
@Override
|
||||
public boolean checkResourceExists(Locale locale) throws Exception {
|
||||
try {
|
||||
// Check that we can get the template, even if we might subsequently get it again.
|
||||
getTemplate(locale);
|
||||
return true;
|
||||
}
|
||||
catch (FileNotFoundException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No FreeMarker view found for URL: " + getUrl());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (ParseException ex) {
|
||||
throw new ApplicationContextException(
|
||||
"Failed to parse FreeMarker template for URL [" + getUrl() + "]", ex);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ApplicationContextException(
|
||||
"Could not load FreeMarker template for URL [" + getUrl() + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Flux<DataBuffer> renderInternal(Map<String, Object> renderAttributes, ServerWebExchange exchange) {
|
||||
// Expose all standard FreeMarker hash models.
|
||||
SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Rendering FreeMarker template [" + getUrl() + "].");
|
||||
}
|
||||
Locale locale = Locale.getDefault(); // TODO
|
||||
DataBuffer dataBuffer = getBufferAllocator().allocateBuffer();
|
||||
try {
|
||||
Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream());
|
||||
getTemplate(locale).process(freeMarkerModel, writer);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
String message = "Could not load FreeMarker template for URL [" + getUrl() + "]";
|
||||
return Flux.error(new IllegalStateException(message, ex));
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
return Flux.error(ex);
|
||||
}
|
||||
return Flux.just(dataBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a FreeMarker template model for the given model Map.
|
||||
* <p>The default implementation builds a {@link SimpleHash}.
|
||||
* @param model the model to use for rendering
|
||||
* @param exchange current exchange
|
||||
* @return the FreeMarker template model, as a {@link SimpleHash} or subclass thereof
|
||||
*/
|
||||
protected SimpleHash getTemplateModel(Map<String, Object> model, ServerWebExchange exchange) {
|
||||
SimpleHash fmModel = new SimpleHash(getObjectWrapper());
|
||||
fmModel.putAll(model);
|
||||
return fmModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured FreeMarker {@link ObjectWrapper}, or the
|
||||
* {@link ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified.
|
||||
* @see freemarker.template.Configuration#getObjectWrapper()
|
||||
*/
|
||||
protected ObjectWrapper getObjectWrapper() {
|
||||
ObjectWrapper ow = getConfiguration().getObjectWrapper();
|
||||
Version version = Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS;
|
||||
return (ow != null ? ow : new DefaultObjectWrapperBuilder(version).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the FreeMarker template for the given locale,
|
||||
* to be rendering by this view.
|
||||
* <p>By default, the template specified by the "url" bean property
|
||||
* will be retrieved.
|
||||
* @param locale the current locale
|
||||
* @return the FreeMarker template to render
|
||||
*/
|
||||
protected Template getTemplate(Locale locale) throws IOException {
|
||||
return (getEncoding() != null ?
|
||||
getConfiguration().getTemplate(getUrl(), locale, getEncoding()) :
|
||||
getConfiguration().getTemplate(getUrl(), locale));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view.freemarker;
|
||||
|
||||
import org.springframework.web.reactive.view.UrlBasedViewResolver;
|
||||
|
||||
/**
|
||||
* A {@code ViewResolver} for resolving {@link FreeMarkerView} instances, i.e.
|
||||
* FreeMarker templates and custom subclasses of it.
|
||||
*
|
||||
* <p>The view class for all views generated by this resolver can be specified
|
||||
* via the "viewClass" property. See {@link UrlBasedViewResolver} for details.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/public class FreeMarkerViewResolver extends UrlBasedViewResolver {
|
||||
|
||||
|
||||
/**
|
||||
* Simple constructor.
|
||||
*/
|
||||
public FreeMarkerViewResolver() {
|
||||
setViewClass(requiredViewClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor with a prefix and suffix.
|
||||
* @param suffix the suffix to prepend view names with
|
||||
* @param prefix the prefix to prepend view names with
|
||||
*/
|
||||
public FreeMarkerViewResolver(String prefix, String suffix) {
|
||||
setViewClass(requiredViewClass());
|
||||
setPrefix(prefix);
|
||||
setSuffix(suffix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Requires {@link FreeMarkerView}.
|
||||
*/
|
||||
@Override
|
||||
protected Class<?> requiredViewClass() {
|
||||
return FreeMarkerView.class;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Support for result handling through view resolution.
|
||||
*/
|
||||
package org.springframework.web.reactive.view;
|
||||
@@ -28,7 +28,7 @@ import org.springframework.http.HttpStatus;
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class MockServerHttpResponse extends AbstractServerHttpResponse {
|
||||
public class MockServerHttpResponse implements ServerHttpResponse {
|
||||
|
||||
private HttpStatus status;
|
||||
|
||||
@@ -56,19 +56,11 @@ public class MockServerHttpResponse extends AbstractServerHttpResponse {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> setBodyInternal(Publisher<DataBuffer> body) {
|
||||
public Mono<Void> setBody(Publisher<DataBuffer> body) {
|
||||
this.body = body;
|
||||
return Flux.from(this.body).after();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHeaders() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeCookies() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter;
|
||||
import org.springframework.core.convert.support.ReactiveStreamsToReactorStreamConverter;
|
||||
import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
|
||||
@@ -46,28 +47,28 @@ public class SimpleHandlerResultHandlerTests {
|
||||
|
||||
HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue"));
|
||||
ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertFalse(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
// Reactor Stream is a Publisher
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertFalse(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertFalse(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,27 +83,31 @@ public class SimpleHandlerResultHandlerTests {
|
||||
|
||||
HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue"));
|
||||
ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertFalse(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertFalse(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(resultHandler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(resultHandler.supports(createHandlerResult(hm, type)));
|
||||
}
|
||||
|
||||
private HandlerResult createHandlerResult(HandlerMethod hm, ResolvableType type) {
|
||||
return new HandlerResult(hm, null, type, new ExtendedModelMap());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ import reactor.rx.Stream;
|
||||
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
@@ -54,12 +56,15 @@ public class InvocableHandlerMethodTests {
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private ModelMap model;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
WebSessionManager sessionManager = mock(WebSessionManager.class);
|
||||
this.request = mock(ServerHttpRequest.class);
|
||||
this.exchange = new DefaultServerWebExchange(request, mock(ServerHttpResponse.class), sessionManager);
|
||||
this.model = new ExtendedModelMap();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,11 +72,11 @@ public class InvocableHandlerMethodTests {
|
||||
public void noArgsMethod() throws Exception {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("noArgs");
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
List<HandlerResult> values = Stream.from(publisher).toList().get();
|
||||
|
||||
assertEquals(1, values.size());
|
||||
assertEquals("success", values.get(0).getResult());
|
||||
assertEquals("success", values.get(0).getReturnValue().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,11 +85,11 @@ public class InvocableHandlerMethodTests {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
|
||||
hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver()));
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
List<HandlerResult> values = Stream.from(publisher).toList().get();
|
||||
|
||||
assertEquals(1, values.size());
|
||||
assertEquals("success:null", values.get(0).getResult());
|
||||
assertEquals("success:null", values.get(0).getReturnValue().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,11 +97,11 @@ public class InvocableHandlerMethodTests {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
|
||||
addResolver(hm, Mono.just("value1"));
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
List<HandlerResult> values = Stream.from(publisher).toList().get();
|
||||
|
||||
assertEquals(1, values.size());
|
||||
assertEquals("success:value1", values.get(0).getResult());
|
||||
assertEquals("success:value1", values.get(0).getReturnValue().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -104,18 +109,18 @@ public class InvocableHandlerMethodTests {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
|
||||
addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3")));
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
List<HandlerResult> values = Stream.from(publisher).toList().get();
|
||||
|
||||
assertEquals(1, values.size());
|
||||
assertEquals("success:value1", values.get(0).getResult());
|
||||
assertEquals("success:value1", values.get(0).getReturnValue().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noResolverForArg() throws Exception {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
Throwable ex = awaitErrorSignal(publisher);
|
||||
|
||||
assertEquals(IllegalStateException.class, ex.getClass());
|
||||
@@ -127,12 +132,12 @@ public class InvocableHandlerMethodTests {
|
||||
public void resolveArgumentWithThrownException() throws Exception {
|
||||
HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class);
|
||||
when(resolver.supportsParameter(any())).thenReturn(true);
|
||||
when(resolver.resolveArgument(any(), any())).thenThrow(new IllegalStateException("boo"));
|
||||
when(resolver.resolveArgument(any(), any(), any())).thenThrow(new IllegalStateException("boo"));
|
||||
|
||||
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
|
||||
hm.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver));
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
Throwable ex = awaitErrorSignal(publisher);
|
||||
|
||||
assertEquals(IllegalStateException.class, ex.getClass());
|
||||
@@ -146,7 +151,7 @@ public class InvocableHandlerMethodTests {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
|
||||
addResolver(hm, Mono.error(new IllegalStateException("boo")));
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
Throwable ex = awaitErrorSignal(publisher);
|
||||
|
||||
assertEquals(IllegalStateException.class, ex.getClass());
|
||||
@@ -160,7 +165,7 @@ public class InvocableHandlerMethodTests {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class);
|
||||
addResolver(hm, Mono.just(1));
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
Throwable ex = awaitErrorSignal(publisher);
|
||||
|
||||
assertEquals(IllegalStateException.class, ex.getClass());
|
||||
@@ -173,7 +178,7 @@ public class InvocableHandlerMethodTests {
|
||||
public void invocationTargetExceptionIsUnwrapped() throws Exception {
|
||||
InvocableHandlerMethod hm = createHandlerMethod("exceptionMethod");
|
||||
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange);
|
||||
Publisher<HandlerResult> publisher = hm.invokeForRequest(this.exchange, this.model);
|
||||
Throwable ex = awaitErrorSignal(publisher);
|
||||
|
||||
assertEquals(IllegalStateException.class, ex.getClass());
|
||||
@@ -190,7 +195,7 @@ public class InvocableHandlerMethodTests {
|
||||
private void addResolver(InvocableHandlerMethod handlerMethod, Publisher<Object> resolvedValue) {
|
||||
HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class);
|
||||
when(resolver.supportsParameter(any())).thenReturn(true);
|
||||
when(resolver.resolveArgument(any(), any())).thenReturn(Mono.from(resolvedValue));
|
||||
when(resolver.resolveArgument(any(), any(), any())).thenReturn(Mono.from(resolvedValue));
|
||||
handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -38,6 +39,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.support.ByteBufferEncoder;
|
||||
import org.springframework.core.codec.support.JacksonJsonEncoder;
|
||||
import org.springframework.core.codec.support.JsonObjectEncoder;
|
||||
@@ -56,6 +58,8 @@ import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -63,7 +67,11 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.reactive.ViewResolver;
|
||||
import org.springframework.web.reactive.handler.SimpleHandlerResultHandler;
|
||||
import org.springframework.web.reactive.view.ViewResolverResultHandler;
|
||||
import org.springframework.web.reactive.view.freemarker.FreeMarkerConfigurer;
|
||||
import org.springframework.web.reactive.view.freemarker.FreeMarkerViewResolver;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
@@ -291,6 +299,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
create("http://localhost:" + this.port + "/observable-create");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void html() throws Exception {
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
URI url = new URI("http://localhost:" + port + "/html?name=Jason");
|
||||
RequestEntity<Void> request = RequestEntity.get(url).accept(MediaType.TEXT_HTML).build();
|
||||
ResponseEntity<String> response = restTemplate.exchange(request, String.class);
|
||||
|
||||
assertEquals("<html><body>Hello: Jason!</body></html>", response.getBody());
|
||||
}
|
||||
|
||||
|
||||
private void serializeAsPojo(String requestUrl) throws Exception {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
@@ -350,7 +370,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
ResponseEntity<Void> response = restTemplate.exchange(request, Void.class);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals(2, this.wac.getBean(TestController.class).persons.size());
|
||||
assertEquals(2, this.wac.getBean(TestRestController.class).persons.size());
|
||||
}
|
||||
|
||||
|
||||
@@ -358,6 +378,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
@SuppressWarnings("unused")
|
||||
static class FrameworkConfig {
|
||||
|
||||
private DataBufferAllocator allocator = new DefaultDataBufferAllocator();
|
||||
|
||||
|
||||
@Bean
|
||||
public RequestMappingHandlerMapping handlerMapping() {
|
||||
return new RequestMappingHandlerMapping();
|
||||
@@ -382,16 +405,42 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
|
||||
@Bean
|
||||
public ResponseBodyResultHandler responseBodyResultHandler() {
|
||||
DataBufferAllocator allocator = new DefaultDataBufferAllocator();
|
||||
return new ResponseBodyResultHandler(Arrays.asList(
|
||||
new ByteBufferEncoder(allocator), new StringEncoder(allocator),
|
||||
new JacksonJsonEncoder(allocator, new JsonObjectEncoder(allocator))),
|
||||
conversionService());
|
||||
List<Encoder<?>> encoders = Arrays.asList(
|
||||
new ByteBufferEncoder(this.allocator), new StringEncoder(this.allocator),
|
||||
new JacksonJsonEncoder(this.allocator, new JsonObjectEncoder(this.allocator)));
|
||||
ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(encoders, conversionService());
|
||||
resultHandler.setOrder(1);
|
||||
return resultHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SimpleHandlerResultHandler simpleHandlerResultHandler() {
|
||||
return new SimpleHandlerResultHandler(conversionService());
|
||||
SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService());
|
||||
resultHandler.setOrder(2);
|
||||
return resultHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ViewResolverResultHandler viewResolverResultHandler() {
|
||||
List<ViewResolver> resolvers = Collections.singletonList(freeMarkerViewResolver());
|
||||
ViewResolverResultHandler resultHandler = new ViewResolverResultHandler(resolvers, conversionService());
|
||||
resultHandler.setOrder(3);
|
||||
return resultHandler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ViewResolver freeMarkerViewResolver() {
|
||||
FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver("", ".ftl");
|
||||
viewResolver.setBufferAllocator(this.allocator);
|
||||
return viewResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfig() {
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setPreferFileSystemAccess(false);
|
||||
configurer.setTemplateLoaderPath("classpath*:org/springframework/web/reactive/view/freemarker/");
|
||||
return configurer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -400,6 +449,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
@SuppressWarnings("unused")
|
||||
static class ApplicationConfig {
|
||||
|
||||
@Bean
|
||||
public TestRestController testRestController() {
|
||||
return new TestRestController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestController testController() {
|
||||
return new TestController();
|
||||
@@ -409,7 +463,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
|
||||
@RestController
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
private static class TestRestController {
|
||||
|
||||
final List<Person> persons = new ArrayList<>();
|
||||
|
||||
@@ -601,6 +655,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
||||
@RequestMapping("/html")
|
||||
public String getHtmlPage(@RequestParam String name, Model model) {
|
||||
model.addAttribute("hello", "Hello: " + name + "!");
|
||||
return "test";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Person {
|
||||
|
||||
private String name;
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.support.StringEncoder;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferAllocator;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
@@ -47,15 +48,15 @@ public class ResponseBodyResultHandlerTests {
|
||||
|
||||
HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated"));
|
||||
ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertFalse(handler.supports(new HandlerResult(hm, null, type)));
|
||||
assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap())));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(handler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap())));
|
||||
|
||||
hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid"));
|
||||
type = ResolvableType.forMethodParameter(hm.getReturnType());
|
||||
assertTrue(handler.supports(new HandlerResult(hm, null, type)));
|
||||
assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.test.TestSubscriber;
|
||||
import rx.Single;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferAllocator;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.HandlerResultHandler;
|
||||
import org.springframework.web.reactive.View;
|
||||
import org.springframework.web.reactive.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.endsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ViewResolverResultHandler}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ViewResolverResultHandlerTests {
|
||||
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
|
||||
private MockServerHttpResponse response;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private ModelMap model;
|
||||
|
||||
private DefaultConversionService conversionService;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
|
||||
this.response = new MockServerHttpResponse();
|
||||
WebSessionManager sessionManager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager);
|
||||
this.model = new ExtendedModelMap().addAttribute("id", "123");
|
||||
this.conversionService = new DefaultConversionService();
|
||||
this.conversionService.addConverter(new ReactiveStreamsToRxJava1Converter());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void supportsWithNullReturnValue() throws Exception {
|
||||
testSupports("handleString", null);
|
||||
testSupports("handleView", null);
|
||||
testSupports("handleMonoString", null);
|
||||
testSupports("handleMonoView", null);
|
||||
testSupports("handleSingleString", null);
|
||||
testSupports("handleSingleView", null);
|
||||
}
|
||||
|
||||
private void testSupports(String methodName, Object returnValue) throws NoSuchMethodException {
|
||||
Method method = TestController.class.getMethod(methodName);
|
||||
ResolvableType returnType = ResolvableType.forMethodParameter(method, -1);
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.model);
|
||||
List<ViewResolver> resolvers = Collections.singletonList(mock(ViewResolver.class));
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
assertTrue(handler.supports(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewReference() throws Exception {
|
||||
TestView view = new TestView("account");
|
||||
List<ViewResolver> resolvers = Collections.singletonList(mock(ViewResolver.class));
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
handle(handler, view, ResolvableType.forClass(View.class));
|
||||
|
||||
new TestSubscriber<DataBuffer>().bindTo(this.response.getBody())
|
||||
.assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewReferenceMono() throws Exception {
|
||||
TestView view = new TestView("account");
|
||||
List<ViewResolver> resolvers = Collections.singletonList(mock(ViewResolver.class));
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
handle(handler, Mono.just(view), ResolvableType.forClass(Mono.class));
|
||||
|
||||
new TestSubscriber<DataBuffer>().bindTo(this.response.getBody())
|
||||
.assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewName() throws Exception {
|
||||
TestView view = new TestView("account");
|
||||
TestViewResolver resolver = new TestViewResolver().addView(view);
|
||||
List<ViewResolver> resolvers = Collections.singletonList(resolver);
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
handle(handler, "account", ResolvableType.forClass(String.class));
|
||||
|
||||
TestSubscriber<DataBuffer> subscriber = new TestSubscriber<>();
|
||||
subscriber.bindTo(this.response.getBody())
|
||||
.assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewNameMono() throws Exception {
|
||||
TestView view = new TestView("account");
|
||||
TestViewResolver resolver = new TestViewResolver().addView(view);
|
||||
List<ViewResolver> resolvers = Collections.singletonList(resolver);
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
handle(handler, Mono.just("account"), ResolvableType.forClass(Mono.class));
|
||||
|
||||
new TestSubscriber<DataBuffer>().bindTo(this.response.getBody())
|
||||
.assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewNameWithMultipleResolvers() throws Exception {
|
||||
TestView view1 = new TestView("account");
|
||||
TestView view2 = new TestView("profile");
|
||||
TestViewResolver resolver1 = new TestViewResolver().addView(view1);
|
||||
TestViewResolver resolver2 = new TestViewResolver().addView(view2);
|
||||
List<ViewResolver> resolvers = Arrays.asList(resolver1, resolver2);
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
handle(handler, "profile", ResolvableType.forClass(String.class));
|
||||
|
||||
new TestSubscriber<DataBuffer>().bindTo(this.response.getBody())
|
||||
.assertValuesWith(buf -> assertEquals("profile: {id=123}", asString(buf)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewNameWithNoMatch() throws Exception {
|
||||
List<ViewResolver> resolvers = Collections.singletonList(mock(ViewResolver.class));
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
TestSubscriber<Void> subscriber = handle(handler, "account", ResolvableType.forClass(String.class));
|
||||
|
||||
subscriber.assertNoValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewNameNotSpecified() throws Exception {
|
||||
List<ViewResolver> resolvers = Collections.singletonList(mock(ViewResolver.class));
|
||||
ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService);
|
||||
TestSubscriber<Void> subscriber = handle(handler, null, ResolvableType.forClass(String.class));
|
||||
|
||||
subscriber.assertErrorWith(ex ->
|
||||
assertThat(ex.getMessage(), endsWith("neither returned a view name nor a View object")));
|
||||
}
|
||||
|
||||
private TestSubscriber<Void> handle(HandlerResultHandler handler, Object value, ResolvableType type) {
|
||||
HandlerResult result = new HandlerResult(new Object(), value, type, this.model);
|
||||
Mono<Void> mono = handler.handleResult(this.exchange, result);
|
||||
TestSubscriber<Void> subscriber = new TestSubscriber<>();
|
||||
return subscriber.bindTo(mono).await(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static DataBuffer asDataBuffer(String value) {
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8));
|
||||
return new DefaultDataBufferAllocator().wrap(byteBuffer);
|
||||
}
|
||||
|
||||
private static String asString(DataBuffer dataBuffer) {
|
||||
ByteBuffer byteBuffer = dataBuffer.asByteBuffer();
|
||||
final byte[] bytes = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(bytes);
|
||||
return new String(bytes, UTF_8);
|
||||
}
|
||||
|
||||
|
||||
private static class TestViewResolver implements ViewResolver {
|
||||
|
||||
private final Map<String, View> views = new HashMap<>();
|
||||
|
||||
|
||||
public TestViewResolver addView(TestView view) {
|
||||
this.views.put(view.getName(), view);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<View> resolveViewName(String viewName, Locale locale) {
|
||||
View view = this.views.get(viewName);
|
||||
return (view != null ? Mono.just(view) : Mono.empty());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TestView implements View {
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
public TestView(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> render(HandlerResult result, Optional<MediaType> contentType,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
String value = this.name + ": " + result.getModel().toString();
|
||||
assertNotNull(value);
|
||||
return Flux.just(asDataBuffer(value));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
||||
public String handleString() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Mono<String> handleMonoString() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Single<String> handleSingleString() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public View handleView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Mono<View> handleMonoView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Single<View> handleSingleView() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.view.freemarker;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import freemarker.template.Configuration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.test.TestSubscriber;
|
||||
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class FreeMarkerViewTests {
|
||||
|
||||
public static final String TEMPLATE_PATH = "classpath*:org/springframework/web/reactive/view/freemarker/";
|
||||
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private GenericApplicationContext context;
|
||||
|
||||
private Configuration freeMarkerConfig;
|
||||
|
||||
@Rule
|
||||
public final ExpectedException exception = ExpectedException.none();
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.context = new GenericApplicationContext();
|
||||
this.context.refresh();
|
||||
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setPreferFileSystemAccess(false);
|
||||
configurer.setTemplateLoaderPath(TEMPLATE_PATH);
|
||||
configurer.setResourceLoader(this.context);
|
||||
this.freeMarkerConfig = configurer.createConfiguration();
|
||||
|
||||
FreeMarkerView fv = new FreeMarkerView();
|
||||
fv.setApplicationContext(this.context);
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void noFreeMarkerConfig() throws Exception {
|
||||
this.exception.expect(ApplicationContextException.class);
|
||||
this.exception.expectMessage("Must define a single FreeMarkerConfig bean");
|
||||
|
||||
FreeMarkerView view = new FreeMarkerView();
|
||||
view.setApplicationContext(this.context);
|
||||
view.setUrl("anythingButNull");
|
||||
view.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noTemplateName() throws Exception {
|
||||
this.exception.expect(IllegalArgumentException.class);
|
||||
this.exception.expectMessage("Property 'url' is required");
|
||||
|
||||
FreeMarkerView freeMarkerView = new FreeMarkerView();
|
||||
freeMarkerView.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkResourceExists() throws Exception {
|
||||
FreeMarkerView view = new FreeMarkerView();
|
||||
view.setConfiguration(this.freeMarkerConfig);
|
||||
view.setUrl("test.ftl");
|
||||
|
||||
assertTrue(view.checkResourceExists(Locale.US));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void render() throws Exception {
|
||||
FreeMarkerView view = new FreeMarkerView();
|
||||
view.setConfiguration(this.freeMarkerConfig);
|
||||
view.setUrl("test.ftl");
|
||||
|
||||
ModelMap model = new ExtendedModelMap();
|
||||
model.addAttribute("hello", "hi FreeMarker");
|
||||
HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model);
|
||||
Flux<DataBuffer> flux = view.render(result, Optional.empty(), this.exchange);
|
||||
|
||||
TestSubscriber<DataBuffer> subscriber = new TestSubscriber<>();
|
||||
subscriber.bindTo(flux).assertValuesWith(dataBuffer ->
|
||||
assertEquals("<html><body>hi FreeMarker</body></html>", asString(dataBuffer)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static String asString(DataBuffer dataBuffer) {
|
||||
ByteBuffer byteBuffer = dataBuffer.asByteBuffer();
|
||||
final byte[] bytes = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(bytes);
|
||||
return new String(bytes, UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<html><body>${hello}</body></html>
|
||||
Reference in New Issue
Block a user