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; 028import java.util.stream.Collectors; 029 030import org.apache.commons.lang3.StringUtils; 031import org.apache.fulcrum.parser.ParameterParser; 032import org.apache.fulcrum.parser.ValueParser.URLCaseFolding; 033import org.apache.logging.log4j.LogManager; 034import org.apache.logging.log4j.Logger; 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$ 086 */ 087public abstract class ActionEvent implements Action 088{ 089 /** Logging */ 090 protected Logger log = LogManager.getLogger(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 ConcurrentMap<String, Method> methodCache = new ConcurrentHashMap<>(); 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 String cacheKey = 138 Arrays.stream(signature) 139 .map(clazz -> ':' + clazz.getCanonicalName()) 140 .collect(Collectors.joining("", name, "")); 141 142 Method method = this.methodCache.get(cacheKey); 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, 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}