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 }