Add HttpMessageConverterView

This commit is contained in:
Rossen Stoyanchev
2016-05-31 17:49:21 -04:00
parent a37b2e3a84
commit 5db1a54ff0
2 changed files with 333 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
/*
* 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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.server.ServerWebExchange;
/**
* A {@link View} that delegates to an {@link HttpMessageConverter}.
*
* @author Rossen Stoyanchev
*/
public class HttpMessageConverterView implements View {
private final HttpMessageConverter<?> converter;
private final Set<String> modelKeys = new HashSet<>(4);
private final List<MediaType> mediaTypes;
public HttpMessageConverterView(HttpMessageConverter<?> converter) {
Assert.notNull(converter, "'converter' is required.");
this.converter = converter;
this.mediaTypes = converter.getWritableMediaTypes();
}
public HttpMessageConverter<?> getConverter() {
return this.converter;
}
/**
* By default model attributes are filtered with
* {@link HttpMessageConverter#canWrite} to find the ones that can be
* rendered. Use this property to further narrow the list and consider only
* attribute(s) under specific model key(s).
* <p>If more than one matching attribute is found, than a Map is rendered,
* or if the {@code Encoder} does not support rendering a {@code Map} then
* an exception is raised.
*/
public void setModelKeys(Set<String> modelKeys) {
this.modelKeys.clear();
if (modelKeys != null) {
this.modelKeys.addAll(modelKeys);
}
}
/**
* Return the configured model keys.
*/
public final Set<String> getModelKeys() {
return this.modelKeys;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return this.mediaTypes;
}
@Override
public Mono<Void> render(HandlerResult result, MediaType contentType, ServerWebExchange exchange) {
Object value = extractObjectToRender(result);
return applyConverter(value, contentType, exchange);
}
protected Object extractObjectToRender(HandlerResult result) {
ModelMap model = result.getModel();
Map<String, Object> map = new HashMap<>(model.size());
for (Map.Entry<String, Object> entry : model.entrySet()) {
if (isEligibleAttribute(entry.getKey(), entry.getValue())) {
map.put(entry.getKey(), entry.getValue());
}
}
if (map.isEmpty()) {
return null;
}
else if (map.size() == 1) {
return map.values().iterator().next();
}
else if (getConverter().canWrite(ResolvableType.forClass(Map.class), null)) {
return map;
}
else {
throw new IllegalStateException(
"Multiple matching attributes found: " + map + ". " +
"However Map rendering is not supported by " + getConverter());
}
}
/**
* Whether the given model attribute key-value pair is eligible for encoding.
* <p>The default implementation checks against the configured
* {@link #setModelKeys model keys} and whether the Encoder supports the
* value type.
*/
protected boolean isEligibleAttribute(String attributeName, Object attributeValue) {
ResolvableType type = ResolvableType.forClass(attributeValue.getClass());
if (getModelKeys().isEmpty()) {
return getConverter().canWrite(type, null);
}
if (getModelKeys().contains(attributeName)) {
if (getConverter().canWrite(type, null)) {
return true;
}
throw new IllegalStateException(
"Model object [" + attributeValue + "] retrieved via key " +
"[" + attributeName + "] is not supported by " + getConverter());
}
return false;
}
@SuppressWarnings("unchecked")
private <T> Mono<Void> applyConverter(Object value, MediaType contentType, ServerWebExchange exchange) {
if (value == null) {
return Mono.empty();
}
Publisher<? extends T> stream = Mono.just((T) value);
ResolvableType type = ResolvableType.forClass(value.getClass());
ServerHttpResponse response = exchange.getResponse();
return ((HttpMessageConverter<T>) getConverter()).write(stream, type, contentType, response);
}
}

View File

@@ -0,0 +1,180 @@
/*
* 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.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import reactor.core.test.TestSubscriber;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.support.JacksonJsonEncoder;
import org.springframework.core.codec.support.Jaxb2Encoder;
import org.springframework.core.codec.support.Pojo;
import org.springframework.core.codec.support.StringEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
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.util.MimeType;
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 junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
/**
* Unit tests for {@link HttpMessageConverterView}.
* @author Rossen Stoyanchev
*/
public class HttpMessageConverterViewTests {
private HttpMessageConverterView view;
private HandlerResult result;
private ModelMap model;
@Before
public void setup() throws Exception {
HttpMessageConverter<?> converter = new CodecHttpMessageConverter<>(new JacksonJsonEncoder());
this.view = new HttpMessageConverterView(converter);
this.model = new ExtendedModelMap();
this.result = new HandlerResult(new Object(), null, ResolvableType.NONE, model);
}
@Test
public void supportedMediaTypes() throws Exception {
List<MimeType> mimeTypes = Arrays.asList(
new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
assertEquals(mimeTypes, this.view.getSupportedMediaTypes());
}
@Test
public void extractObject() throws Exception {
this.view.setModelKeys(Collections.singleton("foo2"));
this.model.addAttribute("foo1", "bar1");
this.model.addAttribute("foo2", "bar2");
this.model.addAttribute("foo3", "bar3");
assertEquals("bar2", this.view.extractObjectToRender(this.result));
}
@Test
public void extractObjectNoMatch() throws Exception {
this.view.setModelKeys(Collections.singleton("foo2"));
this.model.addAttribute("foo1", "bar1");
assertNull(this.view.extractObjectToRender(this.result));
}
@Test
public void extractObjectMultipleMatches() throws Exception {
this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
this.model.addAttribute("foo1", "bar1");
this.model.addAttribute("foo2", "bar2");
this.model.addAttribute("foo3", "bar3");
Object value = this.view.extractObjectToRender(this.result);
assertNotNull(value);
assertEquals(HashMap.class, value.getClass());
Map<?, ?> map = (Map<?, ?>) value;
assertEquals(2, map.size());
assertEquals("bar1", map.get("foo1"));
assertEquals("bar2", map.get("foo2"));
}
@Test
public void extractObjectMultipleMatchesNotSupported() throws Exception {
HttpMessageConverter<?> converter = new CodecHttpMessageConverter<>(new StringEncoder());
HttpMessageConverterView view = new HttpMessageConverterView(converter);
view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
this.model.addAttribute("foo1", "bar1");
this.model.addAttribute("foo2", "bar2");
try {
view.extractObjectToRender(this.result);
fail();
}
catch (IllegalStateException ex) {
String message = ex.getMessage();
assertTrue(message, message.contains("Map rendering is not supported"));
}
}
@Test
public void extractObjectNotSupported() throws Exception {
HttpMessageConverter<?> converter = new CodecHttpMessageConverter<>(new Jaxb2Encoder());
HttpMessageConverterView view = new HttpMessageConverterView(converter);
view.setModelKeys(new HashSet<>(Collections.singletonList("foo1")));
this.model.addAttribute("foo1", "bar1");
try {
view.extractObjectToRender(this.result);
fail();
}
catch (IllegalStateException ex) {
String message = ex.getMessage();
assertTrue(message, message.contains("[foo1] is not supported"));
}
}
@Test
public void render() throws Exception {
this.model.addAttribute("pojo", new Pojo("foo", "bar"));
this.view.setModelKeys(Collections.singleton("pojo"));
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path"));
MockServerHttpResponse response = new MockServerHttpResponse();
WebSessionManager manager = new DefaultWebSessionManager();
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager);
this.view.render(result, MediaType.APPLICATION_JSON, exchange);
new TestSubscriber<DataBuffer>().bindTo(response.getBody())
.assertValuesWith(buf -> assertEquals("{\"foo\":\"foo\",\"bar\":\"bar\"}",
DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8"))));
}
}