001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.web.servlet.mvc.multiaction;
018:
019: import java.lang.reflect.InvocationTargetException;
020: import java.lang.reflect.Method;
021: import java.util.ArrayList;
022: import java.util.HashMap;
023: import java.util.List;
024: import java.util.Map;
025:
026: import javax.servlet.ServletRequest;
027: import javax.servlet.http.HttpServletRequest;
028: import javax.servlet.http.HttpServletResponse;
029: import javax.servlet.http.HttpSession;
030:
031: import org.apache.commons.logging.Log;
032: import org.apache.commons.logging.LogFactory;
033:
034: import org.springframework.beans.BeanUtils;
035: import org.springframework.util.Assert;
036: import org.springframework.validation.ValidationUtils;
037: import org.springframework.validation.Validator;
038: import org.springframework.web.HttpSessionRequiredException;
039: import org.springframework.web.bind.ServletRequestDataBinder;
040: import org.springframework.web.servlet.ModelAndView;
041: import org.springframework.web.servlet.mvc.AbstractController;
042: import org.springframework.web.servlet.mvc.LastModified;
043: import org.springframework.web.util.NestedServletException;
044:
045: /**
046: * Controller implementation that allows multiple request types to be
047: * handled by the same class. Subclasses of this class can handle several
048: * different types of request with methods of the form
049: *
050: * <pre>
051: * (ModelAndView | Map | void) actionName(HttpServletRequest request, HttpServletResponse response);</pre>
052: *
053: * May take a third parameter HttpSession in which an existing session will be required,
054: * or a third parameter of an arbitrary class that gets treated as command
055: * (i.e. an instance of the class gets created, and request parameters get bound to it)
056: *
057: * <p>These methods can throw any kind of exception, but should only let propagate
058: * those that they consider fatal, or which their class or superclass is prepared to
059: * catch by implementing an exception handler.
060: *
061: * <p>When returning just a {@link Map} instance view name translation will be used to generate
062: * the view name. The configured {@link org.springframework.web.servlet.RequestToViewNameTranslator}
063: * will be used to determine the view name.
064: *
065: * <p>When returning <code>void</code> a return value of <code>null</code> is assumed
066: * meaning that the handler method is responsible for writing the response directly to
067: * the supplied {@link HttpServletResponse}.
068: *
069: * <p>This model allows for rapid coding, but loses the advantage of compile-time
070: * checking. It is similar to a Struts 1.1 DispatchAction, but more sophisticated.
071: * Also supports delegation to another object.
072: *
073: * <p>An implementation of the MethodNameResolver interface defined in this package
074: * should return a method name for a given request, based on any aspect of the request,
075: * such as its URL or an "action" parameter. The actual strategy can be configured
076: * via the "methodNameResolver" bean property, for each MultiActionController.
077: *
078: * <p>The default MethodNameResolver is InternalPathMethodNameResolver; further included
079: * strategies are PropertiesMethodNameResolver and ParameterMethodNameResolver.
080: *
081: * <p>Subclasses can implement custom exception handler methods with names such as:
082: *
083: * <pre>
084: * ModelAndView anyMeaningfulName(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception);</pre>
085: *
086: * The third parameter can be any subclass or Exception or RuntimeException.
087: *
088: * <p>There can also be an optional lastModified method for handlers, of signature:
089: *
090: * <pre>
091: * long anyMeaningfulNameLastModified(HttpServletRequest request)</pre>
092: *
093: * If such a method is present, it will be invoked. Default return from getLastModified
094: * is -1, meaning that the content must always be regenerated.
095: *
096: * <p>Note that method overloading isn't allowed.
097: *
098: * <p>See also the description of the workflow performed by
099: * {@link AbstractController the superclass} (in that section of the class
100: * level Javadoc entitled 'workflow').
101: *
102: * <p><b>Note:</b> For maximum data binding flexibility, consider direct usage
103: * of a ServletRequestDataBinder in your controller method, instead of relying
104: * on a declared command argument. This allows for full control over the entire
105: * binder setup and usage, including the invocation of Validators and the
106: * subsequent evaluation of binding/validation errors.
107: *
108: * @author Rod Johnson
109: * @author Juergen Hoeller
110: * @author Colin Sampaleanu
111: * @author Rob Harrop
112: * @see MethodNameResolver
113: * @see InternalPathMethodNameResolver
114: * @see PropertiesMethodNameResolver
115: * @see ParameterMethodNameResolver
116: * @see org.springframework.web.servlet.mvc.LastModified#getLastModified
117: * @see org.springframework.web.bind.ServletRequestDataBinder
118: */
119: public class MultiActionController extends AbstractController implements
120: LastModified {
121:
122: /** Suffix for last-modified methods */
123: public static final String LAST_MODIFIED_METHOD_SUFFIX = "LastModified";
124:
125: /** Default command name used for binding command objects: "command" */
126: public static final String DEFAULT_COMMAND_NAME = "command";
127:
128: /** Log category to use when no mapped handler is found for a request */
129: public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";
130:
131: /** Additional logger to use when no mapped handler is found for a request */
132: protected static final Log pageNotFoundLogger = LogFactory
133: .getLog(PAGE_NOT_FOUND_LOG_CATEGORY);
134:
135: /**
136: * Helper object that knows how to return method names from incoming requests.
137: * Can be overridden via the methodNameResolver bean property
138: */
139: private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver();
140:
141: /** List of Validators to apply to commands */
142: private Validator[] validators;
143:
144: /** Object we'll invoke methods on. Defaults to this. */
145: private Object delegate;
146:
147: /** Methods, keyed by name */
148: private Map handlerMethodMap = new HashMap();
149:
150: /** LastModified methods, keyed by handler method name (without LAST_MODIFIED_SUFFIX) */
151: private Map lastModifiedMethodMap = new HashMap();
152:
153: /** Methods, keyed by exception class */
154: private Map exceptionHandlerMap = new HashMap();
155:
156: /**
157: * Constructor for MultiActionController that looks for handler methods
158: * in the present subclass. Caches methods for quick invocation later.
159: * This class's use of reflection will impose little overhead at runtime.
160: */
161: public MultiActionController() {
162: this .delegate = this ;
163: registerHandlerMethods(this .delegate);
164: // We'll accept no handler methods found here - a delegate might be set later on.
165: }
166:
167: /**
168: * Constructor for MultiActionController that looks for handler methods in delegate,
169: * rather than a subclass of this class. Caches methods for quick invocation later.
170: * @param delegate handler object. This doesn't need to implement any particular
171: * interface, as everything is done using reflection.
172: */
173: public MultiActionController(Object delegate) {
174: setDelegate(delegate);
175: }
176:
177: /**
178: * Set the method name resolver that this class should use.
179: * Allows parameterization of handler method mappings.
180: */
181: public final void setMethodNameResolver(
182: MethodNameResolver methodNameResolver) {
183: this .methodNameResolver = methodNameResolver;
184: }
185:
186: /**
187: * Return the MethodNameResolver used by this class.
188: */
189: public final MethodNameResolver getMethodNameResolver() {
190: return this .methodNameResolver;
191: }
192:
193: /**
194: * Set the Validators for this controller.
195: * The Validator must support the specified command class.
196: */
197: public final void setValidators(Validator[] validators) {
198: this .validators = validators;
199: }
200:
201: /**
202: * Return the Validators for this controller.
203: */
204: public final Validator[] getValidators() {
205: return validators;
206: }
207:
208: /**
209: * Set the delegate used by this class. The default is <code>this</code>,
210: * assuming that handler methods have been added by a subclass.
211: * <p>This method does not get invoked once the class is configured.
212: * @param delegate an object containing handler methods
213: */
214: public final void setDelegate(Object delegate) {
215: Assert.notNull(delegate, "Delegate must not be null");
216: this .delegate = delegate;
217: registerHandlerMethods(this .delegate);
218: // There must be SOME handler methods.
219: if (this .handlerMethodMap.isEmpty()) {
220: throw new IllegalStateException(
221: "No handler methods in class ["
222: + this .delegate.getClass() + "]");
223: }
224: }
225:
226: /**
227: * Registers all handlers methods on the delegate object.
228: */
229: private void registerHandlerMethods(Object delegate) {
230: this .handlerMethodMap.clear();
231: this .lastModifiedMethodMap.clear();
232: this .exceptionHandlerMap.clear();
233:
234: // Look at all methods in the subclass, trying to find
235: // methods that are validators according to our criteria
236: Method[] methods = delegate.getClass().getMethods();
237: for (int i = 0; i < methods.length; i++) {
238: // We're looking for methods with given parameters.
239: Method method = methods[i];
240: if (isExceptionHandlerMethod(method)) {
241: registerExceptionHandlerMethod(method);
242: } else if (isHandlerMethod(method)) {
243: registerHandlerMethod(method);
244: registerLastModifiedMethodIfExists(delegate, method);
245: }
246: }
247: }
248:
249: /**
250: * Is the supplied method a valid handler method?
251: * <p>Does not consider <code>Controller.handleRequest</code> itself
252: * as handler method (to avoid potential stack overflow).
253: */
254: private boolean isHandlerMethod(Method method) {
255: Class returnType = method.getReturnType();
256: if (ModelAndView.class.equals(returnType)
257: || Map.class.equals(returnType)
258: || void.class.equals(returnType)) {
259: Class[] parameterTypes = method.getParameterTypes();
260: return (parameterTypes.length >= 2
261: && HttpServletRequest.class
262: .equals(parameterTypes[0])
263: && HttpServletResponse.class
264: .equals(parameterTypes[1]) && !("handleRequest"
265: .equals(method.getName()) && parameterTypes.length == 2));
266: }
267: return false;
268: }
269:
270: /**
271: * Is the supplied method a valid exception handler method?
272: */
273: private boolean isExceptionHandlerMethod(Method method) {
274: return (isHandlerMethod(method)
275: && method.getParameterTypes().length == 3 && Throwable.class
276: .isAssignableFrom(method.getParameterTypes()[2]));
277: }
278:
279: /**
280: * Registers the supplied method as a request handler.
281: */
282: private void registerHandlerMethod(Method method) {
283: if (logger.isDebugEnabled()) {
284: logger.debug("Found action method [" + method + "]");
285: }
286: this .handlerMethodMap.put(method.getName(), method);
287: }
288:
289: /**
290: * Registers a LastModified handler method for the supplied handler method
291: * if one exists.
292: */
293: private void registerLastModifiedMethodIfExists(Object delegate,
294: Method method) {
295: // Look for corresponding LastModified method.
296: try {
297: Method lastModifiedMethod = delegate.getClass().getMethod(
298: method.getName() + LAST_MODIFIED_METHOD_SUFFIX,
299: new Class[] { HttpServletRequest.class });
300: // Put in cache, keyed by handler method name.
301: this .lastModifiedMethodMap.put(method.getName(),
302: lastModifiedMethod);
303: if (logger.isDebugEnabled()) {
304: logger
305: .debug("Found last modified method for action method ["
306: + method + "]");
307: }
308: } catch (NoSuchMethodException ex) {
309: // No last modified method. That's ok.
310: }
311: }
312:
313: /**
314: * Registers the supplied method as an exception handler.
315: */
316: private void registerExceptionHandlerMethod(Method method) {
317: this .exceptionHandlerMap.put(method.getParameterTypes()[2],
318: method);
319: if (logger.isDebugEnabled()) {
320: logger.debug("Found exception handler method [" + method
321: + "]");
322: }
323: }
324:
325: //---------------------------------------------------------------------
326: // Implementation of LastModified
327: //---------------------------------------------------------------------
328:
329: /**
330: * Try to find an XXXXLastModified method, where XXXX is the name of a handler.
331: * Return -1, indicating that content must be updated, if there's no such handler.
332: * @see org.springframework.web.servlet.mvc.LastModified#getLastModified(HttpServletRequest)
333: */
334: public long getLastModified(HttpServletRequest request) {
335: try {
336: String handlerMethodName = this .methodNameResolver
337: .getHandlerMethodName(request);
338: Method lastModifiedMethod = (Method) this .lastModifiedMethodMap
339: .get(handlerMethodName);
340: if (lastModifiedMethod != null) {
341: try {
342: // invoke the last-modified method
343: Long wrappedLong = (Long) lastModifiedMethod
344: .invoke(this .delegate,
345: new Object[] { request });
346: return wrappedLong.longValue();
347: } catch (Exception ex) {
348: // We encountered an error invoking the last-modified method.
349: // We can't do anything useful except log this, as we can't throw an exception.
350: logger
351: .error(
352: "Failed to invoke last-modified method",
353: ex);
354: }
355: } // if we had a lastModified method for this request
356: } catch (NoSuchRequestHandlingMethodException ex) {
357: // No handler method for this request. This shouldn't happen, as this
358: // method shouldn't be called unless a previous invocation of this class
359: // has generated content. Do nothing, that's OK: We'll return default.
360: }
361: return -1L;
362: }
363:
364: //---------------------------------------------------------------------
365: // Implementation of AbstractController
366: //---------------------------------------------------------------------
367:
368: /**
369: * Determine a handler method and invoke it.
370: * @see MethodNameResolver#getHandlerMethodName
371: * @see #invokeNamedMethod
372: * @see #handleNoSuchRequestHandlingMethod
373: */
374: protected ModelAndView handleRequestInternal(
375: HttpServletRequest request, HttpServletResponse response)
376: throws Exception {
377: try {
378: String methodName = this .methodNameResolver
379: .getHandlerMethodName(request);
380: return invokeNamedMethod(methodName, request, response);
381: } catch (NoSuchRequestHandlingMethodException ex) {
382: return handleNoSuchRequestHandlingMethod(ex, request,
383: response);
384: }
385: }
386:
387: /**
388: * Handle the case where no request handler method was found.
389: * <p>The default implementation logs a warning and sends an HTTP 404 error.
390: * Alternatively, a fallback view could be chosen, or the
391: * NoSuchRequestHandlingMethodException could be rethrown as-is.
392: * @param ex the NoSuchRequestHandlingMethodException to be handled
393: * @param request current HTTP request
394: * @param response current HTTP response
395: * @return a ModelAndView to render, or <code>null</code> if handled directly
396: * @throws Exception an Exception that should be thrown as result of the servlet request
397: */
398: protected ModelAndView handleNoSuchRequestHandlingMethod(
399: NoSuchRequestHandlingMethodException ex,
400: HttpServletRequest request, HttpServletResponse response)
401: throws Exception {
402:
403: pageNotFoundLogger.warn(ex.getMessage());
404: response.sendError(HttpServletResponse.SC_NOT_FOUND);
405: return null;
406: }
407:
408: /**
409: * Invokes the named method.
410: * <p>Uses a custom exception handler if possible; otherwise, throw an
411: * unchecked exception; wrap a checked exception or Throwable.
412: */
413: protected final ModelAndView invokeNamedMethod(String methodName,
414: HttpServletRequest request, HttpServletResponse response)
415: throws Exception {
416:
417: Method method = (Method) this .handlerMethodMap.get(methodName);
418: if (method == null) {
419: throw new NoSuchRequestHandlingMethodException(methodName,
420: getClass());
421: }
422:
423: try {
424: List params = new ArrayList(4);
425: params.add(request);
426: params.add(response);
427:
428: if (method.getParameterTypes().length >= 3
429: && method.getParameterTypes()[2]
430: .equals(HttpSession.class)) {
431: HttpSession session = request.getSession(false);
432: if (session == null) {
433: throw new HttpSessionRequiredException(
434: "Pre-existing session required for handler method '"
435: + methodName + "'");
436: }
437: params.add(session);
438: }
439:
440: // If last parameter isn't of HttpSession type, it's a command.
441: if (method.getParameterTypes().length >= 3
442: && !method.getParameterTypes()[method
443: .getParameterTypes().length - 1]
444: .equals(HttpSession.class)) {
445: Object command = newCommandObject(method
446: .getParameterTypes()[method.getParameterTypes().length - 1]);
447: params.add(command);
448: bind(request, command);
449: }
450:
451: Object returnValue = method.invoke(this .delegate, params
452: .toArray(new Object[params.size()]));
453: return massageReturnValueIfNecessary(returnValue);
454: } catch (InvocationTargetException ex) {
455: // The handler method threw an exception.
456: return handleException(request, response, ex
457: .getTargetException());
458: } catch (Exception ex) {
459: // The binding process threw an exception.
460: return handleException(request, response, ex);
461: }
462: }
463:
464: /**
465: * Processes the return value of a handler method to ensure that it either returns
466: * <code>null</code> or an instance of {@link ModelAndView}. When returning a {@link Map},
467: * the {@link Map} instance is wrapped in a new {@link ModelAndView} instance.
468: */
469: private ModelAndView massageReturnValueIfNecessary(
470: Object returnValue) {
471: if (returnValue instanceof ModelAndView) {
472: return (ModelAndView) returnValue;
473: } else if (returnValue instanceof Map) {
474: return new ModelAndView().addAllObjects((Map) returnValue);
475: } else {
476: // Either returned null or was 'void' return.
477: // We'll assume that the handle method already wrote the response.
478: return null;
479: }
480: }
481:
482: /**
483: * Create a new command object of the given class.
484: * <p>This implementation uses <code>BeanUtils.instantiateClass</code>,
485: * so commands need to have public no-arg constructors.
486: * Subclasses can override this implementation if desired.
487: * @throws Exception if the command object could not be instantiated
488: * @see org.springframework.beans.BeanUtils#instantiateClass(Class)
489: */
490: protected Object newCommandObject(Class clazz) throws Exception {
491: if (logger.isDebugEnabled()) {
492: logger.debug("Must create new command of class ["
493: + clazz.getName() + "]");
494: }
495: return BeanUtils.instantiateClass(clazz);
496: }
497:
498: /**
499: * Bind request parameters onto the given command bean
500: * @param request request from which parameters will be bound
501: * @param command command object, that must be a JavaBean
502: * @throws Exception in case of invalid state or arguments
503: */
504: protected void bind(HttpServletRequest request, Object command)
505: throws Exception {
506: logger
507: .debug("Binding request parameters onto MultiActionController command");
508: ServletRequestDataBinder binder = createBinder(request, command);
509: binder.bind(request);
510: if (this .validators != null) {
511: for (int i = 0; i < this .validators.length; i++) {
512: if (this .validators[i].supports(command.getClass())) {
513: ValidationUtils.invokeValidator(this .validators[i],
514: command, binder.getBindingResult());
515: }
516: }
517: }
518: binder.closeNoCatch();
519: }
520:
521: /**
522: * Create a new binder instance for the given command and request.
523: * <p>Called by <code>bind</code>. Can be overridden to plug in custom
524: * ServletRequestDataBinder subclasses.
525: * <p>Default implementation creates a standard ServletRequestDataBinder,
526: * and invokes <code>initBinder</code>. Note that <code>initBinder</code>
527: * will not be invoked if you override this method!
528: * @param request current HTTP request
529: * @param command the command to bind onto
530: * @return the new binder instance
531: * @throws Exception in case of invalid state or arguments
532: * @see #bind
533: * @see #initBinder
534: */
535: protected ServletRequestDataBinder createBinder(
536: HttpServletRequest request, Object command)
537: throws Exception {
538:
539: ServletRequestDataBinder binder = new ServletRequestDataBinder(
540: command, getCommandName(command));
541: initBinder(request, binder);
542: return binder;
543: }
544:
545: /**
546: * Return the command name to use for the given command object.
547: * Default is "command".
548: * @param command the command object
549: * @return the command name to use
550: * @see #DEFAULT_COMMAND_NAME
551: */
552: protected String getCommandName(Object command) {
553: return DEFAULT_COMMAND_NAME;
554: }
555:
556: /**
557: * Initialize the given binder instance, for example with custom editors.
558: * Called by <code>createBinder</code>.
559: * <p>This method allows you to register custom editors for certain fields of your
560: * command class. For instance, you will be able to transform Date objects into a
561: * String pattern and back, in order to allow your JavaBeans to have Date properties
562: * and still be able to set and display them in an HTML interface.
563: * <p>Default implementation is empty.
564: * <p>Note: the command object is not directly passed to this method, but it's available
565: * via {@link org.springframework.validation.DataBinder#getTarget()}
566: * @param request current HTTP request
567: * @param binder new binder instance
568: * @throws Exception in case of invalid state or arguments
569: * @see #createBinder
570: * @see org.springframework.validation.DataBinder#registerCustomEditor
571: * @see org.springframework.beans.propertyeditors.CustomDateEditor
572: */
573: protected void initBinder(HttpServletRequest request,
574: ServletRequestDataBinder binder) throws Exception {
575:
576: initBinder((ServletRequest) request, binder);
577: }
578:
579: /**
580: * Initialize the given binder instance, for example with custom editors.
581: * @deprecated since Spring 2.0:
582: * use <code>initBinder(HttpServletRequest, ServletRequestDataBinder) instead
583: */
584: protected void initBinder(ServletRequest request,
585: ServletRequestDataBinder binder) throws Exception {
586: }
587:
588: /**
589: * Determine the exception handler method for the given exception.
590: * Can return null if not found.
591: * @return a handler for the given exception type, or <code>null</code>
592: * @param exception the exception to handle
593: */
594: protected Method getExceptionHandler(Throwable exception) {
595: Class exceptionClass = exception.getClass();
596: if (logger.isDebugEnabled()) {
597: logger.debug("Trying to find handler for exception class ["
598: + exceptionClass.getName() + "]");
599: }
600: Method handler = (Method) this .exceptionHandlerMap
601: .get(exceptionClass);
602: while (handler == null
603: && !exceptionClass.equals(Throwable.class)) {
604: if (logger.isDebugEnabled()) {
605: logger
606: .debug("Trying to find handler for exception superclass ["
607: + exceptionClass.getName() + "]");
608: }
609: exceptionClass = exceptionClass.getSuperclass();
610: handler = (Method) this .exceptionHandlerMap
611: .get(exceptionClass);
612: }
613: return handler;
614: }
615:
616: /**
617: * We've encountered an exception which may be recoverable
618: * (InvocationTargetException or HttpSessionRequiredException).
619: * Allow the subclass a chance to handle it.
620: * @param request current HTTP request
621: * @param response current HTTP response
622: * @param ex the exception that got thrown
623: * @return a ModelAndView to render the response
624: */
625: private ModelAndView handleException(HttpServletRequest request,
626: HttpServletResponse response, Throwable ex)
627: throws Exception {
628:
629: Method handler = getExceptionHandler(ex);
630: if (handler != null) {
631: return invokeExceptionHandler(handler, request, response,
632: ex);
633: }
634: // If we get here, there was no custom handler
635: if (ex instanceof Exception) {
636: throw (Exception) ex;
637: }
638: if (ex instanceof Error) {
639: throw (Error) ex;
640: }
641: // Should never happen!
642: throw new NestedServletException(
643: "Unknown Throwable type encountered", ex);
644: }
645:
646: /**
647: * Invoke the selected exception handler.
648: * @param handler handler method to invoke
649: */
650: private ModelAndView invokeExceptionHandler(Method handler,
651: HttpServletRequest request, HttpServletResponse response,
652: Throwable ex) throws Exception {
653:
654: if (handler == null) {
655: throw new NestedServletException(
656: "No handler for exception", ex);
657: }
658:
659: // If we get here, we have a handler.
660: if (logger.isDebugEnabled()) {
661: logger.debug("Invoking exception handler [" + handler
662: + "] for exception [" + ex + "]");
663: }
664: try {
665: Object returnValue = handler.invoke(this .delegate,
666: new Object[] { request, response, ex });
667: return massageReturnValueIfNecessary(returnValue);
668: } catch (InvocationTargetException ex2) {
669: Throwable targetEx = ex2.getTargetException();
670: if (targetEx instanceof Exception) {
671: throw (Exception) targetEx;
672: }
673: if (targetEx instanceof Error) {
674: throw (Error) targetEx;
675: }
676: // Should never happen!
677: throw new NestedServletException(
678: "Unknown Throwable type encountered", targetEx);
679: }
680: }
681:
682: }
|