View Javadoc

    1   package org.apache.turbine.modules;
    2
    3   /*
    4    * Licensed to the Apache Software Foundation (ASF) under one
    5    * or more contributor license agreements.  See the NOTICE file
    6    * distributed with this work for additional information
    7    * regarding copyright ownership.  The ASF licenses this file
    8    * to you under the Apache License, Version 2.0 (the
    9    * "License"); you may not use this file except in compliance
    10   * with the License.  You may obtain a copy of the License at
    11   *
    12   *   http://www.apache.org/licenses/LICENSE-2.0
    13   *
    14   * Unless required by applicable law or agreed to in writing,
    15   * software distributed under the License is distributed on an
    16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    17   * KIND, either express or implied.  See the License for the
    18   * specific language governing permissions and limitations
    19   * under the License.
    20   */
    21
    22  import java.lang.reflect.InvocationTargetException;
    23  import java.lang.reflect.Method;
    24  import java.util.Arrays;
    25
    26  import org.apache.commons.collections.map.MultiKeyMap;
    27  import org.apache.commons.lang.StringUtils;
    28  import org.apache.commons.logging.Log;
    29  import org.apache.commons.logging.LogFactory;
    30  import org.apache.fulcrum.parser.ParameterParser;
    31  import org.apache.fulcrum.parser.ValueParser.URLCaseFolding;
    32  import org.apache.turbine.Turbine;
    33  import org.apache.turbine.TurbineConstants;
    34  import org.apache.turbine.annotation.TurbineActionEvent;
    35  import org.apache.turbine.pipeline.PipelineData;
    36
    37  /**
    38   * <p>
    39   *
    40   * This is an alternative to the Action class that allows you to do
    41   * event based actions. Essentially, you label all your submit buttons
    42   * with the prefix of "eventSubmit_" and the suffix of "methodName".
    43   * For example, "eventSubmit_doDelete". Then any class that subclasses
    44   * this class will get its "doDelete(PipelineData data)" method executed.
    45   * If for any reason, it was not able to execute the method, it will
    46   * fall back to executing the doPerform() method which is required to
    47   * be implemented.
    48   *
    49   * <p>
    50   *
    51   * Limitations:
    52   *
    53   * <p>
    54   *
    55   * Because ParameterParser makes all the key values lowercase, we have
    56   * to do some work to format the string into a method name. For
    57   * example, a button name eventSubmit_doDelete gets converted into
    58   * eventsubmit_dodelete. Thus, we need to form some sort of naming
    59   * convention so that dodelete can be turned into doDelete.
    60   *
    61   * <p>
    62   *
    63   * Thus, the convention is this:
    64   *
    65   * <ul>
    66   * <li>The variable name MUST have the prefix "eventSubmit_".</li>
    67   * <li>The variable name after the prefix MUST begin with the letters
    68   * "do".</li>
    69   * <li>The first letter after the "do" will be capitalized and the
    70   * rest will be lowercase</li>
    71   * </ul>
    72   *
    73   * If you follow these conventions, then you should be ok with your
    74   * method naming in your Action class.
    75   *
    76   * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens </a>
    77   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    78   * @author <a href="quintonm@bellsouth.net">Quinton McCombs</a>
    79   * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
    80   * @version $Id: ActionEvent.java 1706425 2015-10-02 14:47:51Z tv $
    81   */
    82  public abstract class ActionEvent extends Action
    83  {
    84  	/** Logging */
    85  	protected Log log = LogFactory.getLog(this.getClass());
    86
    87  	/** The name of the button to look for. */
    88  	protected static final String BUTTON = "eventSubmit_";
    89  	/** The length of the button to look for. */
    90  	protected static final int BUTTON_LENGTH = BUTTON.length();
    91      /** The default method. */
    92      protected static final String DEFAULT_METHOD = "doPerform";
    93  	/** The prefix of the method name. */
    94  	protected static final String METHOD_NAME_PREFIX = "do";
    95  	/** The length of the method name. */
    96  	protected static final int METHOD_NAME_LENGTH = METHOD_NAME_PREFIX.length();
    97  	/** The length of the button to look for. */
    98  	protected static final int LENGTH = BUTTON.length();
    99
    100 	/**
    101 	 * If true, the eventSubmit_do<xxx> variable must contain
    102 	 * a not null value to be executed.
    103 	 */
    104 	private boolean submitValueKey = false;
    105
    106 	/**
    107 	 * If true, then exceptions raised in eventSubmit_do<xxx> methods
    108 	 * as well as in doPerform methods are bubbled up to the Turbine
    109 	 * servlet's handleException method.
    110 	 */
    111 	protected boolean bubbleUpException = true;
    112
    113 	/**
    114 	 * Cache for the methods to invoke
    115 	 */
    116 	private MultiKeyMap/* <String, Method> */ methodCache = new MultiKeyMap/* <String, Method> */();
    117
    118 	/**
    119 	 * C'tor
    120 	 */
    121 	public ActionEvent()
    122 	{
    123 		super();
    124
    125 		submitValueKey = Turbine.getConfiguration()
    126 				.getBoolean(TurbineConstants.ACTION_EVENTSUBMIT_NEEDSVALUE_KEY,
    127 						TurbineConstants.ACTION_EVENTSUBMIT_NEEDSVALUE_DEFAULT);
    128 		bubbleUpException = Turbine.getConfiguration()
    129 				.getBoolean(TurbineConstants.ACTION_EVENT_BUBBLE_EXCEPTION_UP,
    130 						TurbineConstants.ACTION_EVENT_BUBBLE_EXCEPTION_UP_DEFAULT);
    131
    132 		if (log.isDebugEnabled())
    133 		{
    134     		log.debug(submitValueKey
    135     				? "ActionEvent accepts only eventSubmit_do Keys with a value != 0"
    136     				: "ActionEvent accepts all eventSubmit_do Keys");
    137     		log.debug(bubbleUpException
    138     				  ? "ActionEvent will bubble exceptions up to Turbine.handleException() method"
    139     				  : "ActionEvent will not bubble exceptions up.");
    140 		}
    141 	}
    142
    143 	/**
    144 	 * Retrieve a method of the given name and signature. The value is cached.
    145 	 *
    146 	 * @param name the name of the method
    147 	 * @param signature an array of classes forming the signature of the method
    148 	 * @param pp ParameterParser for correct folding
    149 	 *
    150 	 * @return the method object
    151 	 * @throws NoSuchMethodException if the method does not exist
    152 	 */
    153 	protected Method getMethod(String name, Class<?>[] signature, ParameterParser pp) throws NoSuchMethodException
    154 	{
    155 	    Method method = (Method) this.methodCache.get(name, signature);
    156
    157 	    if (method == null)
    158 	    {
    159 	        // Try annotations of public methods
    160 	        Method[] methods = getClass().getMethods();
    161 	        for (Method m : methods)
    162 	        {
    163 	            if (m.isAnnotationPresent(TurbineActionEvent.class))
    164 	            {
    165 	                TurbineActionEvent tae = m.getAnnotation(TurbineActionEvent.class);
    166 	                if (name.equals(pp.convert(tae.value()))
    167                         && Arrays.equals(signature, m.getParameterTypes()))
    168 	                {
    169 	                    method = m;
    170 	                    break;
    171 	                }
    172 	            }
    173 	        }
    174
    175 	        // Try legacy mode
    176 	        if (method == null)
    177 	        {
    178                 String tmp = name.toLowerCase().substring(METHOD_NAME_LENGTH);
    179 	            method = getClass().getMethod(METHOD_NAME_PREFIX + StringUtils.capitalize(tmp), signature);
    180 	        }
    181
    182 	        this.methodCache.put(name, signature, method);
    183 	    }
    184
    185 	    return method;
    186 	}
    187
    188 	/**
    189 	 * This overrides the default Action.doPerform() to execute the
    190 	 * doEvent() method. If that fails, then it will execute the
    191 	 * doPerform() method instead.
    192 	 *
    193 	 * @param pipelineData Turbine information.
    194 	 * @exception Exception a generic exception.
    195 	 */
    196 	@Override
    197     public void doPerform(PipelineData pipelineData)
    198 			throws Exception
    199 	{
    200 	    ParameterParser pp = pipelineData.get(Turbine.class, ParameterParser.class);
    201 		executeEvents(pp, new Class<?>[]{ PipelineData.class }, new Object[]{ pipelineData });
    202 	}
    203
    204 	/**
    205 	 * This method should be called to execute the event based system.
    206 	 *
    207 	 * @param pp the parameter parser
    208 	 * @param signature the signature of the method to call
    209 	 * @param parameters the parameters for the method to call
    210 	 *
    211 	 * @exception Exception a generic exception.
    212 	 */
    213 	protected void executeEvents(ParameterParser pp, Class<?>[] signature, Object[] parameters)
    214 			throws Exception
    215 	{
    216 		// Name of the button.
    217 		String theButton = null;
    218
    219 		String button = pp.convert(BUTTON);
    220 		String key = null;
    221
    222 		// Loop through and find the button.
    223 		for (String k : pp)
    224 		{
    225 			key = k;
    226 			if (key.startsWith(button))
    227 			{
    228 				if (considerKey(key, pp))
    229 				{
    230 					theButton = key;
    231 					break;
    232 				}
    233 			}
    234 		}
    235
    236 		if (theButton == null)
    237 		{
    238 		    theButton = BUTTON + DEFAULT_METHOD;
    239 		    key = null;
    240 		}
    241
    242 		theButton = formatString(theButton, pp);
    243 		Method method = null;
    244
    245         try
    246         {
    247             method = getMethod(theButton, signature, pp);
    248         }
    249         catch (NoSuchMethodException e)
    250         {
    251             method = getMethod(DEFAULT_METHOD, signature, pp);
    252         }
    253         finally
    254         {
    255             if (key != null)
    256             {
    257                 pp.remove(key);
    258             }
    259         }
    260
    261 		try
    262 		{
    263 			if (log.isDebugEnabled())
    264 			{
    265 				log.debug("Invoking " + method);
    266 			}
    267
    268 			method.invoke(this, parameters);
    269 		}
    270 		catch (InvocationTargetException ite)
    271 		{
    272 			Throwable t = ite.getTargetException();
    273 			if (bubbleUpException)
    274 			{
    275                 if (t instanceof Exception)
    276                 {
    277                     throw (Exception) t;
    278                 }
    279                 else
    280                 {
    281                     throw ite;
    282                 }
    283 			}
    284 			else
    285 			{
    286 			    log.error("Invokation of " + method , t);
    287 			}
    288 		}
    289 	}
    290
    291 	/**
    292 	 * This method does the conversion of the lowercase method name
    293 	 * into the proper case.
    294 	 *
    295 	 * @param input The unconverted method name.
    296 	 * @param pp The parameter parser (for correct folding)
    297 	 * @return A string with the method name in the proper case.
    298 	 */
    299 	protected String formatString(String input, ParameterParser pp)
    300 	{
    301 		String tmp = input;
    302
    303 		if (StringUtils.isNotEmpty(input))
    304 		{
    305 			tmp = input.toLowerCase();
    306
    307 			// Chop off suffixes (for image type)
    308 			String methodName = (tmp.endsWith(".x") || tmp.endsWith(".y"))
    309 					? input.substring(0, input.length() - 2)
    310 					: input;
    311
    312 			if (pp.getUrlFolding() == URLCaseFolding.NONE)
    313 			{
    314                 tmp = methodName.substring(BUTTON_LENGTH);
    315 			}
    316 			else
    317 			{
    318                 tmp = methodName.toLowerCase().substring(BUTTON_LENGTH);
    319 			}
    320 		}
    321
    322 		return tmp;
    323 	}
    324
    325 	/**
    326 	 * Checks whether the selected key really is a valid event.
    327 	 *
    328 	 * @param key The selected key
    329 	 * @param pp The parameter parser to look for the key value
    330 	 *
    331 	 * @return true if this key is really an ActionEvent Key
    332 	 */
    333 	protected boolean considerKey(String key, ParameterParser pp)
    334 	{
    335 		if (!submitValueKey)
    336 		{
    337 			log.debug("No Value required, accepting " + key);
    338 			return true;
    339 		}
    340 		else
    341 		{
    342 			// If the action.eventsubmit.needsvalue key is true,
    343 			// events with a "0" or empty value are ignored.
    344 			// This can be used if you have multiple eventSubmit_do<xxx>
    345 			// fields in your form which are selected by client side code,
    346 			// e.g. JavaScript.
    347 			//
    348 			// If this key is unset or missing, nothing changes for the
    349 			// current behavior.
    350 			//
    351 			String keyValue = pp.getString(key);
    352 			log.debug("Key Value is " + keyValue);
    353 			if (StringUtils.isEmpty(keyValue))
    354 			{
    355 				log.debug("Key is empty, rejecting " + key);
    356 				return false;
    357 			}
    358
    359 			try
    360 			{
    361 				if (Integer.parseInt(keyValue) != 0)
    362 				{
    363 					log.debug("Integer != 0, accepting " + key);
    364 					return true;
    365 				}
    366 			}
    367 			catch (NumberFormatException nfe)
    368 			{
    369 				// Not a number. So it might be a
    370 				// normal Key like "continue" or "exit". Accept
    371 				// it.
    372 				log.debug("Not a number, accepting " + key);
    373 				return true;
    374 			}
    375 		}
    376 		log.debug("Rejecting " + key);
    377 		return false;
    378 	}
    379 }