/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.result.view;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.time.Duration;
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.TreeMap;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import rx.Completable;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.ui.ConcurrentModel;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.get;
import static org.springframework.web.method.ResolvableMethod.on;
/**
* ViewResolutionResultHandler relying on a canned {@link TestViewResolver}
* or a (Mockito) "mock".
*
* @author Rossen Stoyanchev
*/
public class ViewResolutionResultHandlerTests {
private final BindingContext bindingContext = new BindingContext();
@Test
public void supports() throws Exception {
testSupports(on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(String.class));
testSupports(on(Handler.class).annotNotPresent(ModelAttribute.class).resolveReturnType(String.class));
testSupports(on(Handler.class).resolveReturnType(Mono.class, String.class));
testSupports(on(Handler.class).resolveReturnType(Rendering.class));
testSupports(on(Handler.class).resolveReturnType(Mono.class, Rendering.class));
testSupports(on(Handler.class).resolveReturnType(View.class));
testSupports(on(Handler.class).resolveReturnType(Mono.class, View.class));
testSupports(on(Handler.class).resolveReturnType(void.class));
testSupports(on(Handler.class).resolveReturnType(Mono.class, Void.class));
testSupports(on(Handler.class).resolveReturnType(Completable.class));
testSupports(on(Handler.class).resolveReturnType(Model.class));
testSupports(on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(Map.class));
testSupports(on(Handler.class).annotNotPresent(ModelAttribute.class).resolveReturnType(Map.class));
testSupports(on(Handler.class).resolveReturnType(TestBean.class));
testSupports(on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(Long.class));
testDoesNotSupport(on(Handler.class).annotNotPresent(ModelAttribute.class).resolveReturnType(Long.class));
// SPR-15464
testSupports(on(Handler.class).resolveReturnType(Mono.class));
}
private void testSupports(MethodParameter returnType) {
testSupports(returnType, true);
}
private void testDoesNotSupport(MethodParameter returnType) {
testSupports(returnType, false);
}
private void testSupports(MethodParameter returnType, boolean supports) {
ViewResolutionResultHandler resultHandler = resultHandler(mock(ViewResolver.class));
HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.bindingContext);
assertEquals(supports, resultHandler.supports(handlerResult));
}
@Test
public void viewResolverOrder() throws Exception {
TestViewResolver resolver1 = new TestViewResolver("account");
TestViewResolver resolver2 = new TestViewResolver("profile");
resolver1.setOrder(2);
resolver2.setOrder(1);
List<ViewResolver> resolvers = resultHandler(resolver1, resolver2).getViewResolvers();
assertEquals(Arrays.asList(resolver2, resolver1), resolvers);
}
@Test
public void handleReturnValueTypes() throws Exception {
Object returnValue;
MethodParameter returnType;
ViewResolver resolver = new TestViewResolver("account");
returnType = on(Handler.class).resolveReturnType(View.class);
returnValue = new TestView("account");
testHandle("/path", returnType, returnValue, "account: {id=123}");
returnType = on(Handler.class).resolveReturnType(Mono.class, View.class);
returnValue = Mono.just(new TestView("account"));
testHandle("/path", returnType, returnValue, "account: {id=123}");
returnType = on(Handler.class).annotNotPresent(ModelAttribute.class).resolveReturnType(String.class);
returnValue = "account";
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
returnType = on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(String.class);
returnValue = "123";
testHandle("/account", returnType, returnValue, "account: {id=123, myString=123}", resolver);
returnType = on(Handler.class).resolveReturnType(Mono.class, String.class);
returnValue = Mono.just("account");
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
returnType = on(Handler.class).resolveReturnType(Model.class);
returnValue = new ConcurrentModel().addAttribute("name", "Joe");
testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver);
// Work around caching issue...
ResolvableType.clearCache();
returnType = on(Handler.class).annotNotPresent(ModelAttribute.class).resolveReturnType(Map.class);
returnValue = Collections.singletonMap("name", "Joe");
testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver);
// Work around caching issue...
ResolvableType.clearCache();
returnType = on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(Map.class);
returnValue = Collections.singletonMap("name", "Joe");
testHandle("/account", returnType, returnValue, "account: {id=123, myMap={name=Joe}}", resolver);
returnType = on(Handler.class).resolveReturnType(TestBean.class);
returnValue = new TestBean("Joe");
String responseBody = "account: {id=123, " +
"org.springframework.validation.BindingResult.testBean=" +
"org.springframework.validation.BeanPropertyBindingResult: 0 errors, " +
"testBean=TestBean[name=Joe]}";
testHandle("/account", returnType, returnValue, responseBody, resolver);
returnType = on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(Long.class);
testHandle("/account", returnType, 99L, "account: {id=123, myLong=99}", resolver);
returnType = on(Handler.class).resolveReturnType(Rendering.class);
HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY;
returnValue = Rendering.view("account").modelAttribute("a", "a1").status(status).header("h", "h1").build();
String expected = "account: {a=a1, id=123}";
ServerWebExchange exchange = testHandle("/path", returnType, returnValue, expected, resolver);
assertEquals(status, exchange.getResponse().getStatusCode());
assertEquals("h1", exchange.getResponse().getHeaders().getFirst("h"));
}
@Test
public void handleWithMultipleResolvers() throws Exception {
Object returnValue = "profile";
MethodParameter returnType = on(Handler.class).annotNotPresent(ModelAttribute.class).resolveReturnType(String.class);
ViewResolver[] resolvers = {new TestViewResolver("account"), new TestViewResolver("profile")};
testHandle("/account", returnType, returnValue, "profile: {id=123}", resolvers);
}
@Test
public void defaultViewName() throws Exception {
testDefaultViewName(null, on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(String.class));
testDefaultViewName(Mono.empty(), on(Handler.class).resolveReturnType(Mono.class, String.class));
testDefaultViewName(Mono.empty(), on(Handler.class).resolveReturnType(Mono.class, Void.class));
testDefaultViewName(Completable.complete(), on(Handler.class).resolveReturnType(Completable.class));
}
private void testDefaultViewName(Object returnValue, MethodParameter returnType) throws URISyntaxException {
this.bindingContext.getModel().addAttribute("id", "123");
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
MockServerWebExchange exchange = get("/account").toExchange();
handler.handleResult(exchange, result).block(Duration.ofMillis(5000));
assertResponseBody(exchange, "account: {id=123}");
exchange = get("/account/").toExchange();
handler.handleResult(exchange, result).block(Duration.ofMillis(5000));
assertResponseBody(exchange, "account: {id=123}");
exchange = get("/account.123").toExchange();
handler.handleResult(exchange, result).block(Duration.ofMillis(5000));
assertResponseBody(exchange, "account: {id=123}");
}
@Test
public void unresolvedViewName() throws Exception {
String returnValue = "account";
MethodParameter returnType = on(Handler.class).annotPresent(ModelAttribute.class).resolveReturnType(String.class);
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
MockServerWebExchange exchange = get("/path").toExchange();
Mono<Void> mono = resultHandler().handleResult(exchange, result);
StepVerifier.create(mono)
.expectNextCount(0)
.expectErrorMessage("Could not resolve view with name 'account'.")
.verify();
}
@Test
public void contentNegotiation() throws Exception {
TestBean value = new TestBean("Joe");
MethodParameter returnType = on(Handler.class).resolveReturnType(TestBean.class);
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext);
MockServerWebExchange exchange = get("/account").accept(APPLICATION_JSON).toExchange();
TestView defaultView = new TestView("jsonView", APPLICATION_JSON);
resultHandler(Collections.singletonList(defaultView), new TestViewResolver("account"))
.handleResult(exchange, handlerResult)
.block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON, exchange.getResponse().getHeaders().getContentType());
assertResponseBody(exchange, "jsonView: {" +
"org.springframework.validation.BindingResult.testBean=" +
"org.springframework.validation.BeanPropertyBindingResult: 0 errors, " +
"testBean=TestBean[name=Joe]" +
"}");
}
@Test
public void contentNegotiationWith406() throws Exception {
TestBean value = new TestBean("Joe");
MethodParameter returnType = on(Handler.class).resolveReturnType(TestBean.class);
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext);
MockServerWebExchange exchange = get("/account").accept(APPLICATION_JSON).toExchange();
ViewResolutionResultHandler resultHandler = resultHandler(new TestViewResolver("account"));
Mono<Void> mono = resultHandler.handleResult(exchange, handlerResult);
StepVerifier.create(mono)
.expectNextCount(0)
.expectError(NotAcceptableStatusException.class)
.verify();
}
private ViewResolutionResultHandler resultHandler(ViewResolver... resolvers) {
return resultHandler(Collections.emptyList(), resolvers);
}
private ViewResolutionResultHandler resultHandler(List<View> defaultViews, ViewResolver... resolvers) {
List<ViewResolver> resolverList = Arrays.asList(resolvers);
RequestedContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, contentTypeResolver);
handler.setDefaultViews(defaultViews);
return handler;
}
private ServerWebExchange testHandle(String path, MethodParameter returnType, Object returnValue,
String responseBody, ViewResolver... resolvers) throws URISyntaxException {
Model model = this.bindingContext.getModel();
model.asMap().clear();
model.addAttribute("id", "123");
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
MockServerWebExchange exchange = get(path).toExchange();
resultHandler(resolvers).handleResult(exchange, result).block(Duration.ofSeconds(5));
assertResponseBody(exchange, responseBody);
return exchange;
}
private void assertResponseBody(MockServerWebExchange exchange, String responseBody) {
StepVerifier.create(exchange.getResponse().getBody())
.consumeNextWith(buf -> assertEquals(responseBody, DataBufferTestUtils.dumpString(buf, UTF_8)))
.expectComplete()
.verify();
}
private static class TestViewResolver implements ViewResolver, Ordered {
private final Map<String, View> views = new HashMap<>();
private int order = Ordered.LOWEST_PRECEDENCE;
TestViewResolver(String... viewNames) {
Arrays.stream(viewNames).forEach(name -> this.views.put(name, new TestView(name)));
}
void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public Mono<View> resolveViewName(String viewName, Locale locale) {
View view = this.views.get(viewName);
return Mono.justOrEmpty(view);
}
}
private static final class TestView implements View {
private final String name;
private final List<MediaType> mediaTypes;
TestView(String name) {
this.name = name;
this.mediaTypes = Collections.singletonList(MediaType.TEXT_HTML);
}
TestView(String name, MediaType... mediaTypes) {
this.name = name;
this.mediaTypes = Arrays.asList(mediaTypes);
}
@SuppressWarnings("unused")
public String getName() {
return this.name;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return this.mediaTypes;
}
@Override
public Mono<Void> render(Map<String, ?> model, MediaType mediaType, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
if (mediaType != null) {
response.getHeaders().setContentType(mediaType);
}
model = new TreeMap<>(model);
String value = this.name + ": " + model.toString();
ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8));
DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(byteBuffer);
return response.writeWith(Flux.just(dataBuffer));
}
}
private static class TestBean {
private final String name;
TestBean(String name) {
this.name = name;
}
@SuppressWarnings("unused")
public String getName() {
return this.name;
}
@Override
public String toString() {
return "TestBean[name=" + this.name + "]";
}
}
@SuppressWarnings("unused")
private static class Handler {
String string() { return null; }
Mono<String> monoString() { return null; }
@ModelAttribute("myString") String stringWithAnnotation() { return null; }
Rendering rendering() { return null; }
Mono<Rendering> monoRendering() { return null; }
View view() { return null; }
Mono<View> monoView() { return null; }
void voidMethod() { }
Mono<Void> monoVoid() { return null; }
Completable completable() { return null; }
Model model() { return null; }
Map<?,?> map() { return null; }
@ModelAttribute("myMap") Map<?,?> mapWithAnnotation() { return null; }
TestBean testBean() { return null; }
Long longValue() { return null; }
@ModelAttribute("myLong") Long longModelAttribute() { return null; }
Mono<?> monoWildcard() { return null; }
}
}