Browse Source

Merge templating branch

pull/1111/head
Rossen Stoyanchev 10 years ago
parent
commit
3ca1d33905
  1. 2
      spring-web-reactive/build.gradle
  2. 2
      spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java
  3. 72
      spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java
  4. 59
      spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java
  5. 30
      spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java
  6. 5
      spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java
  7. 14
      spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java
  8. 6
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java
  9. 14
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java
  10. 44
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java
  11. 5
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java
  12. 12
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java
  13. 3
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java
  14. 13
      spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java
  15. 85
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java
  16. 166
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java
  17. 168
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java
  18. 151
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java
  19. 116
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java
  20. 39
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java
  21. 116
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java
  22. 219
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java
  23. 58
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java
  24. 4
      spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java
  25. 12
      spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java
  26. 29
      spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java
  27. 35
      spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java
  28. 82
      spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java
  29. 7
      spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java
  30. 289
      spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java
  31. 150
      spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java
  32. 1
      spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl

2
spring-web-reactive/build.gradle

@ -84,6 +84,7 @@ dependencies { @@ -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 { @@ -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"

2
spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java

@ -146,7 +146,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { @@ -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());
}

72
spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java

@ -16,11 +16,14 @@ @@ -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 { @@ -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;
/**
* Return the value returned from the handler wrapped as {@link Optional}.
*/
public Optional<Object> getReturnValue() {
return this.returnValue;
}
public ResolvableType getResultType() {
return this.resultType;
/**
* Return the type of the value returned from the handler.
*/
public ResolvableType getReturnValueType() {
return this.returnValueType;
}
/**
* 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 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 { @@ -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));
}
}

59
spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java

@ -0,0 +1,59 @@ @@ -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);
}

30
spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java

@ -0,0 +1,30 @@ @@ -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);
}

5
spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java

@ -20,6 +20,8 @@ import org.reactivestreams.Publisher; @@ -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 { @@ -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));
}
}

14
spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java

@ -16,6 +16,8 @@ @@ -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 @@ -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 @@ -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));
}
}

6
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java

@ -19,6 +19,7 @@ package org.springframework.web.reactive.method; @@ -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 { @@ -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);
}

14
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java

@ -32,6 +32,7 @@ import org.springframework.core.GenericTypeResolver; @@ -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 { @@ -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 { @@ -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 { @@ -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");

44
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java

@ -0,0 +1,44 @@ @@ -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);
}
}

5
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java

@ -28,6 +28,7 @@ import org.springframework.core.codec.Decoder; @@ -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 @@ -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;

12
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java

@ -38,6 +38,8 @@ import org.springframework.core.convert.ConversionService; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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()) {

3
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java

@ -19,6 +19,7 @@ package org.springframework.web.reactive.method.annotation; @@ -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 @@ -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();

13
spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java

@ -23,6 +23,7 @@ import java.util.HashMap; @@ -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 @@ -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 @@ -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;
}

85
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java

@ -0,0 +1,85 @@ @@ -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() + "]";
}
}

166
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java

@ -0,0 +1,166 @@ @@ -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();
}
}

168
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java

@ -0,0 +1,168 @@ @@ -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);
}
}

151
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java

@ -0,0 +1,151 @@ @@ -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();
}
}

116
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java

@ -0,0 +1,116 @@ @@ -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;
}
}

39
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java

@ -0,0 +1,39 @@ @@ -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();
}

116
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java

@ -0,0 +1,116 @@ @@ -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">
* &lt;#import "/spring.ftl" as spring/&gt;
* &lt;@spring.bind "person.age"/&gt;
* 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;
}
}

219
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java

@ -0,0 +1,219 @@ @@ -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));
}
}

58
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java

@ -0,0 +1,58 @@ @@ -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;
}
}

4
spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
/**
* Support for result handling through view resolution.
*/
package org.springframework.web.reactive.view;

12
spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java

@ -28,7 +28,7 @@ import org.springframework.http.HttpStatus; @@ -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 { @@ -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) {
}

29
spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java

@ -30,6 +30,7 @@ import org.springframework.core.convert.support.GenericConversionService; @@ -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 { @@ -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 { @@ -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());
}

35
spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java

@ -31,6 +31,8 @@ import reactor.rx.Stream; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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));
}

82
spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java

@ -20,6 +20,7 @@ import java.net.URI; @@ -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; @@ -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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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;

7
spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java

@ -25,6 +25,7 @@ import org.springframework.core.ResolvableType; @@ -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 { @@ -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())));
}

289
spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java

@ -0,0 +1,289 @@ @@ -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;
}
}
}

150
spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java

@ -0,0 +1,150 @@ @@ -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);
}
}

1
spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl

@ -0,0 +1 @@ @@ -0,0 +1 @@
<html><body>${hello}</body></html>
Loading…
Cancel
Save