/* Copyright 2005-2006 Tim Fennell * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.sourceforge.stripes.controller; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.After; import net.sourceforge.stripes.action.Before; import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.ReflectUtil; import net.sourceforge.stripes.util.CollectionUtil; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; /** * <p>Interceptor that inspects ActionBeans for {@link Before} and {@link After} annotations and * runs the annotated methods at the requested point in the request lifecycle. There is no limit * on the number of methods within an ActionBean that can be marked with {@code @Before} and * {@code @After} annotations, and individual methods may be marked with one or both annotations.</p> * * <p>To configure the BeforeAfterMethodInterceptor for use you will need to add the following to * your {@code web.xml} (assuming no other interceptors are yet configured):</p> * * <pre> * <init-param> * <param-name>Interceptor.Classes</param-name> * <param-value>net.sourceforge.stripes.controller.BeforeAfterMethodInterceptor</param-value> * </init-param> * </pre> * * <p>If one or more interceptors are already configured in your {@code web.xml} simply separate * the fully qualified names of the interceptors with commas (additional whitespace is ok).</p> * * @see net.sourceforge.stripes.action.Before * @see net.sourceforge.stripes.action.After * @author Jeppe Cramon * @since Stripes 1.3 */ @Intercepts({LifecycleStage.RequestInit, LifecycleStage.ActionBeanResolution, LifecycleStage.HandlerResolution, LifecycleStage.BindingAndValidation, LifecycleStage.CustomValidation, LifecycleStage.EventHandling, LifecycleStage.ResolutionExecution, LifecycleStage.RequestComplete}) public class BeforeAfterMethodInterceptor implements Interceptor { /** Log used throughout the intercetor */ private static final Log log = Log.getInstance(BeforeAfterMethodInterceptor.class); /** Cache of the FilterMethods for the different ActionBean classes */ private Map<Class<? extends ActionBean>, FilterMethods> filterMethodsCache = new ConcurrentHashMap<Class<? extends ActionBean>, FilterMethods>(); /** * Does the main work of the interceptor as described in the class level javadoc. * Executed the before and after methods for the ActionBean as appropriate for the * current lifecycle stage. Lazily examines the ActionBean to determine the set * of methods to execute, if it has not yet been examined. * * @param context the current ExecutionContext * @return a resolution if one of the Before or After methods returns one, or if the * nested interceptors return one * @throws Exception if one of the before/after methods raises an exception */ public Resolution intercept(ExecutionContext context) throws Exception { LifecycleStage stage = context.getLifecycleStage(); ActionBeanContext abc = context.getActionBeanContext(); String event = abc == null ? null : abc.getEventName(); Resolution resolution; // Run @Before methods, as long as there's a bean to run them on if (context.getActionBean() != null) { ActionBean bean = context.getActionBean(); FilterMethods filterMethods = getFilterMethods(bean.getClass()); List<Method> beforeMethods = filterMethods.getBeforeMethods(stage); for (Method method : beforeMethods) { String[] on = method.getAnnotation(Before.class).on(); if (event == null || CollectionUtil.applies(on, event)) { resolution = invoke(bean, method, stage, Before.class); if (resolution != null) { return resolution; } } } } // Continue on and execute other filters and the lifecycle code resolution = context.proceed(); // Run After filter methods (if any) if (context.getActionBean() != null) { ActionBean bean = context.getActionBean(); FilterMethods filterMethods = getFilterMethods(bean.getClass()); List<Method> afterMethods = filterMethods.getAfterMethods(stage); // Re-get the event name in case we're executing after handler resolution // in which case the name will have been null before, and non-null now event = abc == null ? null : abc.getEventName(); Resolution overrideResolution; for (Method method : afterMethods) { String[] on = method.getAnnotation(After.class).on(); if (event == null || CollectionUtil.applies(on, event)) { overrideResolution = invoke(bean, method, stage, After.class); if (overrideResolution != null) { return overrideResolution; } } } } return resolution; } /** * Helper method that will invoke the supplied method and manage any exceptions and * returns from the object. Specifically it will log any exceptions except for * InvocationTargetExceptions which it will attempt to unwrap and rethrow. If the method * returns a Resolution it will be returned; returns of other types will be ignored. */ protected Resolution invoke(ActionBean bean, Method m, LifecycleStage stage, Class<? extends Annotation> when) throws Exception { Class<? extends ActionBean> beanClass = bean.getClass(); Object retval = null; log.debug("Calling @", when.getSimpleName(), " method '", m.getName(), "' at LifecycleStage '", stage, "' on ActionBean '", beanClass.getSimpleName(), "'"); try { retval = m.invoke(bean); } catch (IllegalArgumentException e) { log.error(e, "An InvalidArgumentException was raised when calling @", when.getSimpleName(), " method '", m.getName(), "' at LifecycleStage '", stage, "' on ActionBean '", beanClass.getSimpleName(), "'. See java.lang.reflect.Method.invoke() for possible reasons."); } catch (IllegalAccessException e) { log.error(e, "An IllegalAccessException was raised when calling @", when.getSimpleName(), " method '", m.getName(), "' at LifecycleStage '", stage, "' on ActionBean '", beanClass.getSimpleName(), "'"); } catch (InvocationTargetException e) { // Method threw an exception, so throw the real cause of it if (e.getCause() != null && e.getCause() instanceof Exception) { throw (Exception)e.getCause(); } else { throw e; } } // If we got a return value and it is a resolution, return it if (retval != null && retval instanceof Resolution) { return (Resolution) retval; } else { return null; } } /** * Gets the Before/After methods for the ActionBean. Lazily examines the ActionBean * and stores the information in a cache. Looks for all non-abstract, no-arg methods * that are annotated with either {@code @Before} or {@code @After}. * * @param beanClass The action bean class to get methods for. * @return The before and after methods for the ActionBean */ protected FilterMethods getFilterMethods(Class<? extends ActionBean> beanClass) { FilterMethods filterMethods = filterMethodsCache.get(beanClass); if (filterMethods == null) { filterMethods = new FilterMethods(); // Look for @Before and @After annotations on the methods in the ActionBean class Collection<Method> methods = ReflectUtil.getMethods(beanClass); for (Method method : methods) { if (method.isAnnotationPresent(Before.class) || method.isAnnotationPresent(After.class)) { // Check to ensure that the method has an appropriate signature int mods = method.getModifiers(); if (method.getParameterTypes().length != 0 || Modifier.isAbstract(mods)) { log.warn("Method '", beanClass.getName(), ".", method.getName(), "' is ", "annotated with @Before or @After but has an incompatible ", "signature. @Before/@After methods must be non-abstract ", "zero-argument methods."); continue; } // Now try and make private/protected/package methods callable if (!method.isAccessible()) { try { method.setAccessible(true); } catch (SecurityException se) { log.warn("Method '", beanClass.getName(), ".", method.getName(), "' is ", "annotated with @Before or @After but is not public and ", "calling setAccessible(true) on it threw a SecurityException. ", "Please either declare the method as public, or change your ", "JVM security policy to allow Stripes code to call ", "Method.setAccessible() on your code base."); continue; } } if (method.isAnnotationPresent(Before.class)) { Before annotation = method.getAnnotation(Before.class); filterMethods.addBeforeMethod(annotation.stages(), method); } if (method.isAnnotationPresent(After.class)) { After annotation = method.getAnnotation(After.class); filterMethods.addAfterMethod(annotation.stages(), method); } } } filterMethodsCache.put(beanClass, filterMethods); } return filterMethods; } /** * Helper class used to collect Before and After methods for a class and provide easy * and rapid access to them by LifecycleStage. * * @author Jeppe Cramon */ protected static class FilterMethods { /** Map of Before methods, keyed by the LifecycleStage that they should be invoked before. */ private Map<LifecycleStage, List<Method>> beforeMethods = new HashMap<LifecycleStage, List<Method>>(); /** Map of After methods, keyed by the LifecycleStage that they should be invoked after. */ private Map<LifecycleStage, List<Method>> afterMethods = new HashMap<LifecycleStage, List<Method>>(); /** * Adds a method to be executed before the supplied LifecycleStages. * * @param stages All the LifecycleStages that the given filter method should be invoked before * @param method The filter method to be invoked before the given LifecycleStage(s) */ public void addBeforeMethod(LifecycleStage[] stages, Method method) { for (LifecycleStage stage : stages) { if (stage == LifecycleStage.ActionBeanResolution) { log.warn("LifecycleStage.ActionBeanResolution is unsupported for @Before ", "methods. Method '", method.getDeclaringClass().getName(), ".", method.getName(), "' will not be invoked for this stage."); } else { addFilterMethod(beforeMethods, stage, method); } } } /** * Adds a method to be executed after the supplied LifecycleStages. * * @param stages All the LifecycleStages that the given filter method should be invoked after * @param method The filter method to be invoked after the given LifecycleStage(s) */ public void addAfterMethod(LifecycleStage[] stages, Method method) { for (LifecycleStage stage : stages) { addFilterMethod(afterMethods, stage, method); } } /** * Helper method to add methods to a method map keyed by the LifecycleStage. * * @param methodMap The map of methods * @param stage The LifecycleStage under which to put the method * @param method The method that should be added to the method map */ private void addFilterMethod(Map<LifecycleStage, List<Method>> methodMap, LifecycleStage stage, Method method) { List<Method> methods = methodMap.get(stage); if (methods == null) { methods = new ArrayList<Method>(); methodMap.put(stage, methods); } methods.add(method); } /** * Gets the Before methods for the given LifecycleStage. * * @param stage The LifecycleStage to find Before methods for. * @return A List of before methods, possibly zero length but never null */ public List<Method> getBeforeMethods(LifecycleStage stage) { List<Method> methods = beforeMethods.get(stage); if (methods == null) methods = Collections.emptyList(); return methods; } /** * Gets the Before methods for the given LifecycleStage. * * @param stage The LifecycleStage to find Before methods for. * @return A List of before methods, possibly zero length but never null */ public List<Method> getAfterMethods(LifecycleStage stage) { List<Method> methods = afterMethods.get(stage); if (methods == null) methods = Collections.emptyList(); return methods; } } }