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.annotation.Annotation; 023 024 025import java.lang.reflect.InvocationTargetException; 026import java.lang.reflect.Method; 027import java.util.Arrays; 028 029import org.apache.commons.collections.map.MultiKeyMap; 030import org.apache.commons.lang.StringUtils; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.apache.fulcrum.parser.ParameterParser; 034import org.apache.fulcrum.parser.ValueParser.URLCaseFolding; 035import org.apache.turbine.Turbine; 036import org.apache.turbine.TurbineConstants; 037import org.apache.turbine.annotation.AnnotationProcessor; 038import org.apache.turbine.annotation.TurbineActionEvent; 039import org.apache.turbine.annotation.TurbineConfiguration; 040import org.apache.turbine.pipeline.PipelineData; 041 042/** 043 * <p> 044 * 045 * This is an alternative to the Action class that allows you to do 046 * event based actions. Essentially, you label all your submit buttons 047 * with the prefix of "eventSubmit_" and the suffix of "methodName". 048 * For example, "eventSubmit_doDelete". Then any class that subclasses 049 * this class will get its "doDelete(PipelineData data)" method executed. 050 * If for any reason, it was not able to execute the method, it will 051 * fall back to executing the doPerform() method which is required to 052 * be implemented. 053 * 054 * <p> 055 * 056 * Limitations: 057 * 058 * <p> 059 * 060 * Because ParameterParser makes all the key values lowercase, we have 061 * to do some work to format the string into a method name. For 062 * example, a button name eventSubmit_doDelete gets converted into 063 * eventsubmit_dodelete. Thus, we need to form some sort of naming 064 * convention so that dodelete can be turned into doDelete. 065 * 066 * <p> 067 * 068 * Thus, the convention is this: 069 * 070 * <ul> 071 * <li>The variable name MUST have the prefix "eventSubmit_".</li> 072 * <li>The variable name after the prefix MUST begin with the letters 073 * "do".</li> 074 * <li>The first letter after the "do" will be capitalized and the 075 * rest will be lowercase</li> 076 * </ul> 077 * 078 * If you follow these conventions, then you should be ok with your 079 * method naming in your Action class. 080 * 081 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens </a> 082 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 083 * @author <a href="quintonm@bellsouth.net">Quinton McCombs</a> 084 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a> 085 * @version $Id: ActionEvent.java 1812628 2017-10-19 12:34:25Z gk $ 086 */ 087public abstract class ActionEvent extends Action 088{ 089 /** Logging */ 090 protected Log log = LogFactory.getLog(this.getClass()); 091 092 /** The name of the button to look for. */ 093 protected static final String BUTTON = "eventSubmit_"; 094 /** The length of the button to look for. */ 095 protected static final int BUTTON_LENGTH = BUTTON.length(); 096 /** The default method. */ 097 protected static final String DEFAULT_METHOD = "doPerform"; 098 /** The prefix of the method name. */ 099 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}