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