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.InputStream; 021import java.io.PushbackInputStream; 022import java.lang.annotation.Annotation; 023import java.lang.reflect.Type; 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.EnumSet; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Optional; 031import java.util.Set; 032import javax.servlet.http.HttpServletRequest; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036 037import org.springframework.core.MethodParameter; 038import org.springframework.core.ResolvableType; 039import org.springframework.core.annotation.AnnotationUtils; 040import org.springframework.http.HttpHeaders; 041import org.springframework.http.HttpInputMessage; 042import org.springframework.http.HttpMethod; 043import org.springframework.http.HttpRequest; 044import org.springframework.http.InvalidMediaTypeException; 045import org.springframework.http.MediaType; 046import org.springframework.http.converter.GenericHttpMessageConverter; 047import org.springframework.http.converter.HttpMessageConverter; 048import org.springframework.http.converter.HttpMessageNotReadableException; 049import org.springframework.http.server.ServletServerHttpRequest; 050import org.springframework.lang.UsesJava8; 051import org.springframework.util.Assert; 052import org.springframework.validation.Errors; 053import org.springframework.validation.annotation.Validated; 054import org.springframework.web.HttpMediaTypeNotSupportedException; 055import org.springframework.web.bind.WebDataBinder; 056import org.springframework.web.context.request.NativeWebRequest; 057import org.springframework.web.method.support.HandlerMethodArgumentResolver; 058 059/** 060 * A base class for resolving method argument values by reading from the body of 061 * a request with {@link HttpMessageConverter}s. 062 * 063 * @author Arjen Poutsma 064 * @author Rossen Stoyanchev 065 * @author Juergen Hoeller 066 * @since 3.1 067 */ 068public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { 069 070 private static final Set<HttpMethod> SUPPORTED_METHODS = 071 EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH); 072 073 private static final Object NO_VALUE = new Object(); 074 075 076 protected final Log logger = LogFactory.getLog(getClass()); 077 078 protected final List<HttpMessageConverter<?>> messageConverters; 079 080 protected final List<MediaType> allSupportedMediaTypes; 081 082 private final RequestResponseBodyAdviceChain advice; 083 084 085 /** 086 * Basic constructor with converters only. 087 */ 088 public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters) { 089 this(converters, null); 090 } 091 092 /** 093 * Constructor with converters and {@code Request~} and {@code ResponseBodyAdvice}. 094 * @since 4.2 095 */ 096 public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters, 097 List<Object> requestResponseBodyAdvice) { 098 099 Assert.notEmpty(converters, "'messageConverters' must not be empty"); 100 this.messageConverters = converters; 101 this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters); 102 this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice); 103 } 104 105 106 /** 107 * Return the media types supported by all provided message converters sorted 108 * by specificity via {@link MediaType#sortBySpecificity(List)}. 109 */ 110 private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) { 111 Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>(); 112 for (HttpMessageConverter<?> messageConverter : messageConverters) { 113 allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); 114 } 115 List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes); 116 MediaType.sortBySpecificity(result); 117 return Collections.unmodifiableList(result); 118 } 119 120 121 /** 122 * Return the configured {@link RequestBodyAdvice} and 123 * {@link RequestBodyAdvice} where each instance may be wrapped as a 124 * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}. 125 */ 126 protected RequestResponseBodyAdviceChain getAdvice() { 127 return this.advice; 128 } 129 130 /** 131 * Create the method argument value of the expected parameter type by 132 * reading from the given request. 133 * @param <T> the expected type of the argument value to be created 134 * @param webRequest the current request 135 * @param parameter the method parameter descriptor (may be {@code null}) 136 * @param paramType the type of the argument value to be created 137 * @return the created method argument value 138 * @throws IOException if the reading from the request fails 139 * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found 140 */ 141 protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, 142 Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { 143 144 HttpInputMessage inputMessage = createInputMessage(webRequest); 145 return readWithMessageConverters(inputMessage, parameter, paramType); 146 } 147 148 /** 149 * Create the method argument value of the expected parameter type by reading 150 * from the given HttpInputMessage. 151 * @param <T> the expected type of the argument value to be created 152 * @param inputMessage the HTTP input message representing the current request 153 * @param parameter the method parameter descriptor (may be {@code null}) 154 * @param targetType the target type, not necessarily the same as the method 155 * parameter type, e.g. for {@code HttpEntity<String>}. 156 * @return the created method argument value 157 * @throws IOException if the reading from the request fails 158 * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found 159 */ 160 @SuppressWarnings("unchecked") 161 protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, 162 Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { 163 164 MediaType contentType; 165 boolean noContentType = false; 166 try { 167 contentType = inputMessage.getHeaders().getContentType(); 168 } 169 catch (InvalidMediaTypeException ex) { 170 throw new HttpMediaTypeNotSupportedException(ex.getMessage()); 171 } 172 if (contentType == null) { 173 noContentType = true; 174 contentType = MediaType.APPLICATION_OCTET_STREAM; 175 } 176 177 Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null); 178 Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null); 179 if (targetClass == null) { 180 ResolvableType resolvableType = (parameter != null ? 181 ResolvableType.forMethodParameter(parameter) : ResolvableType.forType(targetType)); 182 targetClass = (Class<T>) resolvableType.resolve(); 183 } 184 185 HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod(); 186 Object body = NO_VALUE; 187 188 try { 189 inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage); 190 191 for (HttpMessageConverter<?> converter : this.messageConverters) { 192 Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); 193 if (converter instanceof GenericHttpMessageConverter) { 194 GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter; 195 if (genericConverter.canRead(targetType, contextClass, contentType)) { 196 if (logger.isDebugEnabled()) { 197 logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); 198 } 199 if (inputMessage.getBody() != null) { 200 inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType); 201 body = genericConverter.read(targetType, contextClass, inputMessage); 202 body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType); 203 } 204 else { 205 body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType); 206 } 207 break; 208 } 209 } 210 else if (targetClass != null) { 211 if (converter.canRead(targetClass, contentType)) { 212 if (logger.isDebugEnabled()) { 213 logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); 214 } 215 if (inputMessage.getBody() != null) { 216 inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType); 217 body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage); 218 body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType); 219 } 220 else { 221 body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType); 222 } 223 break; 224 } 225 } 226 } 227 } 228 catch (IOException ex) { 229 throw new HttpMessageNotReadableException("I/O error while reading input message", ex); 230 } 231 232 if (body == NO_VALUE) { 233 if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || 234 (noContentType && inputMessage.getBody() == null)) { 235 return null; 236 } 237 throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); 238 } 239 240 return body; 241 } 242 243 /** 244 * Create a new {@link HttpInputMessage} from the given {@link NativeWebRequest}. 245 * @param webRequest the web request to create an input message from 246 * @return the input message 247 */ 248 protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) { 249 HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); 250 return new ServletServerHttpRequest(servletRequest); 251 } 252 253 /** 254 * Validate the binding target if applicable. 255 * <p>The default implementation checks for {@code @javax.validation.Valid}, 256 * Spring's {@link org.springframework.validation.annotation.Validated}, 257 * and custom annotations whose name starts with "Valid". 258 * @param binder the DataBinder to be used 259 * @param parameter the method parameter descriptor 260 * @since 4.1.5 261 * @see #isBindExceptionRequired 262 */ 263 protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { 264 Annotation[] annotations = parameter.getParameterAnnotations(); 265 for (Annotation ann : annotations) { 266 Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); 267 if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { 268 Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); 269 Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); 270 binder.validate(validationHints); 271 break; 272 } 273 } 274 } 275 276 /** 277 * Whether to raise a fatal bind exception on validation errors. 278 * @param binder the data binder used to perform data binding 279 * @param parameter the method parameter descriptor 280 * @return {@code true} if the next method argument is not of type {@link Errors} 281 * @since 4.1.5 282 */ 283 protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) { 284 int i = parameter.getParameterIndex(); Class<?>[] paramTypes = parameter.getMethod().getParameterTypes(); 286 boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1])); 287 return !hasBindingResult; 288 } 289 290 /** 291 * Adapt the given argument against the method parameter, if necessary. 292 * @param arg the resolved argument 293 * @param parameter the method parameter descriptor 294 * @return the adapted argument, or the original resolved argument as-is 295 * @since 4.3.5 296 */ 297 protected Object adaptArgumentIfNecessary(Object arg, MethodParameter parameter) { 298 return (parameter.isOptional() ? OptionalResolver.resolveValue(arg) : arg); 299 } 300 301 302 private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage { 303 304 private final HttpHeaders headers; 305 306 private final InputStream body; 307 308 private final HttpMethod method; 309 310 311 public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException { 312 this.headers = inputMessage.getHeaders(); 313 InputStream inputStream = inputMessage.getBody(); 314 if (inputStream == null) { 315 this.body = null; 316 } 317 else if (inputStream.markSupported()) { 318 inputStream.mark(1); 319 this.body = (inputStream.read() != -1 ? inputStream : null); 320 inputStream.reset(); 321 } 322 else { 323 PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream); 324 int b = pushbackInputStream.read(); 325 if (b == -1) { 326 this.body = null; 327 } 328 else { 329 this.body = pushbackInputStream; 330 pushbackInputStream.unread(b); 331 } 332 } 333 this.method = ((HttpRequest) inputMessage).getMethod(); 334 } 335 336 @Override 337 public HttpHeaders getHeaders() { 338 return this.headers; 339 } 340 341 @Override 342 public InputStream getBody() throws IOException { 343 return this.body; 344 } 345 346 public HttpMethod getMethod() { 347 return this.method; 348 } 349 } 350 351 352 /** 353 * Inner class to avoid hard-coded dependency on Java 8 Optional type... 354 */ 355 @UsesJava8 356 private static class OptionalResolver { 357 358 public static Object resolveValue(Object value) { 359 if (value == null || (value instanceof Collection && ((Collection) value).isEmpty()) || 360 (value instanceof Object[] && ((Object[]) value).length == 0)) { 361 return Optional.empty(); 362 } 363 return Optional.of(value); 364 } 365 } 366 367}