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.lang.annotation.Annotation;
021import java.lang.reflect.Method;
022import java.lang.reflect.Type;
023import java.util.concurrent.Callable;
024
025import org.springframework.core.MethodParameter;
026import org.springframework.core.ResolvableType;
027import org.springframework.http.HttpStatus;
028import org.springframework.util.ClassUtils;
029import org.springframework.util.StringUtils;
030import org.springframework.web.bind.annotation.ResponseStatus;
031import org.springframework.web.context.request.ServletWebRequest;
032import org.springframework.web.method.HandlerMethod;
033import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
034import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
035import org.springframework.web.method.support.InvocableHandlerMethod;
036import org.springframework.web.method.support.ModelAndViewContainer;
037import org.springframework.web.servlet.View;
038import org.springframework.web.util.NestedServletException;
039
040/**
041 * Extends {@link InvocableHandlerMethod} with the ability to handle return
042 * values through a registered {@link HandlerMethodReturnValueHandler} and
043 * also supports setting the response status based on a method-level
044 * {@code @ResponseStatus} annotation.
045 *
046 * <p>A {@code null} return value (including void) may be interpreted as the
047 * end of request processing in combination with a {@code @ResponseStatus}
048 * annotation, a not-modified check condition
049 * (see {@link ServletWebRequest#checkNotModified(long)}), or
050 * a method argument that provides access to the response stream.
051 *
052 * @author Rossen Stoyanchev
053 * @author Juergen Hoeller
054 * @since 3.1
055 */
056public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
057
058        private static final Method CALLABLE_METHOD = ClassUtils.getMethod(Callable.class, "call");
059
060        private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
061
062
063        /**
064         * Creates an instance from the given handler and method.
065         */
066        public ServletInvocableHandlerMethod(Object handler, Method method) {
067                super(handler, method);
068        }
069
070        /**
071         * Create an instance from a {@code HandlerMethod}.
072         */
073        public ServletInvocableHandlerMethod(HandlerMethod handlerMethod) {
074                super(handlerMethod);
075        }
076
077
078        /**
079         * Register {@link HandlerMethodReturnValueHandler} instances to use to
080         * handle return values.
081         */
082        public void setHandlerMethodReturnValueHandlers(HandlerMethodReturnValueHandlerComposite returnValueHandlers) {
083                this.returnValueHandlers = returnValueHandlers;
084        }
085
086
087        /**
088         * Invoke the method and handle the return value through one of the
089         * configured {@link HandlerMethodReturnValueHandler}s.
090         * @param webRequest the current request
091         * @param mavContainer the ModelAndViewContainer for this request
092         * @param providedArgs "given" arguments matched by type (not resolved)
093         */
094        public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
095                        Object... providedArgs) throws Exception {
096
097                Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
098                setResponseStatus(webRequest);
099
100                if (returnValue == null) {
101                        if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
102                                mavContainer.setRequestHandled(true);
103                                return;
104                        }
105                }
106                else if (StringUtils.hasText(getResponseStatusReason())) {
107                        mavContainer.setRequestHandled(true);
108                        return;
109                }
110
111                mavContainer.setRequestHandled(false);
112                try {
113                        this.returnValueHandlers.handleReturnValue(
114                                        returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
115                }
116                catch (Exception ex) {
117                        if (logger.isTraceEnabled()) {
118                                logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex);
119                        }
120                        throw ex;
121                }
122        }
123
124        /**
125         * Set the response status according to the {@link ResponseStatus} annotation.
126         */
127        private void setResponseStatus(ServletWebRequest webRequest) throws IOException {
128                HttpStatus status = getResponseStatus();
129                if (status == null) {
130                        return;
131                }
132
133                String reason = getResponseStatusReason();
134                if (StringUtils.hasText(reason)) {
135                        webRequest.getResponse().sendError(status.value(), reason);
136                }
137                else {
138                        webRequest.getResponse().setStatus(status.value());
139                }
140
141                // To be picked up by RedirectView
142                webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, status);
143        }
144
145        /**
146         * Does the given request qualify as "not modified"?
147         * @see ServletWebRequest#checkNotModified(long)
148         * @see ServletWebRequest#checkNotModified(String)
149         */
150        private boolean isRequestNotModified(ServletWebRequest webRequest) {
151                return webRequest.isNotModified();
152        }
153
154        private String getReturnValueHandlingErrorMessage(String message, Object returnValue) {
155                StringBuilder sb = new StringBuilder(message);
156                if (returnValue != null) {
157                        sb.append(" [type=").append(returnValue.getClass().getName()).append("]");
158                }
159                sb.append(" [value=").append(returnValue).append("]");
160                return getDetailedErrorMessage(sb.toString());
161        }
162
163        /**
164         * Create a nested ServletInvocableHandlerMethod subclass that returns the
165         * the given value (or raises an Exception if the value is one) rather than
166         * actually invoking the controller method. This is useful when processing
167         * async return values (e.g. Callable, DeferredResult, ListenableFuture).
168         */
169        ServletInvocableHandlerMethod wrapConcurrentResult(Object result) {
170                return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result));
171        }
172
173
174        /**
175         * A nested subclass of {@code ServletInvocableHandlerMethod} that uses a
176         * simple {@link Callable} instead of the original controller as the handler in
177         * order to return the fixed (concurrent) result value given to it. Effectively
178         * "resumes" processing with the asynchronously produced return value.
179         */
180        private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMethod {
181
182                private final MethodParameter returnType;
183
184                public ConcurrentResultHandlerMethod(final Object result, ConcurrentResultMethodParameter returnType) {
185                        super(new Callable<Object>() {
186                                @Override
187                                public Object call() throws Exception {
188                                        if (result instanceof Exception) {
189                                                throw (Exception) result;
190                                        }
191                                        else if (result instanceof Throwable) {
192                                                throw new NestedServletException("Async processing failed", (Throwable) result);
193                                        }
194                                        return result;
195                                }
196                        }, CALLABLE_METHOD);
197
198                        setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
199                        this.returnType = returnType;
200                }
201
202                /**
203                 * Bridge to actual controller type-level annotations.
204                 */
205                @Override
206                public Class<?> getBeanType() {
207                        return ServletInvocableHandlerMethod.this.getBeanType();
208                }
209
210                /**
211                 * Bridge to actual return value or generic type within the declared
212                 * async return type, e.g. Foo instead of {@code DeferredResult<Foo>}.
213                 */
214                @Override
215                public MethodParameter getReturnValueType(Object returnValue) {
216                        return this.returnType;
217                }
218
219                /**
220                 * Bridge to controller method-level annotations.
221                 */
222                @Override
223                public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
224                        return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType);
225                }
226
227                /**
228                 * Bridge to controller method-level annotations.
229                 */
230                @Override
231                public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
232                        return ServletInvocableHandlerMethod.this.hasMethodAnnotation(annotationType);
233                }
234        }
235
236
237        /**
238         * MethodParameter subclass based on the actual return value type or if
239         * that's null falling back on the generic type within the declared async
240         * return type, e.g. Foo instead of {@code DeferredResult<Foo>}.
241         */
242        private class ConcurrentResultMethodParameter extends HandlerMethodParameter {
243
244                private final Object returnValue;
245
246                private final ResolvableType returnType;
247
248                public ConcurrentResultMethodParameter(Object returnValue) {
249                        super(-1);
250                        this.returnValue = returnValue;
251                        this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0);
252                }
253
254                public ConcurrentResultMethodParameter(ConcurrentResultMethodParameter original) {
255                        super(original);
256                        this.returnValue = original.returnValue;
257                        this.returnType = original.returnType;
258                }
259
260                @Override
261                public Class<?> getParameterType() {
262                        if (this.returnValue != null) {
263                                return this.returnValue.getClass();
264                        }
265                        if (!ResolvableType.NONE.equals(this.returnType)) {
266                                return this.returnType.resolve(Object.class);
267                        }
268                        return super.getParameterType();
269                }
270
271                @Override
272                public Type getGenericParameterType() {
273                        return this.returnType.getType();
274                }
275
276                @Override
277                public ConcurrentResultMethodParameter clone() {
278                        return new ConcurrentResultMethodParameter(this);
279                }
280        }
281
282}