AbstractMessageConverterMethodArgumentResolver.java 12.6 KB
Newer Older
1
/*
2
 * Copyright 2002-2015 the original author or authors.
3 4 5 6 7 8 9 10 11 12 13 14 15 16
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

17
package org.springframework.web.servlet.mvc.method.annotation;
18 19

import java.io.IOException;
20 21
import java.io.InputStream;
import java.io.PushbackInputStream;
22
import java.lang.annotation.Annotation;
23
import java.lang.reflect.Type;
24 25
import java.util.ArrayList;
import java.util.Collections;
26
import java.util.EnumSet;
27 28 29 30 31 32 33
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
J
Juergen Hoeller 已提交
34

35
import org.springframework.core.MethodParameter;
36
import org.springframework.core.ResolvableType;
37
import org.springframework.core.annotation.AnnotationUtils;
38
import org.springframework.http.HttpHeaders;
39
import org.springframework.http.HttpInputMessage;
40 41
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
42
import org.springframework.http.InvalidMediaTypeException;
43
import org.springframework.http.MediaType;
44
import org.springframework.http.converter.GenericHttpMessageConverter;
45
import org.springframework.http.converter.HttpMessageConverter;
J
Juergen Hoeller 已提交
46
import org.springframework.http.converter.HttpMessageNotReadableException;
47 48
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
49
import org.springframework.validation.Errors;
50
import org.springframework.validation.annotation.Validated;
51
import org.springframework.web.HttpMediaTypeNotSupportedException;
52
import org.springframework.web.bind.WebDataBinder;
53 54 55 56
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;

/**
57 58
 * A base class for resolving method argument values by reading from the body of
 * a request with {@link HttpMessageConverter}s.
59 60 61 62 63 64 65
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @since 3.1
 */
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {

66 67
	private static final Set<HttpMethod> SUPPORTED_METHODS =
			EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
68

69 70 71
	private static final Object NO_VALUE = new Object();


72
	protected final Log logger = LogFactory.getLog(getClass());
73

74
	protected final List<HttpMessageConverter<?>> messageConverters;
75

76 77
	protected final List<MediaType> allSupportedMediaTypes;

R
Rossen Stoyanchev 已提交
78 79
	private final RequestResponseBodyAdviceChain advice;

J
Juergen Hoeller 已提交
80

R
Rossen Stoyanchev 已提交
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
	/**
	 * Basic constructor with converters only.
	 */
	public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
		this(converters, null);
	}

	/**
	 * Constructor with converters and {@code Request~} and {@code ResponseBodyAdvice}.
	 * @since 4.2
	 */
	public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters,
			List<Object> requestResponseBodyAdvice) {

		Assert.notEmpty(converters, "'messageConverters' must not be empty");
		this.messageConverters = converters;
		this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
		this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
99 100
	}

J
Juergen Hoeller 已提交
101

102
	/**
103 104
	 * Return the media types supported by all provided message converters sorted
	 * by specificity via {@link MediaType#sortBySpecificity(List)}.
105 106 107 108 109 110 111 112 113 114
	 */
	private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
		Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>();
		for (HttpMessageConverter<?> messageConverter : messageConverters) {
			allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
		}
		List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes);
		MediaType.sortBySpecificity(result);
		return Collections.unmodifiableList(result);
	}
115

116

R
Rossen Stoyanchev 已提交
117 118 119 120 121 122 123 124 125
	/**
	 * Return the configured {@link RequestBodyAdvice} and
	 * {@link RequestBodyAdvice} where each instance may be wrapped as a
	 * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}.
	 */
	protected RequestResponseBodyAdviceChain getAdvice() {
		return this.advice;
	}

126
	/**
J
Juergen Hoeller 已提交
127
	 * Create the method argument value of the expected parameter type by
128 129
	 * reading from the given request.
	 * @param <T> the expected type of the argument value to be created
130 131 132 133 134 135 136
	 * @param webRequest the current request
	 * @param methodParam the method argument
	 * @param paramType the type of the argument value to be created
	 * @return the created method argument value
	 * @throws IOException if the reading from the request fails
	 * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
	 */
J
Juergen Hoeller 已提交
137 138
	protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam,
			Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
139 140 141 142

		HttpInputMessage inputMessage = createInputMessage(webRequest);
		return readWithMessageConverters(inputMessage, methodParam, paramType);
	}
143 144

	/**
J
Juergen Hoeller 已提交
145
	 * Create the method argument value of the expected parameter type by reading
146 147
	 * from the given HttpInputMessage.
	 * @param <T> the expected type of the argument value to be created
148
	 * @param inputMessage the HTTP input message representing the current request
R
Rossen Stoyanchev 已提交
149
	 * @param param the method parameter descriptor (may be {@code null})
150 151
	 * @param targetType the target type, not necessarily the same as the method
	 * parameter type, e.g. for {@code HttpEntity<String>}.
152 153 154 155 156
	 * @return the created method argument value
	 * @throws IOException if the reading from the request fails
	 * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
	 */
	@SuppressWarnings("unchecked")
J
Juergen Hoeller 已提交
157 158
	protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param,
			Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
159

160
		MediaType contentType;
161
		boolean noContentType = false;
162 163 164 165 166 167
		try {
			contentType = inputMessage.getHeaders().getContentType();
		}
		catch (InvalidMediaTypeException ex) {
			throw new HttpMediaTypeNotSupportedException(ex.getMessage());
		}
J
Juergen Hoeller 已提交
168
		if (contentType == null) {
169
			noContentType = true;
J
Juergen Hoeller 已提交
170 171
			contentType = MediaType.APPLICATION_OCTET_STREAM;
		}
172

R
Rossen Stoyanchev 已提交
173
		Class<?> contextClass = (param != null ? param.getContainingClass() : null);
174 175
		Class<T> targetClass = (targetType instanceof Class<?> ? (Class<T>) targetType : null);
		if (targetClass == null) {
R
Rossen Stoyanchev 已提交
176 177
			ResolvableType resolvableType = (param != null ?
					ResolvableType.forMethodParameter(param) : ResolvableType.forType(targetType));
178 179
			targetClass = (Class<T>) resolvableType.resolve();
		}
J
Juergen Hoeller 已提交
180

181
		HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod();
182 183
		Object body = NO_VALUE;

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
		try {
			inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);

			for (HttpMessageConverter<?> converter : this.messageConverters) {
				Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
				if (converter instanceof GenericHttpMessageConverter) {
					GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
					if (genericConverter.canRead(targetType, contextClass, contentType)) {
						if (logger.isDebugEnabled()) {
							logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
						}
						if (inputMessage.getBody() != null) {
							inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
							body = genericConverter.read(targetType, contextClass, inputMessage);
							body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
						}
						else {
							body = null;
							body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
						}
						break;
205
					}
J
Juergen Hoeller 已提交
206
				}
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
				else if (targetClass != null) {
					if (converter.canRead(targetClass, contentType)) {
						if (logger.isDebugEnabled()) {
							logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
						}
						if (inputMessage.getBody() != null) {
							inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
							body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
							body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
						}
						else {
							body = null;
							body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
						}
						break;
222
					}
223 224
				}
			}
J
Juergen Hoeller 已提交
225
		}
226 227 228
		catch (IOException ex) {
			throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
		}
J
Juergen Hoeller 已提交
229

230
		if (body == NO_VALUE) {
231 232
			if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
					(noContentType && inputMessage.getBody() == null)) {
233 234
				return null;
			}
235 236 237 238
			throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
		}

		return body;
J
Juergen Hoeller 已提交
239
	}
240 241

	/**
J
Juergen Hoeller 已提交
242
	 * Create a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
243 244 245 246 247 248 249 250
	 * @param webRequest the web request to create an input message from
	 * @return the input message
	 */
	protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
		HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
		return new ServletServerHttpRequest(servletRequest);
	}

251
	/**
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
	 * Validate the request part if applicable.
	 * <p>The default implementation checks for {@code @javax.validation.Valid},
	 * Spring's {@link org.springframework.validation.annotation.Validated},
	 * and custom annotations whose name starts with "Valid".
	 * @param binder the DataBinder to be used
	 * @param methodParam the method parameter
	 * @see #isBindExceptionRequired
	 * @since 4.1.5
	 */
	protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
		Annotation[] annotations = methodParam.getParameterAnnotations();
		for (Annotation ann : annotations) {
			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
			if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
				Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
				Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
				binder.validate(validationHints);
				break;
			}
		}
	}

	/**
	 * Whether to raise a fatal bind exception on validation errors.
	 * @param binder the data binder used to perform data binding
	 * @param methodParam the method argument
278 279 280
	 * @return {@code true} if the next method argument is not of type {@link Errors}
	 * @since 4.1.5
	 */
281 282 283
	protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
		int i = methodParam.getParameterIndex();
		Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
284 285 286 287
		boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
		return !hasBindingResult;
	}

288 289 290 291 292 293 294

	private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {

		private final HttpHeaders headers;

		private final InputStream body;

295 296
		private final HttpMethod method;

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319

		public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
			this.headers = inputMessage.getHeaders();
			InputStream inputStream = inputMessage.getBody();
			if (inputStream == null) {
				this.body = null;
			}
			else if (inputStream.markSupported()) {
				inputStream.mark(1);
				this.body = (inputStream.read() != -1 ? inputStream : null);
				inputStream.reset();
			}
			else {
				PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
				int b = pushbackInputStream.read();
				if (b == -1) {
					this.body = null;
				}
				else {
					this.body = pushbackInputStream;
					pushbackInputStream.unread(b);
				}
			}
320
			this.method = ((HttpRequest) inputMessage).getMethod();
321 322 323 324 325 326 327 328 329 330 331
		}

		@Override
		public HttpHeaders getHeaders() {
			return this.headers;
		}

		@Override
		public InputStream getBody() throws IOException {
			return this.body;
		}
332 333 334 335

		public HttpMethod getMethod() {
			return this.method;
		}
336 337
	}

338
}