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 }