001package org.apache.turbine.modules; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.lang.reflect.InvocationTargetException; 023import java.lang.reflect.Method; 024import java.util.Arrays; 025 026import org.apache.commons.collections.map.MultiKeyMap; 027import org.apache.commons.lang.StringUtils; 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030import org.apache.fulcrum.parser.ParameterParser; 031import org.apache.fulcrum.parser.ValueParser.URLCaseFolding; 032import org.apache.turbine.Turbine; 033import org.apache.turbine.TurbineConstants; 034import org.apache.turbine.annotation.TurbineActionEvent; 035import org.apache.turbine.pipeline.PipelineData; 036 037/** 038 * <p> 039 * 040 * This is an alternative to the Action class that allows you to do 041 * event based actions. Essentially, you label all your submit buttons 042 * with the prefix of "eventSubmit_" and the suffix of "methodName". 043 * For example, "eventSubmit_doDelete". Then any class that subclasses 044 * this class will get its "doDelete(PipelineData data)" method executed. 045 * If for any reason, it was not able to execute the method, it will 046 * fall back to executing the doPerform() method which is required to 047 * be implemented. 048 * 049 * <p> 050 * 051 * Limitations: 052 * 053 * <p> 054 * 055 * Because ParameterParser makes all the key values lowercase, we have 056 * to do some work to format the string into a method name. For 057 * example, a button name eventSubmit_doDelete gets converted into 058 * eventsubmit_dodelete. Thus, we need to form some sort of naming 059 * convention so that dodelete can be turned into doDelete. 060 * 061 * <p> 062 * 063 * Thus, the convention is this: 064 * 065 * <ul> 066 * <li>The variable name MUST have the prefix "eventSubmit_".</li> 067 * <li>The variable name after the prefix MUST begin with the letters 068 * "do".</li> 069 * <li>The first letter after the "do" will be capitalized and the 070 * rest will be lowercase</li> 071 * </ul> 072 * 073 * If you follow these conventions, then you should be ok with your 074 * method naming in your Action class. 075 * 076 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens </a> 077 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 078 * @author <a href="quintonm@bellsouth.net">Quinton McCombs</a> 079 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a> 080 * @version $Id: ActionEvent.java 1706425 2015-10-02 14:47:51Z tv $ 081 */ 082public abstract class ActionEvent extends Action 083{ 084 /** Logging */ 085 protected Log log = LogFactory.getLog(this.getClass()); 086 087 /** The name of the button to look for. */ 088 protected static final String BUTTON = "eventSubmit_"; 089 /** The length of the button to look for. */ 090 protected static final int BUTTON_LENGTH = BUTTON.length(); 091 /** The default method. */ 092 protected static final String DEFAULT_METHOD = "doPerform"; 093 /** The prefix of the method name. */ 094 protected static final String METHOD_NAME_PREFIX = "do"; 095 /** The length of the method name. */ 096 protected static final int METHOD_NAME_LENGTH = METHOD_NAME_PREFIX.length(); 097 /** The length of the button to look for. */ 098 protected static final int LENGTH = BUTTON.length(); 099 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}