001/* 002 * Copyright 2002-2017 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.servlet.mvc.method.annotation; 018 019import java.io.IOException; 020import java.io.OutputStream; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import javax.servlet.ServletRequest; 025import javax.servlet.http.HttpServletResponse; 026 027import org.apache.commons.logging.Log; 028import org.apache.commons.logging.LogFactory; 029 030import org.springframework.core.MethodParameter; 031import org.springframework.core.ResolvableType; 032import org.springframework.http.HttpHeaders; 033import org.springframework.http.HttpStatus; 034import org.springframework.http.MediaType; 035import org.springframework.http.ResponseEntity; 036import org.springframework.http.converter.HttpMessageConverter; 037import org.springframework.http.server.ServerHttpResponse; 038import org.springframework.http.server.ServletServerHttpResponse; 039import org.springframework.util.Assert; 040import org.springframework.web.context.request.NativeWebRequest; 041import org.springframework.web.context.request.async.DeferredResult; 042import org.springframework.web.context.request.async.WebAsyncUtils; 043import org.springframework.web.filter.ShallowEtagHeaderFilter; 044import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; 045import org.springframework.web.method.support.ModelAndViewContainer; 046 047/** 048 * Handler for return values of type {@link ResponseBodyEmitter} (and the 049 * {@code ResponseEntity<ResponseBodyEmitter>} sub-class) as well as any other 050 * async type with a {@link #getAdapterMap() registered adapter}. 051 * 052 * @author Rossen Stoyanchev 053 * @since 4.2 054 */ 055@SuppressWarnings("deprecation") 056public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { 057 058 private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class); 059 060 061 private final List<HttpMessageConverter<?>> messageConverters; 062 063 private final Map<Class<?>, ResponseBodyEmitterAdapter> adapterMap; 064 065 066 public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) { 067 Assert.notEmpty(messageConverters, "HttpMessageConverter List must not be empty"); 068 this.messageConverters = messageConverters; 069 this.adapterMap = new HashMap<Class<?>, ResponseBodyEmitterAdapter>(4); 070 this.adapterMap.put(ResponseBodyEmitter.class, new SimpleResponseBodyEmitterAdapter()); 071 } 072 073 074 /** 075 * Return the map with {@code ResponseBodyEmitter} adapters. 076 * By default the map contains a single adapter {@code ResponseBodyEmitter} 077 * that simply downcasts the return value. 078 * @return the map of adapters 079 * @deprecated in 4.3.8, see comments on {@link ResponseBodyEmitterAdapter} 080 */ 081 @Deprecated 082 public Map<Class<?>, ResponseBodyEmitterAdapter> getAdapterMap() { 083 return this.adapterMap; 084 } 085 086 private ResponseBodyEmitterAdapter getAdapterFor(Class<?> type) { 087 if (type != null) { 088 for (Class<?> adapteeType : getAdapterMap().keySet()) { 089 if (adapteeType.isAssignableFrom(type)) { 090 return getAdapterMap().get(adapteeType); 091 } 092 } 093 } 094 return null; 095 } 096 097 098 @Override 099 public boolean supportsReturnType(MethodParameter returnType) { 100 Class<?> bodyType; 101 if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { 102 bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); 103 } 104 else { 105 bodyType = returnType.getParameterType(); 106 } 107 return (getAdapterFor(bodyType) != null); 108 } 109 110 @Override 111 public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { 112 if (returnValue != null) { 113 Object adaptFrom = returnValue; 114 if (returnValue instanceof ResponseEntity) { 115 adaptFrom = ((ResponseEntity) returnValue).getBody(); 116 } 117 if (adaptFrom != null) { 118 return (getAdapterFor(adaptFrom.getClass()) != null); 119 } 120 } 121 return false; 122 } 123 124 @Override 125 public void handleReturnValue(Object returnValue, MethodParameter returnType, 126 ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { 127 128 if (returnValue == null) { 129 mavContainer.setRequestHandled(true); 130 return; 131 } 132 133 HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); 134 ServerHttpResponse outputMessage = new ServletServerHttpResponse(response); 135 136 if (returnValue instanceof ResponseEntity) { 137 ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue; 138 response.setStatus(responseEntity.getStatusCodeValue()); 139 outputMessage.getHeaders().putAll(responseEntity.getHeaders()); 140 returnValue = responseEntity.getBody(); 141 if (returnValue == null) { 142 mavContainer.setRequestHandled(true); 143 outputMessage.flush(); 144 return; 145 } 146 } 147 148 ServletRequest request = webRequest.getNativeRequest(ServletRequest.class); 149 ShallowEtagHeaderFilter.disableContentCaching(request); 150 151 ResponseBodyEmitterAdapter adapter = getAdapterFor(returnValue.getClass()); 152 if (adapter == null) { 153 throw new IllegalStateException( 154 "Could not find ResponseBodyEmitterAdapter for return value type: " + returnValue.getClass()); 155 } 156 ResponseBodyEmitter emitter = adapter.adaptToEmitter(returnValue, outputMessage); 157 emitter.extendResponse(outputMessage); 158 159 // Commit the response and wrap to ignore further header changes 160 outputMessage.getBody(); 161 outputMessage.flush(); 162 outputMessage = new StreamingServletServerHttpResponse(outputMessage); 163 164 DeferredResult<?> deferredResult = new DeferredResult<Object>(emitter.getTimeout()); 165 WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer); 166 167 HttpMessageConvertingHandler handler = new HttpMessageConvertingHandler(outputMessage, deferredResult); 168 emitter.initialize(handler); 169 } 170 171 172 /** 173 * Adapter for {@code ResponseBodyEmitter} return values. 174 */ 175 private static class SimpleResponseBodyEmitterAdapter implements ResponseBodyEmitterAdapter { 176 177 @Override 178 public ResponseBodyEmitter adaptToEmitter(Object returnValue, ServerHttpResponse response) { 179 Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue, "ResponseBodyEmitter expected"); 180 return (ResponseBodyEmitter) returnValue; 181 } 182 } 183 184 /** 185 * ResponseBodyEmitter.Handler that writes with HttpMessageConverter's. 186 */ 187 private class HttpMessageConvertingHandler implements ResponseBodyEmitter.Handler { 188 189 private final ServerHttpResponse outputMessage; 190 191 private final DeferredResult<?> deferredResult; 192 193 public HttpMessageConvertingHandler(ServerHttpResponse outputMessage, DeferredResult<?> deferredResult) { 194 this.outputMessage = outputMessage; 195 this.deferredResult = deferredResult; 196 } 197 198 @Override 199 public void send(Object data, MediaType mediaType) throws IOException { 200 sendInternal(data, mediaType); 201 } 202 203 @SuppressWarnings("unchecked") 204 private <T> void sendInternal(T data, MediaType mediaType) throws IOException { 205 for (HttpMessageConverter<?> converter : ResponseBodyEmitterReturnValueHandler.this.messageConverters) { 206 if (converter.canWrite(data.getClass(), mediaType)) { 207 ((HttpMessageConverter<T>) converter).write(data, mediaType, this.outputMessage); 208 this.outputMessage.flush(); 209 if (logger.isDebugEnabled()) { 210 logger.debug("Written [" + data + "] using [" + converter + "]"); 211 } 212 return; 213 } 214 } 215 throw new IllegalArgumentException("No suitable converter for " + data.getClass()); 216 } 217 218 @Override 219 public void complete() { 220 this.deferredResult.setResult(null); 221 } 222 223 @Override 224 public void completeWithError(Throwable failure) { 225 this.deferredResult.setErrorResult(failure); 226 } 227 228 @Override 229 public void onTimeout(Runnable callback) { 230 this.deferredResult.onTimeout(callback); 231 } 232 233 @Override 234 public void onCompletion(Runnable callback) { 235 this.deferredResult.onCompletion(callback); 236 } 237 } 238 239 240 /** 241 * Wrap to silently ignore header changes HttpMessageConverter's that would 242 * otherwise cause HttpHeaders to raise exceptions. 243 */ 244 private static class StreamingServletServerHttpResponse implements ServerHttpResponse { 245 246 private final ServerHttpResponse delegate; 247 248 private final HttpHeaders mutableHeaders = new HttpHeaders(); 249 250 public StreamingServletServerHttpResponse(ServerHttpResponse delegate) { 251 this.delegate = delegate; 252 this.mutableHeaders.putAll(delegate.getHeaders()); 253 } 254 255 @Override 256 public void setStatusCode(HttpStatus status) { 257 this.delegate.setStatusCode(status); 258 } 259 260 @Override 261 public HttpHeaders getHeaders() { 262 return this.mutableHeaders; 263 } 264 265 @Override 266 public OutputStream getBody() throws IOException { 267 return this.delegate.getBody(); 268 } 269 270 @Override 271 public void flush() throws IOException { 272 this.delegate.flush(); 273 } 274 275 @Override 276 public void close() { 277 this.delegate.close(); 278 } 279 } 280 281}