From 35b93b2948f88c8af1721deaac9fadfd2a60d5c9 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 6 Sep 2016 13:51:27 +0200 Subject: [PATCH] Add template rendering support This commit introduces template rendering support in the web.reactive package, through a Response.render method and a Rendering interface. --- .../function/DefaultConfiguration.java | 8 + .../function/DefaultResponseBuilder.java | 28 +++ .../web/reactive/function/Rendering.java | 40 ++++ .../reactive/function/RenderingResponse.java | 88 ++++++++ .../web/reactive/function/Response.java | 25 +++ .../web/reactive/function/Router.java | 21 ++ .../reactive/result/view/ModelAndView.java | 202 ++++++++++++++++++ ....java => DefaultResponseBuilderTests.java} | 12 +- .../DispatcherHandlerIntegrationTests.java | 7 + .../function/RenderingResponseTests.java | 77 +++++++ .../web/reactive/function/RouterTests.java | 12 +- .../result/view/ModelAndViewTests.java | 83 +++++++ 12 files changed, 594 insertions(+), 9 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Rendering.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RenderingResponse.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ModelAndView.java rename spring-web-reactive/src/test/java/org/springframework/web/reactive/function/{ResponseTests.java => DefaultResponseBuilderTests.java} (91%) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RenderingResponseTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ModelAndViewTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultConfiguration.java index 42ebabeb04..3eca98b338 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultConfiguration.java @@ -34,6 +34,7 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.ClassUtils; +import org.springframework.web.reactive.result.view.ViewResolver; /** * A default implementation of configuration. @@ -54,6 +55,8 @@ class DefaultConfiguration implements Router.Configuration { private final List> messageWriters = new ArrayList<>(); + private final List viewResolvers = new ArrayList<>(); + public DefaultConfiguration() { this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); this.messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder())); @@ -78,4 +81,9 @@ class DefaultConfiguration implements Router.Configuration { public Supplier>> messageWriters() { return this.messageWriters::stream; } + + @Override + public Supplier> viewResolvers() { + return this.viewResolvers::stream; + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java index 7da5a0f799..d8d95f0da2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/DefaultResponseBuilder.java @@ -21,10 +21,15 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.Map; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; +import org.springframework.core.Conventions; import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; @@ -169,4 +174,27 @@ class DefaultResponseBuilder implements Response.BodyBuilder { return ServerSentEventResponse .fromPublisher(this.statusCode, this.headers, eventsPublisher, eventClass); } + + @Override + public Response render(String name, Object... modelAttributes) { + Map modelMap = Arrays.stream(modelAttributes) + .filter(o -> !isEmptyCollection(o)) + .collect(Collectors.toMap(Conventions::getVariableName, o -> o)); + return new RenderingResponse(this.statusCode, this.headers, name, modelMap); + } + + @Override + public Response render(String name, Map model) { + Assert.hasLength(name, "'name' must not be empty"); + Map modelMap = new LinkedHashMap<>(); + if (model != null) { + modelMap.putAll(model); + } + return new RenderingResponse(this.statusCode, this.headers, name, modelMap); + } + + private static boolean isEmptyCollection(Object o) { + return o instanceof Collection && ((Collection) o).isEmpty(); + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Rendering.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Rendering.java new file mode 100644 index 0000000000..eee2714130 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Rendering.java @@ -0,0 +1,40 @@ +/* + * 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.function; + +import java.util.Map; + +/** + * Represents a template rendering, based on a {@code String} name and a model {@code Map}. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public interface Rendering { + + /** + * Return the name of the template to be rendered. + */ + String name(); + + /** + * Return the unmodifiable model map. + */ + Map model(); + + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RenderingResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RenderingResponse.java new file mode 100644 index 0000000000..bc0183f9a9 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/RenderingResponse.java @@ -0,0 +1,88 @@ +/* + * 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.function; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * @author Arjen Poutsma + */ +class RenderingResponse extends AbstractResponse { + + private final String name; + + private final Map model; + + private final Rendering rendering = new DefaultRendering(); + + public RenderingResponse(int statusCode, HttpHeaders headers, String name, + Map model) { + super(statusCode, headers); + this.name = name; + this.model = Collections.unmodifiableMap(model); + } + + @Override + public Rendering body() { + return this.rendering; + } + + @Override + public Mono writeTo(ServerWebExchange exchange) { + writeStatusAndHeaders(exchange); + MediaType contentType = exchange.getResponse().getHeaders().getContentType(); + Locale locale = Locale.ENGLISH; // TODO + return Flux.fromStream(viewResolverStream(exchange)) + .concatMap(viewResolver -> viewResolver.resolveViewName(this.name, locale)) + .next() + .otherwiseIfEmpty(Mono.error(new IllegalArgumentException("Could not resolve view with name '" + this.name +"'"))) + .then(view -> view.render(this.model, contentType, exchange)); + } + + private Stream viewResolverStream(ServerWebExchange exchange) { + return exchange.>>getAttribute( + Router.VIEW_RESOLVERS_ATTRIBUTE) + .orElseThrow(() -> new IllegalStateException( + "Could not find ViewResolvers in ServerWebExchange")) + .get(); + } + + private class DefaultRendering implements Rendering { + + @Override + public String name() { + return name; + } + + @Override + public Map model() { + return model; + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java index 18fba581fb..eaa2b4c662 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Response.java @@ -18,6 +18,8 @@ package org.springframework.web.reactive.function; import java.net.URI; import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.Map; import java.util.Set; import org.reactivestreams.Publisher; @@ -170,6 +172,7 @@ public interface Response { */ Mono writeTo(ServerWebExchange exchange); + /** * Defines a builder that adds headers to the response. * @@ -277,6 +280,7 @@ public interface Response { > Response build(T voidPublisher); } + /** * Defines a builder that adds a body to the response. */ @@ -348,6 +352,27 @@ public interface Response { */ > Response sse(S eventsPublisher, Class eventClass); + /** + * Render the template with the given {@code name} using the given {@code modelAttributes}. + * The model attributes are mapped under a + * {@linkplain org.springframework.core.Conventions#getVariableName generated name}. + *

Note: Empty {@link Collection Collections} are not added to + * the model when using this method because we cannot correctly determine + * the true convention name. + * @param name the name of the template to be rendered + * @param modelAttributes the modelAttributes used to render the template + * @return the built response + */ + Response render(String name, Object... modelAttributes); + + /** + * Render the template with the given {@code name} using the given {@code model}. + * @param name the name of the template to be rendered + * @param model the model used to render the template + * @return the built response + */ + Response render(String name, Map model); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Router.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Router.java index 9912d8fee4..ea5b5780f3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Router.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/function/Router.java @@ -29,6 +29,7 @@ import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.HttpWebHandlerAdapter; @@ -82,6 +83,13 @@ public abstract class Router { */ public static final String URI_TEMPLATE_VARIABLES_ATTRIBUTE = Router.class.getName() + ".uriTemplateVariables"; + /** + * Name of the {@link ServerWebExchange} attribute that contains the {@link Supplier} to the + * {@linkplain Stream stream} of {@link ViewResolver}s obtained + * from the {@linkplain Configuration#viewResolvers() configuration}. + */ + public static final String VIEW_RESOLVERS_ATTRIBUTE = Router.class.getName() + ".viewResolvers"; + /** * Route to the given handler function if the given request predicate applies. @@ -215,6 +223,7 @@ public abstract class Router { attributes.put(REQUEST_ATTRIBUTE, request); attributes.put(HTTP_MESSAGE_READERS_ATTRIBUTE, configuration.messageReaders()); attributes.put(HTTP_MESSAGE_WRITERS_ATTRIBUTE, configuration.messageWriters()); + attributes.put(VIEW_RESOLVERS_ATTRIBUTE, configuration.viewResolvers()); } @SuppressWarnings("unchecked") @@ -251,6 +260,11 @@ public abstract class Router { return () -> applicationContext.getBeansOfType(HttpMessageWriter.class).values().stream() .map(CastingUtils::cast); } + + @Override + public Supplier> viewResolvers() { + return () -> applicationContext.getBeansOfType(ViewResolver.class).values().stream(); + } }; } @@ -273,6 +287,13 @@ public abstract class Router { * @return the stream of message writers */ Supplier>> messageWriters(); + + /** + * Supply a {@linkplain Stream stream} of {@link ViewResolver}s to be used for view name + * resolution. + * @return the stream of view resolvers + */ + Supplier> viewResolvers(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ModelAndView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ModelAndView.java new file mode 100644 index 0000000000..3bec37bdde --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ModelAndView.java @@ -0,0 +1,202 @@ +/* + * 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.result.view; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import org.springframework.core.Conventions; +import org.springframework.util.Assert; + +/** + * Immutable holder for both model and view in the web MVC framework. Note that these are entirely + * distinct. This class merely holds both to make it possible for a handler to return both model + * and view in a single return value. + * + *

Represents a model and view returned by a handler. The view can take the form of a String + * view name which will need to be resolved by a {@link ViewResolver}; alternatively a + * {@link View} can be specified directly. The model + * is a {@link Map}, allowing the use of multiple objects keyed by name. + * @param the type of view the model contains (either a {@link View} or a {@code String} + * denoting the view name). + * @author Arjen Poutsma + * @see ViewResolver + * @since 5.0 + */ +public class ModelAndView { + + /** + * View instance or view name String + */ + private final T view; + + /** + * Model Map + */ + private final Map model; + + + private ModelAndView(T view, Map model) { + Assert.notNull(view, "'view' must not be null"); + Assert.notNull(model, "'model' must not be null"); + this.view = view; + this.model = Collections.unmodifiableMap(model); + } + + /** + * Return a builder for a {@code ModelAndView} with a view name. + * @return the builder + */ + public static Builder viewName(String viewName) { + Assert.hasLength(viewName, "'viewName' must not be empty"); + return new BuilderImpl<>(viewName); + } + + /** + * Return a builder for a {@code ModelAndView} with a {@link View} instance. + * @return the builder + */ + public static Builder view(View view) { + Assert.notNull(view, "'view' must not be null"); + return new BuilderImpl<>(view); + } + + /** + * Return the optional view name to be resolved by the DispatcherHandler + * via a ViewResolver. + */ + public Optional viewName() { + return this.view instanceof String ? Optional.of((String) this.view) : Optional.empty(); + } + + /** + * Return the optional View object. + */ + public Optional view() { + return this.view instanceof View ? Optional.of((View) this.view) : Optional.empty(); + } + + /** + * Return whether we use a view reference, i.e. {@code true} + * if the view has been specified via a name to be resolved by a ViewResolver. + */ + public boolean isReference() { + return (this.view instanceof String); + } + + /** + * Return the unmodifiable model map. Never returns {@code null}. + */ + public Map model() { + return this.model; + } + + /** + * Return diagnostic information about this model and view. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ModelAndView: "); + if (isReference()) { + sb.append("reference to view with name '").append(this.view).append("'"); + } + else { + sb.append("materialized View is [").append(this.view).append(']'); + } + sb.append("; model is ").append(this.model); + return sb.toString(); + } + + + /** + * A mutable builder for a {@link ModelAndView}. + * @param the type of view the model contains (either {@link View} or a view name). + */ + public interface Builder { + + /** + * Add the supplied model attribute under the supplied name. + * @param attributeName the name of the model attribute (never {@code null}) + * @param attributeValue the model attribute value (can be {@code null}) + */ + Builder modelAttribute(String attributeName, Object attributeValue); + + /** + * Add a model attribute using parameter name generation. + * @param attributeValue the object to add to the model (never {@code null}) + */ + Builder modelAttribute(Object attributeValue); + + /** + * Copy all attributes in the supplied {@code Map} into the model. + * @see #modelAttribute(String, Object) + */ + Builder modelAttributes(Map attributes); + + /** + * Builds the {@link ModelAndView}. + * @return the built model and view + */ + ModelAndView build(); + + } + + + private static class BuilderImpl implements Builder { + + private final T view; + + private final Map model = new LinkedHashMap<>(); + + public BuilderImpl(T view) { + this.view = view; + } + + @Override + public Builder modelAttribute(String attributeName, Object attributeValue) { + Assert.notNull(attributeName, "Model attribute name must not be null"); + this.model.put(attributeName, attributeValue); + return this; + } + + @Override + public Builder modelAttribute(Object attributeValue) { + Assert.notNull(attributeValue, "Model object must not be null"); + if (attributeValue instanceof Collection && ((Collection) attributeValue).isEmpty()) { + return this; + } + return modelAttribute(Conventions.getVariableName(attributeValue), attributeValue); + } + + @Override + public Builder modelAttributes(Map attributes) { + if (attributes != null) { + this.model.putAll(attributes); + } + return this; + } + + @Override + public ModelAndView build() { + return new ModelAndView(this.view, this.model); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/ResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultResponseBuilderTests.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/function/ResponseTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultResponseBuilderTests.java index 73deb105e4..9035ce4aa7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/ResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DefaultResponseBuilderTests.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.function; import java.net.URI; import java.time.ZonedDateTime; import java.util.Collections; +import java.util.Map; import org.junit.Test; @@ -32,7 +33,7 @@ import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma */ -public class ResponseTests { +public class DefaultResponseBuilderTests { @Test public void from() throws Exception { @@ -121,6 +122,7 @@ public class ResponseTests { Response result = Response.ok().eTag("foo").build(); assertEquals("\"foo\"", result.headers().getETag()); } + @Test public void lastModified() throws Exception { ZonedDateTime now = ZonedDateTime.now(); @@ -141,7 +143,13 @@ public class ResponseTests { } @Test - public void writeTo() throws Exception { + public void renderObjectArray() throws Exception { + Response result = + Response.ok().render("name", this, Collections.emptyList(), "foo"); + Map model = result.body().model(); + assertEquals(2, model.size()); + assertEquals(this, model.get("defaultResponseBuilderTests")); + assertEquals("foo", model.get("string")); } } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java index 00a95c54d6..bd94eca51f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/DispatcherHandlerIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.function; +import java.util.Collections; import java.util.List; import java.util.function.Supplier; import java.util.stream.Stream; @@ -45,6 +46,7 @@ import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.function.support.HandlerFunctionAdapter; import org.springframework.web.reactive.function.support.ResponseResultHandler; +import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertEquals; @@ -128,6 +130,11 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr public Supplier>> messageWriters() { return () -> getMessageWriters().stream(); } + + @Override + public Supplier> viewResolvers() { + return () -> Collections.emptySet().stream(); + } }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RenderingResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RenderingResponseTests.java new file mode 100644 index 0000000000..d450c5b2aa --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RenderingResponseTests.java @@ -0,0 +1,77 @@ +/* + * 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.function; + +import java.net.URI; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Arjen Poutsma + */ +public class RenderingResponseTests { + + private final Map model = Collections.singletonMap("foo", "bar"); + + private final RenderingResponse renderingResponse = new RenderingResponse(200, new HttpHeaders(), "view", + model); + + @Test + public void body() throws Exception { + assertEquals("view", renderingResponse.body().name()); + assertEquals(model, renderingResponse.body().model()); + } + + @Test + public void writeTo() throws Exception { + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, URI.create("http://localhost")); + MockServerHttpResponse response = new MockServerHttpResponse(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, new MockWebSessionManager()); + ViewResolver viewResolver = mock(ViewResolver.class); + View view = mock(View.class); + when(viewResolver.resolveViewName("view", Locale.ENGLISH)).thenReturn(Mono.just(view)); + when(view.render(model, null, exchange)).thenReturn(Mono.empty()); + exchange.getAttributes().put(Router.VIEW_RESOLVERS_ATTRIBUTE, + (Supplier>) () -> Collections + .singleton(viewResolver).stream()); + + + renderingResponse.writeTo(exchange).block(); + } + + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterTests.java index 8ddd11961e..74bfb38a4c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/function/RouterTests.java @@ -37,15 +37,11 @@ import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; /** * @author Arjen Poutsma @@ -137,6 +133,8 @@ public class RouterTests { () -> Collections.>emptyList().stream()); when(configuration.messageWriters()).thenReturn( () -> Collections.>emptyList().stream()); + when(configuration.viewResolvers()).thenReturn( + () -> Collections.emptyList().stream()); HttpHandler result = Router.toHttpHandler(routingFunction, configuration); assertNotNull(result); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ModelAndViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ModelAndViewTests.java new file mode 100644 index 0000000000..c44f3a240e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ModelAndViewTests.java @@ -0,0 +1,83 @@ +/* + * 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.result.view; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +public class ModelAndViewTests { + + @Test + public void view() { + View view = mock(View.class); + ModelAndView mav = ModelAndView.view(view).build(); + assertFalse(mav.isReference()); + assertTrue(mav.view().isPresent()); + assertFalse(mav.viewName().isPresent()); + assertEquals(view, mav.view().get()); + } + + @Test + public void viewName() { + String viewName = "foo"; + ModelAndView mav = ModelAndView.viewName(viewName).build(); + assertTrue(mav.isReference()); + assertTrue(mav.viewName().isPresent()); + assertFalse(mav.view().isPresent()); + assertEquals(viewName, mav.viewName().get()); + } + + @Test + public void modelAttribute() { + ModelAndView mav = ModelAndView.viewName("foo") + .modelAttribute("foo", "bar").build(); + assertEquals(1, mav.model().size()); + assertTrue(mav.model().containsKey("foo")); + assertEquals("bar", mav.model().get("foo")); + } + + @Test + public void modelAttributeNoName() { + ModelAndView mav = ModelAndView.viewName("foo") + .modelAttribute(this).build(); + assertEquals(1, mav.model().size()); + assertTrue(mav.model().containsKey("modelAndViewTests")); + assertEquals(this, mav.model().get("modelAndViewTests")); + } + + @Test + public void modelAttributes() { + Map model = new HashMap<>(); + model.put("foo", "bar"); + model.put("baz", "qux"); + ModelAndView mav = ModelAndView.viewName("foo") + .modelAttributes(model).build(); + assertEquals(2, mav.model().size()); + assertTrue(mav.model().containsKey("foo")); + assertEquals("bar", mav.model().get("foo")); + assertTrue(mav.model().containsKey("baz")); + assertEquals("qux", mav.model().get("baz")); + } +} \ No newline at end of file