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; 023import java.lang.reflect.InvocationTargetException; 024import java.lang.reflect.Method; 025import java.util.Arrays; 026import java.util.concurrent.ConcurrentHashMap; 027import java.util.concurrent.ConcurrentMap; 028 029import org.apache.commons.lang3.StringUtils; 030import org.apache.fulcrum.parser.ParameterParser; 031import org.apache.fulcrum.parser.ValueParser.URLCaseFolding; 032import org.apache.logging.log4j.LogManager; 033import org.apache.logging.log4j.Logger; 034import org.apache.turbine.Turbine; 035import org.apache.turbine.TurbineConstants; 036import org.apache.turbine.annotation.AnnotationProcessor; 037import org.apache.turbine.annotation.TurbineActionEvent; 038import org.apache.turbine.annotation.TurbineConfiguration; 039import org.apache.turbine.pipeline.PipelineData; 040 041/** 042 * <p> 043 * 044 * This is an alternative to the Action class that allows you to do 045 * event based actions. Essentially, you label all your submit buttons 046 * with the prefix of "eventSubmit_" and the suffix of "methodName". 047 * For example, "eventSubmit_doDelete". Then any class that subclasses 048 * this class will get its "doDelete(PipelineData data)" method executed. 049 * If for any reason, it was not able to execute the method, it will 050 * fall back to executing the doPerform() method which is required to 051 * be implemented. 052 * 053 * <p> 054 * 055 * Limitations: 056 * 057 * <p> 058 * 059 * Because ParameterParser makes all the key values lowercase, we have 060 * to do some work to format the string into a method name. For 061 * example, a button name eventSubmit_doDelete gets converted into 062 * eventsubmit_dodelete. Thus, we need to form some sort of naming 063 * convention so that dodelete can be turned into doDelete. 064 * 065 * <p> 066 * 067 * Thus, the convention is this: 068 * 069 * <ul> 070 * <li>The variable name MUST have the prefix "eventSubmit_".</li> 071 * <li>The variable name after the prefix MUST begin with the letters 072 * "do".</li> 073 * <li>The first letter after the "do" will be capitalized and the 074 * rest will be lowercase</li> 075 * </ul> 076 * 077 * If you follow these conventions, then you should be ok with your 078 * method naming in your Action class. 079 * 080 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens </a> 081 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 082 * @author <a href="quintonm@bellsouth.net">Quinton McCombs</a> 083 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a> 084 * @version $Id$ 085 */ 086public abstract class ActionEvent implements Action 087{ 088 /** Logging */ 089 protected Logger log = LogManager.getLogger(this.getClass()); 090 091 /** The name of the button to look for. */ 092 protected static final String BUTTON = "eventSubmit_"; 093 /** The length of the button to look for. */ 094 protected static final int BUTTON_LENGTH = BUTTON.length(); 095 /** The default method. */ 096 protected static final String DEFAULT_METHOD = "doPerform"; 097 /** The prefix of the method name. */ 098 protected static final String METHOD_NAME_PREFIX = "do"; 099 /** The length of the method name. */ 100 protected static final int METHOD_NAME_LENGTH = METHOD_NAME_PREFIX.length(); 101 /** The length of the button to look for. */ 102 protected static final int LENGTH = BUTTON.length(); 103 104 /** 105 * If true, the eventSubmit_do<xxx> variable must contain 106 * a not null value to be executed. 107 */ 108 @TurbineConfiguration( TurbineConstants.ACTION_EVENTSUBMIT_NEEDSVALUE_KEY ) 109 private boolean submitValueKey = TurbineConstants.ACTION_EVENTSUBMIT_NEEDSVALUE_DEFAULT; 110 111 /** 112 * If true, then exceptions raised in eventSubmit_do<xxx> methods 113 * as well as in doPerform methods are bubbled up to the Turbine 114 * servlet's handleException method. 115 */ 116 @TurbineConfiguration( TurbineConstants.ACTION_EVENT_BUBBLE_EXCEPTION_UP ) 117 protected boolean bubbleUpException = TurbineConstants.ACTION_EVENT_BUBBLE_EXCEPTION_UP_DEFAULT; 118 119 /** 120 * Cache for the methods to invoke 121 */ 122 private ConcurrentMap<String, Method> methodCache = new ConcurrentHashMap<>(); 123 124 /** 125 * Retrieve a method of the given name and signature. The value is cached. 126 * 127 * @param name the name of the method 128 * @param signature an array of classes forming the signature of the method 129 * @param pp ParameterParser for correct folding 130 * 131 * @return the method object 132 * @throws NoSuchMethodException if the method does not exist 133 */ 134 protected Method getMethod(String name, Class<?>[] signature, ParameterParser pp) throws NoSuchMethodException 135 { 136 StringBuilder cacheKey = new StringBuilder(name); 137 for (Class<?> clazz : signature) 138 { 139 cacheKey.append(':').append(clazz.getCanonicalName()); 140 } 141 142 Method method = this.methodCache.get(cacheKey.toString()); 143 144 if (method == null) 145 { 146 // Try annotations of public methods 147 Method[] methods = getClass().getMethods(); 148 149 methodLoop: 150 for (Method m : methods) 151 { 152 Annotation[] annotations = AnnotationProcessor.getAnnotations(m); 153 for (Annotation a : annotations) 154 { 155 if (a instanceof TurbineActionEvent) 156 { 157 TurbineActionEvent tae = (TurbineActionEvent) a; 158 if (name.equals(pp.convert(tae.value())) 159 && Arrays.equals(signature, m.getParameterTypes())) 160 { 161 method = m; 162 break methodLoop; 163 } 164 } 165 } 166 } 167 168 // Try legacy mode 169 if (method == null) 170 { 171 String tmp = name.toLowerCase().substring(METHOD_NAME_LENGTH); 172 method = getClass().getMethod(METHOD_NAME_PREFIX + StringUtils.capitalize(tmp), signature); 173 } 174 175 Method oldMethod = this.methodCache.putIfAbsent(cacheKey.toString(), method); 176 if (oldMethod != null) 177 { 178 method = oldMethod; 179 } 180 } 181 182 return method; 183 } 184 185 /** 186 * This overrides the default Action.doPerform() to execute the 187 * doEvent() method. If that fails, then it will execute the 188 * doPerform() method instead. 189 * 190 * @param pipelineData Turbine information. 191 * @throws Exception a generic exception. 192 */ 193 @Override 194 public void doPerform(PipelineData pipelineData) 195 throws Exception 196 { 197 ParameterParser pp = pipelineData.get(Turbine.class, ParameterParser.class); 198 executeEvents(pp, new Class<?>[]{ PipelineData.class }, new Object[]{ pipelineData }); 199 } 200 201 /** 202 * This method should be called to execute the event based system. 203 * 204 * @param pp the parameter parser 205 * @param signature the signature of the method to call 206 * @param parameters the parameters for the method to call 207 * 208 * @throws Exception a generic exception. 209 */ 210 protected void executeEvents(ParameterParser pp, Class<?>[] signature, Object[] parameters) 211 throws Exception 212 { 213 // Name of the button. 214 String theButton = null; 215 216 String button = pp.convert(BUTTON); 217 String key = null; 218 219 // Loop through and find the button. 220 for (String k : pp) 221 { 222 key = k; 223 if (key.startsWith(button)) 224 { 225 if (considerKey(key, pp)) 226 { 227 theButton = key; 228 break; 229 } 230 } 231 } 232 233 if (theButton == null) 234 { 235 theButton = BUTTON + DEFAULT_METHOD; 236 key = null; 237 } 238 239 theButton = formatString(theButton, pp); 240 Method method = null; 241 242 try 243 { 244 method = getMethod(theButton, signature, pp); 245 } 246 catch (NoSuchMethodException e) 247 { 248 method = getMethod(DEFAULT_METHOD, signature, pp); 249 } 250 finally 251 { 252 if (key != null) 253 { 254 pp.remove(key); 255 } 256 } 257 258 try 259 { 260 log.debug("Invoking {}", method); 261 262 method.invoke(this, parameters); 263 } 264 catch (InvocationTargetException ite) 265 { 266 Throwable t = ite.getTargetException(); 267 if (bubbleUpException) 268 { 269 if (t instanceof Exception) 270 { 271 throw (Exception) t; 272 } 273 else 274 { 275 throw ite; 276 } 277 } 278 else 279 { 280 log.error("Invokation of {}", method, t); 281 } 282 } 283 } 284 285 /** 286 * This method does the conversion of the lowercase method name 287 * into the proper case. 288 * 289 * @param input The unconverted method name. 290 * @param pp The parameter parser (for correct folding) 291 * @return A string with the method name in the proper case. 292 */ 293 protected String formatString(String input, ParameterParser pp) 294 { 295 String tmp = input; 296 297 if (StringUtils.isNotEmpty(input)) 298 { 299 tmp = input.toLowerCase(); 300 301 // Chop off suffixes (for image type) 302 String methodName = (tmp.endsWith(".x") || tmp.endsWith(".y")) 303 ? input.substring(0, input.length() - 2) 304 : input; 305 306 if (pp.getUrlFolding() == URLCaseFolding.NONE) 307 { 308 tmp = methodName.substring(BUTTON_LENGTH); 309 } 310 else 311 { 312 tmp = methodName.toLowerCase().substring(BUTTON_LENGTH); 313 } 314 } 315 316 return tmp; 317 } 318 319 /** 320 * Checks whether the selected key really is a valid event. 321 * 322 * @param key The selected key 323 * @param pp The parameter parser to look for the key value 324 * 325 * @return true if this key is really an ActionEvent Key 326 */ 327 protected boolean considerKey(String key, ParameterParser pp) 328 { 329 if (!submitValueKey) 330 { 331 log.debug("No Value required, accepting {}", key); 332 return true; 333 } 334 else 335 { 336 // If the action.eventsubmit.needsvalue key is true, 337 // events with a "0" or empty value are ignored. 338 // This can be used if you have multiple eventSubmit_do<xxx> 339 // fields in your form which are selected by client side code, 340 // e.g. JavaScript. 341 // 342 // If this key is unset or missing, nothing changes for the 343 // current behavior. 344 // 345 String keyValue = pp.getString(key); 346 log.debug("Key Value is {}", keyValue); 347 if (StringUtils.isEmpty(keyValue)) 348 { 349 log.debug("Key is empty, rejecting {}", key); 350 return false; 351 } 352 353 try 354 { 355 if (Integer.parseInt(keyValue) != 0) 356 { 357 log.debug("Integer != 0, accepting {}", key); 358 return true; 359 } 360 } 361 catch (NumberFormatException nfe) 362 { 363 // Not a number. So it might be a 364 // normal Key like "continue" or "exit". Accept 365 // it. 366 log.debug("Not a number, accepting " + key); 367 return true; 368 } 369 } 370 log.debug("Rejecting {}", key); 371 return false; 372 } 373}