Add HttpMessageConverterView
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"))));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user