1. /*
  2. * Copyright 2002-2004 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. package org.springframework.web.servlet.mvc.multiaction;
  17. import java.lang.reflect.InvocationTargetException;
  18. import java.lang.reflect.Method;
  19. import java.util.ArrayList;
  20. import java.util.HashMap;
  21. import java.util.List;
  22. import java.util.Map;
  23. import javax.servlet.ServletException;
  24. import javax.servlet.ServletRequest;
  25. import javax.servlet.http.HttpServletRequest;
  26. import javax.servlet.http.HttpServletResponse;
  27. import javax.servlet.http.HttpSession;
  28. import org.springframework.context.ApplicationContextException;
  29. import org.springframework.web.bind.ServletRequestDataBinder;
  30. import org.springframework.web.servlet.ModelAndView;
  31. import org.springframework.web.servlet.mvc.AbstractController;
  32. import org.springframework.web.servlet.mvc.LastModified;
  33. import org.springframework.web.servlet.mvc.SessionRequiredException;
  34. /**
  35. * Controller implementation that allows multiple request types to be
  36. * handled by the same class. Subclasses of this class can handle several
  37. * different types of request with methods of the form
  38. *
  39. * <pre>
  40. * ModelAndView actionName(HttpServletRequest request, HttpServletResponse response);</pre>
  41. *
  42. * May take a third parameter HttpSession in which an existing session will be required,
  43. * or a third parameter of an arbitrary class that gets treated as command
  44. * (i.e. an instance of the class gets created, and request parameters get bound to it)
  45. *
  46. * <p>These methods can throw any kind of exception, but should only let propagate
  47. * those that they consider fatal, or which their class or superclass is prepared to
  48. * catch by implementing an exception handler.
  49. *
  50. * <p>This model allows for rapid coding, but loses the advantage of compile-time
  51. * checking. It is similar to a Struts 1.1 DispatchAction, but more sophisticated.
  52. * Also supports delegation to another object.
  53. *
  54. * <p>An implementation of the MethodNameResolver interface defined in this package
  55. * should return a method name for a given request, based on any aspect of the request,
  56. * such as its URL or an "action" parameter. The actual strategy can be configured
  57. * via the "methodNameResolver" bean property, for each MultiActionController.
  58. *
  59. * <p>The default MethodNameResolver is InternalPathMethodNameResolver; further included
  60. * strategies are PropertiesMethodNameResolver and ParameterMethodNameResolver.
  61. *
  62. * <p>Subclasses can implement custom exception handler methods with names such as:
  63. *
  64. * <pre>
  65. * ModelAndView anyMeaningfulName(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception);</pre>
  66. *
  67. * The third parameter can be any subclass or Exception or RuntimeException.
  68. *
  69. * <p>There can also be an optional lastModified method for handlers, of signature:
  70. *
  71. * <pre>
  72. * long anyMeaningfulNameLastModified(HttpServletRequest request)</pre>
  73. *
  74. * If such a method is present, it will be invoked. Default return from getLastModified
  75. * is -1, meaning that the content must always be regenerated.
  76. *
  77. * <p>Note that method overloading isn't allowed.
  78. *
  79. * @author Rod Johnson
  80. * @author Juergen Hoeller
  81. * @see MethodNameResolver
  82. * @see InternalPathMethodNameResolver
  83. * @see PropertiesMethodNameResolver
  84. * @see ParameterMethodNameResolver
  85. * @see org.springframework.web.servlet.mvc.LastModified#getLastModified
  86. */
  87. public class MultiActionController extends AbstractController implements LastModified {
  88. /** Prefix for last modified methods */
  89. public static final String LAST_MODIFIED_METHOD_SUFFIX = "LastModified";
  90. //---------------------------------------------------------------------
  91. // Instance data
  92. //---------------------------------------------------------------------
  93. /**
  94. * Helper object that knows how to return method names from incoming requests.
  95. * Can be overridden via the methodNameResolver bean property
  96. */
  97. private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver();
  98. /** Object we'll invoke methods on. Defaults to this. */
  99. private Object delegate;
  100. /** Methods, keyed by name */
  101. private Map methodHash;
  102. /** LastModified methods, keyed by handler method name (without LAST_MODIFIED_SUFFIX) */
  103. private Map lastModifiedMethodHash;
  104. /** Methods, keyed by exception class */
  105. private Map exceptionHandlerHash;
  106. //---------------------------------------------------------------------
  107. // Constructors
  108. //---------------------------------------------------------------------
  109. /**
  110. * Constructor for MultiActionController that looks for handler methods
  111. * in the present subclass.Caches methods for quick invocation later.
  112. * This class's use of reflection will impose little overhead at runtime.
  113. * @throws ApplicationContextException if the class doesn't contain any
  114. * action handler methods (and so could never handle any requests).
  115. */
  116. public MultiActionController() throws ApplicationContextException {
  117. setDelegate(this);
  118. }
  119. /**
  120. * Constructor for MultiActionController that looks for handler methods in delegate,
  121. * rather than a subclass of this class. Caches methods.
  122. * @param delegate handler class. This doesn't need to implement any particular
  123. * interface, as everything is done using reflection.
  124. * @throws ApplicationContextException if the class doesn't contain any handler methods
  125. */
  126. public MultiActionController(Object delegate) throws ApplicationContextException {
  127. setDelegate(delegate);
  128. }
  129. //---------------------------------------------------------------------
  130. // Bean properties
  131. //---------------------------------------------------------------------
  132. /**
  133. * Set the method name resolver used by this class.
  134. * Allows parameterization of mappings.
  135. * @param methodNameResolver the method name resolver used by this class
  136. */
  137. public final void setMethodNameResolver(MethodNameResolver methodNameResolver) {
  138. this.methodNameResolver = methodNameResolver;
  139. }
  140. /**
  141. * Get the MethodNameResolver used by this class
  142. * @return MethodNameResolver the method name resolver used by this class
  143. */
  144. public final MethodNameResolver getMethodNameResolver() {
  145. return this.methodNameResolver;
  146. }
  147. /**
  148. * Set the delegate used by this class. The default is
  149. * "this", assuming that handler methods have been added
  150. * by a subclass. This method is rarely invoked once
  151. * the class is configured.
  152. * @param delegate class containing methods, which may
  153. * be the present class, the handler methods being in a subclass
  154. * @throws ApplicationContextException if there aren't
  155. * any valid request handling methods in the subclass.
  156. */
  157. public final void setDelegate(Object delegate) throws ApplicationContextException {
  158. if (delegate == null) {
  159. throw new IllegalArgumentException("Delegate cannot be null in MultiActionController");
  160. }
  161. this.delegate = delegate;
  162. this.methodHash = new HashMap();
  163. this.lastModifiedMethodHash = new HashMap();
  164. // Look at all methods in the subclass, trying to find
  165. // methods that are validators according to our criteria
  166. Method[] methods = delegate.getClass().getMethods();
  167. for (int i = 0; i < methods.length; i++) {
  168. // we're looking for methods with given parameters
  169. if (methods[i].getReturnType().equals(ModelAndView.class)) {
  170. // we have a potential handler method, with the correct return type
  171. Class[] params = methods[i].getParameterTypes();
  172. // Check that the number and types of methods is correct.
  173. // We don't care about the declared exceptions
  174. if (params.length >= 2 && params[0].equals(HttpServletRequest.class) &&
  175. params[1].equals(HttpServletResponse.class)) {
  176. // we're in business
  177. logger.info("Found action method [" + methods[i] + "]");
  178. this.methodHash.put(methods[i].getName(), methods[i]);
  179. // look for corresponding LastModified method
  180. try {
  181. Method lastModifiedMethod = delegate.getClass().getMethod(methods[i].getName() + LAST_MODIFIED_METHOD_SUFFIX,
  182. new Class[] { HttpServletRequest.class } );
  183. // put in cache, keyed by handler method name
  184. this.lastModifiedMethodHash.put(methods[i].getName(), lastModifiedMethod);
  185. logger.info("Found last modified method for action method [" + methods[i] + "]");
  186. }
  187. catch (NoSuchMethodException ex) {
  188. // No last modified method. That's ok.
  189. }
  190. }
  191. }
  192. }
  193. // There must be SOME handler methods.
  194. // WHAT IF SETTING DELEGATE LATER!?
  195. if (this.methodHash.isEmpty()) {
  196. throw new ApplicationContextException("No handler methods in class " + getClass().getName());
  197. }
  198. // now look for exception handlers
  199. this.exceptionHandlerHash = new HashMap();
  200. for (int i = 0; i < methods.length; i++) {
  201. if (methods[i].getReturnType().equals(ModelAndView.class) &&
  202. methods[i].getParameterTypes().length == 3) {
  203. Class[] params = methods[i].getParameterTypes();
  204. if (params[0].equals(HttpServletRequest.class) &&
  205. params[1].equals(HttpServletResponse.class) &&
  206. Throwable.class.isAssignableFrom(params[2])
  207. ) {
  208. // Have an exception handler
  209. this.exceptionHandlerHash.put(params[2], methods[i]);
  210. logger.info("Found exception handler method [" + methods[i] + "]");
  211. }
  212. }
  213. }
  214. }
  215. //---------------------------------------------------------------------
  216. // Implementation of LastModified
  217. //---------------------------------------------------------------------
  218. /**
  219. * Try to find an XXXXLastModified method, where XXXX is the name of a handler.
  220. * Return -1, indicating that content must be updated, if there's no such handler.
  221. * @see org.springframework.web.servlet.mvc.LastModified#getLastModified(HttpServletRequest)
  222. */
  223. public final long getLastModified(HttpServletRequest request) {
  224. try {
  225. String handlerMethodName = methodNameResolver.getHandlerMethodName(request);
  226. Method lastModifiedMethod = (Method) this.lastModifiedMethodHash.get(handlerMethodName);
  227. if (lastModifiedMethod != null) {
  228. try {
  229. // Invoke the LastModified method
  230. Long wrappedLong = (Long) lastModifiedMethod.invoke(this.delegate, new Object[] { request });
  231. return wrappedLong.longValue();
  232. }
  233. catch (Exception ex) {
  234. // We encountered an error invoking the lastModified method
  235. // We can't do anything useful except log this, as we can't throw an exception
  236. logger.error("Failed to invoke lastModified method", ex);
  237. }
  238. } // if we had a lastModified method for this request
  239. }
  240. catch (NoSuchRequestHandlingMethodException ex) {
  241. // No handler method for this request. This shouldn't
  242. // happen, as this method shouldn't be called unless a previous invocation
  243. // of this class has generated content.
  244. // Do nothing, that's ok: we'll return default
  245. }
  246. // The default if we didn't find a method
  247. return -1L;
  248. }
  249. //---------------------------------------------------------------------
  250. // Implementation of Controller
  251. //---------------------------------------------------------------------
  252. protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
  253. throws Exception {
  254. String name = this.methodNameResolver.getHandlerMethodName(request);
  255. return invokeNamedMethod(name, request, response);
  256. }
  257. /**
  258. * Invoke the named method.
  259. * Use a custom exception handler if possible;
  260. * otherwise, throw an unchecked exception;
  261. * wrap a checked exception or Throwable
  262. */
  263. protected final ModelAndView invokeNamedMethod(String method, HttpServletRequest request, HttpServletResponse response)
  264. throws Exception {
  265. Method m = (Method) this.methodHash.get(method);
  266. if (m == null) {
  267. throw new NoSuchRequestHandlingMethodException(method, this);
  268. }
  269. try {
  270. // A real generic Collection! Parameters to method.
  271. List params = new ArrayList(4);
  272. params.add(request);
  273. params.add(response);
  274. if (m.getParameterTypes().length >= 3 && m.getParameterTypes()[2].equals(HttpSession.class) ){
  275. HttpSession session = request.getSession(false);
  276. if (session == null) {
  277. return handleException(request, response,
  278. new SessionRequiredException("Session was required for method '" + method + "'"));
  279. }
  280. params.add(session);
  281. }
  282. // If last parameter isn't of HttpSession type, it's a command.
  283. if (m.getParameterTypes().length >= 3 &&
  284. !m.getParameterTypes()[m.getParameterTypes().length - 1].equals(HttpSession.class)) {
  285. Object command = newCommandObject(m.getParameterTypes()[m.getParameterTypes().length - 1]);
  286. params.add(command);
  287. bind(request, command);
  288. }
  289. return (ModelAndView) m.invoke(this.delegate, params.toArray(new Object[params.size()]));
  290. }
  291. catch (InvocationTargetException ex) {
  292. // This is what we're looking for: the handler method threw an exception
  293. Throwable t = ex.getTargetException();
  294. return handleException(request, response, t);
  295. }
  296. }
  297. /**
  298. * We've encountered an exception which may be recoverable
  299. * (InvocationTargetException or SessionRequiredException).
  300. * Allow the subclass a chance to handle it.
  301. * @param request current HTTP request
  302. * @param response current HTTP response
  303. * @param t the exception that got thrown
  304. * @return a ModelAndView to render the response
  305. */
  306. private ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Throwable t)
  307. throws Exception {
  308. Method handler = getExceptionHandler(t);
  309. if (handler != null) {
  310. return invokeExceptionHandler(handler, request, response, t);
  311. }
  312. // If we get here, there was no custom handler
  313. if (t instanceof Exception) {
  314. throw (Exception) t;
  315. }
  316. if (t instanceof Error) {
  317. throw (Error) t;
  318. }
  319. // Shouldn't happen
  320. throw new ServletException("Unknown Throwable type encountered: " + t);
  321. }
  322. /**
  323. * Create a new command object of the given class.
  324. * Subclasses can override this implementation if they want.
  325. * This implementation uses class.newInstance(), so commands need to have
  326. * public no arg constructors.
  327. */
  328. protected Object newCommandObject(Class clazz) throws ServletException {
  329. logger.info("Must create new command of " + clazz);
  330. try {
  331. Object command = clazz.newInstance();
  332. return command;
  333. }
  334. catch (Exception ex) {
  335. throw new ServletException("Cannot instantiate command " + clazz + "; does it have a public no arg constructor?", ex);
  336. }
  337. }
  338. /**
  339. * Bind request parameters onto the given command bean
  340. * @param request request from which parameters will be bound
  341. * @param command command object, that must be a JavaBean
  342. */
  343. protected void bind(ServletRequest request, Object command) throws ServletException {
  344. logger.info("Binding request parameters onto command");
  345. ServletRequestDataBinder binder = new ServletRequestDataBinder(command, "command");
  346. binder.bind(request);
  347. binder.closeNoCatch();
  348. }
  349. /**
  350. * Can return null if not found.
  351. * @return a handler for the given exception type
  352. * @param exception Won't be a ServletException or IOException
  353. */
  354. protected Method getExceptionHandler(Throwable exception) {
  355. Class exceptionClass = exception.getClass();
  356. logger.info("Trying to find handler for exception of " + exceptionClass);
  357. Method handler = (Method) this.exceptionHandlerHash.get(exceptionClass);
  358. while (handler == null && !exceptionClass.equals(Throwable.class)) {
  359. logger.info("Looking at superclass " + exceptionClass);
  360. exceptionClass = exceptionClass.getSuperclass();
  361. handler = (Method) this.exceptionHandlerHash.get(exceptionClass);
  362. }
  363. return handler;
  364. }
  365. /**
  366. * Invoke the selected exception handler.
  367. * @param handler handler method to invoke
  368. */
  369. private ModelAndView invokeExceptionHandler(Method handler, HttpServletRequest request,
  370. HttpServletResponse response, Throwable exception) throws Exception {
  371. if (handler == null) {
  372. throw new ServletException("No handler for exception", exception);
  373. }
  374. // If we get here, we have a handler
  375. logger.info("Invoking exception handler [" + handler + "] for exception [" + exception + "]");
  376. try {
  377. ModelAndView mv = (ModelAndView) handler.invoke(this.delegate, new Object[] { request, response, exception });
  378. return mv;
  379. }
  380. catch (InvocationTargetException ex) {
  381. Throwable t = ex.getTargetException();
  382. if (t instanceof Exception) {
  383. throw (Exception) t;
  384. }
  385. if (t instanceof Error) {
  386. throw (Error) t;
  387. }
  388. // Shouldn't happen
  389. throw new ServletException("Unknown Throwable type encountered: " + t);
  390. }
  391. }
  392. }