001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.reactive.result.view;
018
019import java.util.HashSet;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import org.reactivestreams.Publisher;
026import reactor.core.publisher.Mono;
027
028import org.springframework.core.ResolvableType;
029import org.springframework.core.codec.Encoder;
030import org.springframework.core.codec.Hints;
031import org.springframework.http.MediaType;
032import org.springframework.http.codec.EncoderHttpMessageWriter;
033import org.springframework.http.codec.HttpMessageWriter;
034import org.springframework.lang.Nullable;
035import org.springframework.util.Assert;
036import org.springframework.web.server.ServerWebExchange;
037
038/**
039 * {@code View} that writes model attribute(s) with an {@link HttpMessageWriter}.
040 *
041 * @author Rossen Stoyanchev
042 * @since 5.0
043 */
044public class HttpMessageWriterView implements View {
045
046        private final HttpMessageWriter<?> writer;
047
048        private final Set<String> modelKeys = new HashSet<>(4);
049
050        private final boolean canWriteMap;
051
052
053        /**
054         * Constructor with an {@code Encoder}.
055         */
056        public HttpMessageWriterView(Encoder<?> encoder) {
057                this(new EncoderHttpMessageWriter<>(encoder));
058        }
059
060        /**
061         * Constructor with a fully initialized {@link HttpMessageWriter}.
062         */
063        public HttpMessageWriterView(HttpMessageWriter<?> writer) {
064                Assert.notNull(writer, "HttpMessageWriter is required");
065                this.writer = writer;
066                this.canWriteMap = writer.canWrite(ResolvableType.forClass(Map.class), null);
067        }
068
069
070        /**
071         * Return the configured message writer.
072         */
073        public HttpMessageWriter<?> getMessageWriter() {
074                return this.writer;
075        }
076
077        /**
078         * {@inheritDoc}
079         * <p>The implementation of this method for {@link HttpMessageWriterView}
080         * delegates to {@link HttpMessageWriter#getWritableMediaTypes()}.
081         */
082        @Override
083        public List<MediaType> getSupportedMediaTypes() {
084                return this.writer.getWritableMediaTypes();
085        }
086
087        /**
088         * Set the attributes in the model that should be rendered by this view.
089         * When set, all other model attributes will be ignored. The matching
090         * attributes are further narrowed with {@link HttpMessageWriter#canWrite}.
091         * The matching attributes are processed as follows:
092         * <ul>
093         * <li>0: nothing is written to the response body.
094         * <li>1: the matching attribute is passed to the writer.
095         * <li>2..N: if the writer supports {@link Map}, write all matches;
096         * otherwise raise an {@link IllegalStateException}.
097         * </ul>
098         */
099        public void setModelKeys(@Nullable Set<String> modelKeys) {
100                this.modelKeys.clear();
101                if (modelKeys != null) {
102                        this.modelKeys.addAll(modelKeys);
103                }
104        }
105
106        /**
107         * Return the configured model keys.
108         */
109        public final Set<String> getModelKeys() {
110                return this.modelKeys;
111        }
112
113
114        @Override
115        @SuppressWarnings("unchecked")
116        public Mono<Void> render(
117                        @Nullable Map<String, ?> model, @Nullable MediaType contentType, ServerWebExchange exchange) {
118
119                Object value = getObjectToRender(model);
120                return (value != null ? write(value, contentType, exchange) : exchange.getResponse().setComplete());
121        }
122
123        @Nullable
124        private Object getObjectToRender(@Nullable Map<String, ?> model) {
125                if (model == null) {
126                        return null;
127                }
128
129                Map<String, ?> result = model.entrySet().stream()
130                                .filter(this::isMatch)
131                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
132
133                if (result.isEmpty()) {
134                        return null;
135                }
136                else if (result.size() == 1) {
137                        return result.values().iterator().next();
138                }
139                else if (this.canWriteMap) {
140                        return result;
141                }
142                else {
143                        throw new IllegalStateException("Multiple matches found: " + result + " but " +
144                                        "Map rendering is not supported by " + getMessageWriter().getClass().getName());
145                }
146        }
147
148        private boolean isMatch(Map.Entry<String, ?> entry) {
149                if (entry.getValue() == null) {
150                        return false;
151                }
152                if (!getModelKeys().isEmpty() && !getModelKeys().contains(entry.getKey())) {
153                        return false;
154                }
155                ResolvableType type = ResolvableType.forInstance(entry.getValue());
156                return getMessageWriter().canWrite(type, null);
157        }
158
159        @SuppressWarnings("unchecked")
160        private <T> Mono<Void> write(T value, @Nullable MediaType contentType, ServerWebExchange exchange) {
161                Publisher<T> input = Mono.justOrEmpty(value);
162                ResolvableType elementType = ResolvableType.forClass(value.getClass());
163                return ((HttpMessageWriter<T>) this.writer).write(
164                                input, elementType, contentType, exchange.getResponse(),
165                                Hints.from(Hints.LOG_PREFIX_HINT, exchange.getLogPrefix()));
166        }
167
168}