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