View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.mvc.method.annotation;
18  
19  import java.io.IOException;
20  import java.lang.annotation.Annotation;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.Type;
23  import java.util.concurrent.Callable;
24  
25  import org.springframework.core.MethodParameter;
26  import org.springframework.core.ResolvableType;
27  import org.springframework.http.HttpStatus;
28  import org.springframework.util.ClassUtils;
29  import org.springframework.util.StringUtils;
30  import org.springframework.web.bind.annotation.ResponseStatus;
31  import org.springframework.web.context.request.ServletWebRequest;
32  import org.springframework.web.method.HandlerMethod;
33  import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
34  import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
35  import org.springframework.web.method.support.InvocableHandlerMethod;
36  import org.springframework.web.method.support.ModelAndViewContainer;
37  import org.springframework.web.servlet.View;
38  import org.springframework.web.util.NestedServletException;
39  
40  /**
41   * Extends {@link InvocableHandlerMethod} with the ability to handle return
42   * values through a registered {@link HandlerMethodReturnValueHandler} and
43   * also supports setting the response status based on a method-level
44   * {@code @ResponseStatus} annotation.
45   *
46   * <p>A {@code null} return value (including void) may be interpreted as the
47   * end of request processing in combination with a {@code @ResponseStatus}
48   * annotation, a not-modified check condition
49   * (see {@link ServletWebRequest#checkNotModified(long)}), or
50   * a method argument that provides access to the response stream.
51   *
52   * @author Rossen Stoyanchev
53   * @since 3.1
54   */
55  public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
56  
57  	private static final Method CALLABLE_METHOD = ClassUtils.getMethod(Callable.class, "call");
58  
59  
60  	private HttpStatus responseStatus;
61  
62  	private String responseReason;
63  
64  	private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
65  
66  
67  	/**
68  	 * Creates an instance from the given handler and method.
69  	 */
70  	public ServletInvocableHandlerMethod(Object handler, Method method) {
71  		super(handler, method);
72  		initResponseStatus();
73  	}
74  
75  	/**
76  	 * Create an instance from a {@code HandlerMethod}.
77  	 */
78  	public ServletInvocableHandlerMethod(HandlerMethod handlerMethod) {
79  		super(handlerMethod);
80  		initResponseStatus();
81  	}
82  
83  	private void initResponseStatus() {
84  		ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class);
85  		if (annotation != null) {
86  			this.responseStatus = annotation.value();
87  			this.responseReason = annotation.reason();
88  		}
89  	}
90  
91  
92  	/**
93  	 * Register {@link HandlerMethodReturnValueHandler} instances to use to
94  	 * handle return values.
95  	 */
96  	public void setHandlerMethodReturnValueHandlers(HandlerMethodReturnValueHandlerComposite returnValueHandlers) {
97  		this.returnValueHandlers = returnValueHandlers;
98  	}
99  
100 	/**
101 	 * Invokes the method and handles the return value through one of the
102 	 * configured {@link HandlerMethodReturnValueHandler}s.
103 	 * @param webRequest the current request
104 	 * @param mavContainer the ModelAndViewContainer for this request
105 	 * @param providedArgs "given" arguments matched by type (not resolved)
106 	 */
107 	public void invokeAndHandle(ServletWebRequest webRequest,
108 			ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
109 
110 		Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
111 		setResponseStatus(webRequest);
112 
113 		if (returnValue == null) {
114 			if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {
115 				mavContainer.setRequestHandled(true);
116 				return;
117 			}
118 		}
119 		else if (StringUtils.hasText(this.responseReason)) {
120 			mavContainer.setRequestHandled(true);
121 			return;
122 		}
123 
124 		mavContainer.setRequestHandled(false);
125 		try {
126 			this.returnValueHandlers.handleReturnValue(
127 					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
128 		}
129 		catch (Exception ex) {
130 			if (logger.isTraceEnabled()) {
131 				logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex);
132 			}
133 			throw ex;
134 		}
135 	}
136 
137 	/**
138 	 * Set the response status according to the {@link ResponseStatus} annotation.
139 	 */
140 	private void setResponseStatus(ServletWebRequest webRequest) throws IOException {
141 		if (this.responseStatus == null) {
142 			return;
143 		}
144 		if (StringUtils.hasText(this.responseReason)) {
145 			webRequest.getResponse().sendError(this.responseStatus.value(), this.responseReason);
146 		}
147 		else {
148 			webRequest.getResponse().setStatus(this.responseStatus.value());
149 		}
150 		// to be picked up by the RedirectView
151 		webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, this.responseStatus);
152 	}
153 
154 	/**
155 	 * Does the given request qualify as "not modified"?
156 	 * @see ServletWebRequest#checkNotModified(long)
157 	 * @see ServletWebRequest#checkNotModified(String)
158 	 */
159 	private boolean isRequestNotModified(ServletWebRequest webRequest) {
160 		return webRequest.isNotModified();
161 	}
162 
163 	/**
164 	 * Does this method have the response status instruction?
165 	 */
166 	private boolean hasResponseStatus() {
167 		return (this.responseStatus != null);
168 	}
169 
170 	private String getReturnValueHandlingErrorMessage(String message, Object returnValue) {
171 		StringBuilder sb = new StringBuilder(message);
172 		if (returnValue != null) {
173 			sb.append(" [type=").append(returnValue.getClass().getName()).append("]");
174 		}
175 		sb.append(" [value=").append(returnValue).append("]");
176 		return getDetailedErrorMessage(sb.toString());
177 	}
178 
179 	/**
180 	 * Create a nested ServletInvocableHandlerMethod subclass that returns the
181 	 * the given value (or raises an Exception if the value is one) rather than
182 	 * actually invoking the controller method. This is useful when processing
183 	 * async return values (e.g. Callable, DeferredResult, ListenableFuture).
184 	 */
185 	ServletInvocableHandlerMethod wrapConcurrentResult(Object result) {
186 		return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result));
187 	}
188 
189 
190 	/**
191 	 * A nested subclass of {@code ServletInvocableHandlerMethod} that uses a
192 	 * simple {@link Callable} instead of the original controller as the handler in
193 	 * order to return the fixed (concurrent) result value given to it. Effectively
194 	 * "resumes" processing with the asynchronously produced return value.
195 	 */
196 	private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMethod {
197 
198 		private final MethodParameter returnType;
199 
200 		public ConcurrentResultHandlerMethod(final Object result, ConcurrentResultMethodParameter returnType) {
201 			super(new Callable<Object>() {
202 				@Override
203 				public Object call() throws Exception {
204 					if (result instanceof Exception) {
205 						throw (Exception) result;
206 					}
207 					else if (result instanceof Throwable) {
208 						throw new NestedServletException("Async processing failed", (Throwable) result);
209 					}
210 					return result;
211 				}
212 			}, CALLABLE_METHOD);
213 			setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
214 			this.returnType = returnType;
215 		}
216 
217 		/**
218 		 * Bridge to actual controller type-level annotations.
219 		 */
220 		@Override
221 		public Class<?> getBeanType() {
222 			return ServletInvocableHandlerMethod.this.getBeanType();
223 		}
224 
225 		/**
226 		 * Bridge to actual return value or generic type within the declared
227 		 * async return type, e.g. Foo instead of {@code DeferredResult<Foo>}.
228 		 */
229 		@Override
230 		public MethodParameter getReturnValueType(Object returnValue) {
231 			return this.returnType;
232 		}
233 
234 		/**
235 		 * Bridge to controller method-level annotations.
236 		 */
237 		@Override
238 		public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
239 			return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType);
240 		}
241 	}
242 
243 
244 	/**
245 	 * MethodParameter subclass based on the actual return value type or if
246 	 * that's null falling back on the generic type within the declared async
247 	 * return type, e.g. Foo instead of {@code DeferredResult<Foo>}.
248 	 */
249 	private class ConcurrentResultMethodParameter extends HandlerMethodParameter {
250 
251 		private final Object returnValue;
252 
253 		private final ResolvableType returnType;
254 
255 		public ConcurrentResultMethodParameter(Object returnValue) {
256 			super(-1);
257 			this.returnValue = returnValue;
258 			this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0);
259 		}
260 
261 		@Override
262 		public Class<?> getParameterType() {
263 			return (this.returnValue != null ? this.returnValue.getClass() : this.returnType.getRawClass());
264 		}
265 
266 		@Override
267 		public Type getGenericParameterType() {
268 			return this.returnType.getType();
269 		}
270 	}
271 
272 }