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