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<xxx> 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<xxx> 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<xxx>
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 }