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}