/*
* 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.core.codec.Encoder;
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.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;
/**
* Create a {@code View} with the given {@code Encoder}.
* Internally this creates
* {@link CodecHttpMessageConverter#CodecHttpMessageConverter(Encoder)
* CodecHttpMessageConverter(Encoder)}.
*/
public HttpMessageConverterView(Encoder<?> encoder) {
this(new CodecHttpMessageConverter<>(encoder));
}
/**
* Create a View that delegates to the given message converter.
*/
public HttpMessageConverterView(HttpMessageConverter<?> converter) {
Assert.notNull(converter, "'converter' is required.");
this.converter = converter;
this.mediaTypes = converter.getWritableMediaTypes();
}
/**
* Return the configured message converter.
*/
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);
}
}