diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index e5bd4f5972b..9e1497016a0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; + import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -62,11 +63,27 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements protected final List allSupportedMediaTypes; + private final RequestResponseBodyAdviceChain advice; - public AbstractMessageConverterMethodArgumentResolver(List> messageConverters) { - Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); - this.messageConverters = messageConverters; - this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters); + + /** + * Basic constructor with converters only. + */ + public AbstractMessageConverterMethodArgumentResolver(List> converters) { + this(converters, null); + } + + /** + * Constructor with converters and {@code Request~} and {@code ResponseBodyAdvice}. + * @since 4.2 + */ + public AbstractMessageConverterMethodArgumentResolver(List> converters, + List requestResponseBodyAdvice) { + + Assert.notEmpty(converters, "'messageConverters' must not be empty"); + this.messageConverters = converters; + this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters); + this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice); } @@ -85,6 +102,15 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements } + /** + * 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; + } + /** * Create the method argument value of the expected parameter type by * reading from the given request. @@ -108,7 +134,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements * from the given HttpInputMessage. * @param the expected type of the argument value to be created * @param inputMessage the HTTP input message representing the current request - * @param methodParam the method parameter descriptor (may be {@code null}) + * @param param the method parameter descriptor (may be {@code null}) * @param targetType the type of object to create, not necessarily the same as * the method parameter type (e.g. for {@code HttpEntity} method * parameter the target type is String) @@ -118,7 +144,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements */ @SuppressWarnings("unchecked") protected Object readWithMessageConverters(HttpInputMessage inputMessage, - MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException { + MethodParameter param, Type targetType) throws IOException, HttpMediaTypeNotSupportedException { MediaType contentType; try { @@ -131,32 +157,35 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements contentType = MediaType.APPLICATION_OCTET_STREAM; } - Class contextClass = (methodParam != null ? methodParam.getContainingClass() : null); + Class contextClass = (param != null ? param.getContainingClass() : null); Class targetClass = (targetType instanceof Class ? (Class) targetType : null); if (targetClass == null) { - ResolvableType resolvableType = (methodParam != null ? - ResolvableType.forMethodParameter(methodParam) : ResolvableType.forType(targetType)); + ResolvableType resolvableType = (param != null ? + ResolvableType.forMethodParameter(param) : ResolvableType.forType(targetType)); targetClass = (Class) resolvableType.resolve(); } for (HttpMessageConverter converter : this.messageConverters) { + Class> converterType = (Class>) converter.getClass(); if (converter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter; if (genericConverter.canRead(targetType, contextClass, contentType)) { if (logger.isDebugEnabled()) { - logger.debug("Reading [" + targetType + "] as \"" + - contentType + "\" using [" + converter + "]"); + logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } - return genericConverter.read(targetType, contextClass, inputMessage); + inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); + T body = (T) genericConverter.read(targetType, contextClass, inputMessage); + return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); } } else if (targetClass != null) { if (converter.canRead(targetClass, contentType)) { if (logger.isDebugEnabled()) { - logger.debug("Reading [" + targetClass.getName() + "] as \"" + - contentType + "\" using [" + converter + "]"); + logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } - return ((HttpMessageConverter) converter).read(targetClass, inputMessage); + inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); + T body = ((HttpMessageConverter) converter).read(targetClass, inputMessage); + return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index d912e1cc84a..c20d47ea905 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -54,31 +55,25 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe private final ContentNegotiationManager contentNegotiationManager; - private final ResponseBodyAdviceChain adviceChain; - - protected AbstractMessageConverterMethodProcessor(List> messageConverters) { - this(messageConverters, null); + protected AbstractMessageConverterMethodProcessor(List> converters) { + this(converters, null); } - protected AbstractMessageConverterMethodProcessor(List> messageConverters, - ContentNegotiationManager manager) { - this(messageConverters, manager, null); + protected AbstractMessageConverterMethodProcessor(List> converters, + ContentNegotiationManager contentNegotiationManager) { + + this(converters, contentNegotiationManager, null); } - protected AbstractMessageConverterMethodProcessor(List> messageConverters, - ContentNegotiationManager manager, List responseBodyAdvice) { + protected AbstractMessageConverterMethodProcessor(List> converters, + ContentNegotiationManager manager, List requestResponseBodyAdvice) { - super(messageConverters); + super(converters, requestResponseBodyAdvice); this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager()); - this.adviceChain = new ResponseBodyAdviceChain(responseBodyAdvice); } - protected ResponseBodyAdviceChain getAdviceChain() { - return this.adviceChain; - } - /** * Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}. * @param webRequest the web request to create an output message from @@ -155,13 +150,14 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter messageConverter : this.messageConverters) { if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { - returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType, - (Class>) messageConverter.getClass(), inputMessage, outputMessage); + returnValue = (T) getAdvice().beforeBodyWrite(returnValue, returnType, selectedMediaType, + (Class>) messageConverter.getClass(), + inputMessage, outputMessage); if (returnValue != null) { ((HttpMessageConverter) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { - logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + - messageConverter + "]"); + logger.debug("Written [" + returnValue + "] as \"" + + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 5841dcfe29c..f6f55b274a0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -54,20 +54,47 @@ import org.springframework.web.method.support.ModelAndViewContainer; */ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor { - public HttpEntityMethodProcessor(List> messageConverters) { - super(messageConverters); + + /** + * Basic constructor with converters only. Suitable for resolving + * {@code HttpEntity}. For handling {@code ResponseEntity} consider also + * providing a {@code ContentNegotiationManager}. + */ + public HttpEntityMethodProcessor(List> converters) { + super(converters); } - public HttpEntityMethodProcessor(List> messageConverters, - ContentNegotiationManager contentNegotiationManager) { + /** + * Basic constructor with converters and {@code ContentNegotiationManager}. + * Suitable for resolving {@code HttpEntity} and handling {@code ResponseEntity} + * without {@code Request~} or {@code ResponseBodyAdvice}. + */ + public HttpEntityMethodProcessor(List> converters, + ContentNegotiationManager manager) { - super(messageConverters, contentNegotiationManager); + super(converters, manager); } - public HttpEntityMethodProcessor(List> messageConverters, - ContentNegotiationManager contentNegotiationManager, List responseBodyAdvice) { + /** + * Complete constructor for resolving {@code HttpEntity} method arguments. + * For handling {@code ResponseEntity} consider also providing a + * {@code ContentNegotiationManager}. + * @since 4.2 + */ + public HttpEntityMethodProcessor(List> converters, + List requestResponseBodyAdvice) { - super(messageConverters, contentNegotiationManager, responseBodyAdvice); + super(converters, null, requestResponseBodyAdvice); + } + + /** + * Complete constructor for resolving {@code HttpEntity} and handling + * {@code ResponseEntity}. + */ + public HttpEntityMethodProcessor(List> converters, + ContentNegotiationManager manager, List requestResponseBodyAdvice) { + + super(converters, manager, requestResponseBodyAdvice); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java new file mode 100644 index 00000000000..0d810c29f57 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.lang.reflect.Type; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; + + +/** + * Allows customizing the request before its body is read and converted into an + * Object and also allows the resulting Object before it is passed into a + * controller method as an {code @RequestBody} or an {@code HttpEntity} method + * argument. + * + *

Implementations of this contract may be registered directly with the + * {@code RequestMappingHandlerAdapter} or more likely annotated with + * {@code @ControllerAdvice} in which case they are auto-detected. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public interface RequestBodyAdvice { + + /** + * Invoked first to determine if this interceptor applies. + * @param methodParameter the method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity}. + * @param converterType the selected converter type + * @return whether this interceptor should be invoked or not + */ + boolean supports(MethodParameter methodParameter, Type targetType, + Class> converterType); + + /** + * Invoked second (and last) if the body is empty. + * @param body set to {@code null} before the first advice is called + * @param inputMessage the request + * @param parameter the method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity}. + * @param converterType the selected converter type + * @return the value to use or {@code null} which may then raise an + * {@code HttpMessageNotReadableException} if the argument is required. + */ + Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType); + + /** + * Invoked second before the request body is read and converted. + * @param inputMessage the request + * @param parameter the target method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity}. + * @param converterType the converter used to deserialize the body + * @return the input request or a new instance, never {@code null} + */ + HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType); + + /** + * Invoked third (and last) after the request body is converted to an Object. + * @param body set to the converter Object before the 1st advice is called + * @param inputMessage the request + * @param parameter the target method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity}. + * @param converterType the converter used to deserialize the body + * @return the same body or a new instance + */ + Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 57687e8f5da..e9ccd1dd489 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -131,7 +131,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter private List> messageConverters; - private List responseBodyAdvice = new ArrayList(); + private List requestResponseBodyAdvice = new ArrayList(); private WebBindingInitializer webBindingInitializer; @@ -329,15 +329,24 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter } /** - * Add one or more components to modify the response after the execution of a - * controller method annotated with {@code @ResponseBody}, or a method returning - * {@code ResponseEntity} and before the body is written to the response with - * the selected {@code HttpMessageConverter}. + * Add one or more {@code RequestBodyAdvice} instances to intercept the + * request before it is read and converted for {@code @RequestBody} and + * {@code HttpEntity} method arguments. + */ + public void setRequestBodyAdvice(List requestBodyAdvice) { + if (requestBodyAdvice != null) { + this.requestResponseBodyAdvice.addAll(requestBodyAdvice); + } + } + + /** + * Add one or more {@code ResponseBodyAdvice} instances to intercept the + * response before {@code @ResponseBody} or {@code ResponseEntity} return + * values are written to the response body. */ public void setResponseBodyAdvice(List> responseBodyAdvice) { - this.responseBodyAdvice.clear(); if (responseBodyAdvice != null) { - this.responseBodyAdvice.addAll(responseBodyAdvice); + this.requestResponseBodyAdvice.addAll(responseBodyAdvice); } } @@ -520,7 +529,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter List beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(beans); - List responseBodyAdviceBeans = new ArrayList(); + List requestResponseBodyAdviceBeans = new ArrayList(); for (ControllerAdviceBean bean : beans) { Set attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS); @@ -533,14 +542,18 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter this.initBinderAdviceCache.put(bean, binderMethods); logger.info("Detected @InitBinder methods in " + bean); } + if (RequestBodyAdvice.class.isAssignableFrom(bean.getBeanType())) { + requestResponseBodyAdviceBeans.add(bean); + logger.info("Detected RequestBodyAdvice bean in " + bean); + } if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) { - responseBodyAdviceBeans.add(bean); + requestResponseBodyAdviceBeans.add(bean); logger.info("Detected ResponseBodyAdvice bean in " + bean); } } - if (!responseBodyAdviceBeans.isEmpty()) { - this.responseBodyAdvice.addAll(0, responseBodyAdviceBeans); + if (!requestResponseBodyAdviceBeans.isEmpty()) { + this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans); } } @@ -559,8 +572,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); - resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters())); - resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters())); + resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); + resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); @@ -569,7 +582,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); - resolvers.add(new HttpEntityMethodProcessor(getMessageConverters())); + resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); @@ -633,8 +646,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter handlers.add(new ViewMethodReturnValueHandler()); handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters())); handlers.add(new StreamingResponseBodyReturnValueHandler()); - handlers.add(new HttpEntityMethodProcessor( - getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); + handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), + this.contentNegotiationManager, this.requestResponseBodyAdvice)); handlers.add(new HttpHeadersReturnValueHandler()); handlers.add(new CallableMethodReturnValueHandler()); handlers.add(new DeferredResultMethodReturnValueHandler()); @@ -643,8 +656,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter // Annotation-based return value types handlers.add(new ModelAttributeMethodProcessor(false)); - handlers.add(new RequestResponseBodyMethodProcessor( - getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); + handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), + this.contentNegotiationManager, this.requestResponseBodyAdvice)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java index 69982f7c7b6..2f6475ca53f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.Part; @@ -74,10 +75,24 @@ import org.springframework.web.util.WebUtils; */ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver { + + /** + * Basic constructor with converters only. + */ public RequestPartMethodArgumentResolver(List> messageConverters) { super(messageConverters); } + /** + * Constructor with converters and {@code Request~} and + * {@code ResponseBodyAdvice}. + */ + public RequestPartMethodArgumentResolver(List> messageConverters, + List requestResponseBodyAdvice) { + + super(messageConverters, requestResponseBodyAdvice); + } + /** * Supports the following: diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java new file mode 100644 index 00000000000..d89d7c4200d --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.util.CollectionUtils; +import org.springframework.web.method.ControllerAdviceBean; + + +/** + * Invokes {@link RequestBodyAdvice} and {@link ResponseBodyAdvice} where each + * instance may be (and is most likely) wrapped with + * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice { + + private final List requestBodyAdvice = new ArrayList(4); + + private final List responseBodyAdvice = new ArrayList(4); + + + /** + * Create an instance from a list of objects that are either of type + * {@code ControllerAdviceBean} or {@code RequestBodyAdvice}. + */ + public RequestResponseBodyAdviceChain(List requestResponseBodyAdvice) { + initAdvice(requestResponseBodyAdvice); + } + + private void initAdvice(List requestResponseBodyAdvice) { + if (requestResponseBodyAdvice == null) { + return; + } + for (Object advice : requestResponseBodyAdvice) { + Class beanType = (advice instanceof ControllerAdviceBean ? + ((ControllerAdviceBean) advice).getBeanType() : advice.getClass()); + if (RequestBodyAdvice.class.isAssignableFrom(beanType)) { + this.requestBodyAdvice.add(advice); + } + else if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) { + this.responseBodyAdvice.add(advice); + } + } + } + + private List getAdvice(Class adviceType) { + if (RequestBodyAdvice.class.equals(adviceType)) { + return this.requestBodyAdvice; + } + else if (ResponseBodyAdvice.class.equals(adviceType)) { + return this.responseBodyAdvice; + } + else { + throw new IllegalArgumentException("Unexpected adviceType: " + adviceType); + } + } + + + @Override + public boolean supports(MethodParameter param, Type type, Class> converterType) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) { + + for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { + if (advice.supports(parameter, targetType, converterType)) { + body = advice.handleEmptyBody(body, inputMessage, parameter, targetType, converterType); + } + } + return body; + } + + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter, + Type targetType, Class> converterType) { + + for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { + if (advice.supports(parameter, targetType, converterType)) { + request = advice.beforeBodyRead(request, parameter, targetType, converterType); + } + } + return request; + } + + @Override + public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) { + + for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { + if (advice.supports(parameter, targetType, converterType)) { + body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType); + } + } + return body; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType, + Class> converterType, + ServerHttpRequest request, ServerHttpResponse response) { + + return processBody(body, returnType, contentType, converterType, request, response); + } + + @SuppressWarnings("unchecked") + private Object processBody(Object body, MethodParameter returnType, MediaType contentType, + Class> converterType, + ServerHttpRequest request, ServerHttpResponse response) { + + for (ResponseBodyAdvice advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { + if (advice.supports(returnType, converterType)) { + body = ((ResponseBodyAdvice) advice).beforeBodyWrite((T) body, returnType, + contentType, converterType, request, response); + } + } + return body; + } + + @SuppressWarnings("unchecked") + private List getMatchingAdvice(MethodParameter parameter, Class adviceType) { + List availableAdvice = getAdvice(adviceType); + if (CollectionUtils.isEmpty(availableAdvice)) { + return Collections.emptyList(); + } + List result = new ArrayList(availableAdvice.size()); + for (Object advice : availableAdvice) { + if (advice instanceof ControllerAdviceBean) { + ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; + if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) { + continue; + } + advice = adviceBean.resolveBean(); + } + if (adviceType.isAssignableFrom(advice.getClass())) { + result.add((A) advice); + } + } + return result; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index 84f81d7138a..0fb8294cba2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -21,6 +21,7 @@ import java.io.InputStream; import java.io.PushbackInputStream; import java.lang.reflect.Type; import java.util.List; + import javax.servlet.http.HttpServletRequest; import org.springframework.core.Conventions; @@ -59,20 +60,48 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv */ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { - public RequestResponseBodyMethodProcessor(List> messageConverters) { - super(messageConverters); + + /** + * Basic constructor with converters only. Suitable for resolving + * {@code @RequestBody}. For handling {@code @ResponseBody} consider also + * providing a {@code ContentNegotiationManager}. + */ + public RequestResponseBodyMethodProcessor(List> converters) { + super(converters); } - public RequestResponseBodyMethodProcessor(List> messageConverters, - ContentNegotiationManager contentNegotiationManager) { + /** + * Basic constructor with converters and {@code ContentNegotiationManager}. + * Suitable for resolving {@code @RequestBody} and handling + * {@code @ResponseBody} without {@code Request~} or + * {@code ResponseBodyAdvice}. + */ + public RequestResponseBodyMethodProcessor(List> converters, + ContentNegotiationManager manager) { - super(messageConverters, contentNegotiationManager); + super(converters, manager); } - public RequestResponseBodyMethodProcessor(List> messageConverters, - ContentNegotiationManager contentNegotiationManager, List responseBodyAdvice) { + /** + * Complete constructor for resolving {@code @RequestBody} method arguments. + * For handling {@code @ResponseBody} consider also providing a + * {@code ContentNegotiationManager}. + * @since 4.2 + */ + public RequestResponseBodyMethodProcessor(List> converters, + List requestResponseBodyAdvice) { - super(messageConverters, contentNegotiationManager, responseBodyAdvice); + super(converters, null, requestResponseBodyAdvice); + } + + /** + * Complete constructor for resolving {@code @RequestBody} and handling + * {@code @ResponseBody}. + */ + public RequestResponseBodyMethodProcessor(List> converters, + ContentNegotiationManager manager, List requestResponseBodyAdvice) { + + super(converters, manager, requestResponseBodyAdvice); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java deleted file mode 100644 index 4b17fce7a1f..00000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2002-2015 the original author or authors. - * - * 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. - */ - -package org.springframework.web.servlet.mvc.method.annotation; - -import java.util.List; - -import org.springframework.core.MethodParameter; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.util.CollectionUtils; -import org.springframework.web.method.ControllerAdviceBean; - -/** - * Invokes a a list of {@link ResponseBodyAdvice} beans. - * - * @author Rossen Stoyanchev - * @since 4.1 - */ -class ResponseBodyAdviceChain { - - private final List advice; - - - public ResponseBodyAdviceChain(List advice) { - this.advice = advice; - } - - - public boolean hasAdvice() { - return !CollectionUtils.isEmpty(this.advice); - } - - @SuppressWarnings("unchecked") - public T invoke(T body, MethodParameter returnType, - MediaType selectedContentType, Class> selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - - if (this.advice != null) { - for (Object advice : this.advice) { - if (advice instanceof ControllerAdviceBean) { - ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; - if (!adviceBean.isApplicableToBeanType(returnType.getContainingClass())) { - continue; - } - advice = adviceBean.resolveBean(); - } - if (advice instanceof ResponseBodyAdvice) { - ResponseBodyAdvice typedAdvice = (ResponseBodyAdvice) advice; - if (typedAdvice.supports(returnType, selectedConverterType)) { - body = typedAdvice.beforeBodyWrite(body, returnType, - selectedContentType, selectedConverterType, request, response); - } - } - else { - throw new IllegalStateException("Expected ResponseBodyAdvice: " + advice); - } - } - } - return body; - } - -} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java index d87a2022662..ff553f67e73 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -97,7 +97,7 @@ public class AnnotationDrivenBeanDefinitionParserTests { loadBeanDefinitions("mvc-config-message-converters.xml"); verifyMessageConverters(appContext.getBean(RequestMappingHandlerAdapter.class), true); verifyMessageConverters(appContext.getBean(ExceptionHandlerExceptionResolver.class), true); - verifyResponseBodyAdvice(appContext.getBean(RequestMappingHandlerAdapter.class)); + verifyRequestResponseBodyAdvice(appContext.getBean(RequestMappingHandlerAdapter.class)); verifyResponseBodyAdvice(appContext.getBean(ExceptionHandlerExceptionResolver.class)); } @@ -182,6 +182,16 @@ public class AnnotationDrivenBeanDefinitionParserTests { assertTrue(converters.get(0) instanceof JsonViewResponseBodyAdvice); } + @SuppressWarnings("unchecked") + private void verifyRequestResponseBodyAdvice(Object bean) { + assertNotNull(bean); + Object value = new DirectFieldAccessor(bean).getPropertyValue("requestResponseBodyAdvice"); + assertNotNull(value); + assertTrue(value instanceof List); + List converters = (List) value; + assertTrue(converters.get(0) instanceof JsonViewResponseBodyAdvice); + } + } class TestWebArgumentResolver implements WebArgumentResolver { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index af0e3a9dd0b..17e3ed3fb76 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -188,7 +188,8 @@ public class WebMvcConfigurationSupportTests { assertTrue(validator instanceof LocalValidatorFactoryBean); DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(adapter); - List interceptors = (List) fieldAccessor.getPropertyValue("responseBodyAdvice"); + @SuppressWarnings("unchecked") + List interceptors = (List) fieldAccessor.getPropertyValue("requestResponseBodyAdvice"); assertEquals(1, interceptors.size()); assertEquals(JsonViewResponseBodyAdvice.class, interceptors.get(0).getClass()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChainTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChainTests.java similarity index 59% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChainTests.java rename to spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChainTests.java index bc8a2a8e26d..299fcc13884 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChainTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChainTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,18 @@ package org.springframework.web.servlet.mvc.method.annotation; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + import java.util.Arrays; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; @@ -38,17 +43,13 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.ControllerAdviceBean; -import static org.junit.Assert.*; -import static org.mockito.BDDMockito.*; - /** - * Unit tests for - * {@link ResponseBodyAdviceChain}. + * Unit tests for {@link RequestResponseBodyAdviceChain}. * * @author Rossen Stoyanchev - * @since 4.1 + * @since 4.2 */ -public class ResponseBodyAdviceChainTests { +public class RequestResponseBodyAdviceChainTests { private String body; @@ -56,10 +57,10 @@ public class ResponseBodyAdviceChainTests { private Class> converterType; + private MethodParameter paramType; private MethodParameter returnType; private ServerHttpRequest request; - private ServerHttpResponse response; @@ -68,25 +69,53 @@ public class ResponseBodyAdviceChainTests { this.body = "body"; this.contentType = MediaType.TEXT_PLAIN; this.converterType = StringHttpMessageConverter.class; - this.returnType = new MethodParameter(ClassUtils.getMethod(this.getClass(), "handle"), -1); + this.paramType = new MethodParameter(ClassUtils.getMethod(this.getClass(), "handle", String.class), 0); + this.returnType = new MethodParameter(ClassUtils.getMethod(this.getClass(), "handle", String.class), -1); this.request = new ServletServerHttpRequest(new MockHttpServletRequest()); this.response = new ServletServerHttpResponse(new MockHttpServletResponse()); } + + @SuppressWarnings("unchecked") + @Test + public void requestBodyAdvice() { + + RequestBodyAdvice requestAdvice = Mockito.mock(RequestBodyAdvice.class); + ResponseBodyAdvice responseAdvice = Mockito.mock(ResponseBodyAdvice.class); + List advice = Arrays.asList(requestAdvice, responseAdvice); + RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(advice); + + HttpInputMessage wrapped = new ServletServerHttpRequest(new MockHttpServletRequest()); + given(requestAdvice.supports(this.paramType, String.class, this.converterType)).willReturn(true); + given(requestAdvice.beforeBodyRead(eq(this.request), eq(this.paramType), eq(String.class), + eq(this.converterType))).willReturn(wrapped); + + assertSame(wrapped, chain.beforeBodyRead(this.request, this.paramType, String.class, this.converterType)); + + String modified = "body++"; + given(requestAdvice.afterBodyRead(eq(this.body), eq(this.request), eq(this.paramType), + eq(String.class), eq(this.converterType))).willReturn(modified); + + assertEquals(modified, chain.afterBodyRead(this.body, this.request, this.paramType, + String.class, this.converterType)); + } + + @SuppressWarnings("unchecked") @Test public void responseBodyAdvice() { - @SuppressWarnings("unchecked") - ResponseBodyAdvice advice = Mockito.mock(ResponseBodyAdvice.class); - ResponseBodyAdviceChain chain = new ResponseBodyAdviceChain(Arrays.asList(advice)); + RequestBodyAdvice requestAdvice = Mockito.mock(RequestBodyAdvice.class); + ResponseBodyAdvice responseAdvice = Mockito.mock(ResponseBodyAdvice.class); + List advice = Arrays.asList(requestAdvice, responseAdvice); + RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(advice); String expected = "body++"; - given(advice.supports(this.returnType, this.converterType)).willReturn(true); - given(advice.beforeBodyWrite(eq(this.body), eq(this.returnType), eq(this.contentType), + given(responseAdvice.supports(this.returnType, this.converterType)).willReturn(true); + given(responseAdvice.beforeBodyWrite(eq(this.body), eq(this.returnType), eq(this.contentType), eq(this.converterType), same(this.request), same(this.response))).willReturn(expected); - String actual = chain.invoke(this.body, this.returnType, - this.contentType, this.converterType, this.request, this.response); + String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType, + this.converterType, this.request, this.response); assertEquals(expected, actual); } @@ -95,10 +124,10 @@ public class ResponseBodyAdviceChainTests { public void controllerAdvice() { Object adviceBean = new ControllerAdviceBean(new MyControllerAdvice()); - ResponseBodyAdviceChain chain = new ResponseBodyAdviceChain(Arrays.asList(adviceBean)); + RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Arrays.asList(adviceBean)); - String actual = chain.invoke(this.body, this.returnType, - this.contentType, this.converterType, this.request, this.response); + String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType, + this.converterType, this.request, this.response); assertEquals("body-MyControllerAdvice", actual); } @@ -107,10 +136,10 @@ public class ResponseBodyAdviceChainTests { public void controllerAdviceNotApplicable() { Object adviceBean = new ControllerAdviceBean(new TargetedControllerAdvice()); - ResponseBodyAdviceChain chain = new ResponseBodyAdviceChain(Arrays.asList(adviceBean)); + RequestResponseBodyAdviceChain chain = new RequestResponseBodyAdviceChain(Arrays.asList(adviceBean)); - String actual = chain.invoke(this.body, this.returnType, - this.contentType, this.converterType, this.request, this.response); + String actual = (String) chain.beforeBodyWrite(this.body, this.returnType, this.contentType, + this.converterType, this.request, this.response); assertEquals(this.body, actual); } @@ -152,10 +181,10 @@ public class ResponseBodyAdviceChainTests { } } - @SuppressWarnings("unused") @ResponseBody - public String handle() { + public String handle(String body) { return ""; } + }