001 package org.apache.commons.digester3; 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 022 import static java.lang.System.arraycopy; 023 import static java.lang.String.format; 024 import static java.util.Arrays.fill; 025 import static org.apache.commons.beanutils.ConvertUtils.convert; 026 import static org.apache.commons.beanutils.MethodUtils.invokeExactMethod; 027 import static org.apache.commons.beanutils.MethodUtils.invokeMethod; 028 029 import java.util.Formatter; 030 031 import org.xml.sax.Attributes; 032 import org.xml.sax.SAXException; 033 034 /** 035 * <p> 036 * Rule implementation that calls a method on an object on the stack (normally the top/parent object), passing arguments 037 * collected from subsequent <code>CallParamRule</code> rules or from the body of this element. 038 * </p> 039 * <p> 040 * By using {@link #CallMethodRule(String methodName)} a method call can be made to a method which accepts no arguments. 041 * </p> 042 * <p> 043 * Incompatible method parameter types are converted using <code>org.apache.commons.beanutils.ConvertUtils</code>. 044 * </p> 045 * <p> 046 * This rule now uses {@link org.apache.commons.beanutils.MethodUtils#invokeMethod} by default. 047 * This increases the kinds of methods successfully and allows primitives to be matched by passing in wrapper classes. 048 * There are rare cases when {@link org.apache.commons.beanutils.MethodUtils#invokeExactMethod} (the old default) is 049 * required. This method is much stricter in it's reflection. 050 * Setting the <code>UseExactMatch</code> to true reverts to the use of this method. 051 * </p> 052 * <p> 053 * Note that the target method is invoked when the <i>end</i> of the tag the CallMethodRule fired on is encountered, 054 * <i>not</i> when the last parameter becomes available. This implies that rules which fire on tags nested within the 055 * one associated with the CallMethodRule will fire before the CallMethodRule invokes the target method. This behavior 056 * is not configurable. 057 * </p> 058 * <p> 059 * Note also that if a CallMethodRule is expecting exactly one parameter and that parameter is not available (eg 060 * CallParamRule is used with an attribute name but the attribute does not exist) then the method will not be invoked. 061 * If a CallMethodRule is expecting more than one parameter, then it is always invoked, regardless of whether the 062 * parameters were available or not; missing parameters are converted to the appropriate target type by calling 063 * ConvertUtils.convert. Note that the default ConvertUtils converters for the String type returns a null when passed a 064 * null, meaning that CallMethodRule will passed null for all String parameters for which there is no parameter info 065 * available from the XML. However parameters of type Float and Integer will be passed a real object containing a zero 066 * value as that is the output of the default ConvertUtils converters for those types when passed a null. You can 067 * register custom converters to change this behavior; see the BeanUtils library documentation for more info. 068 * </p> 069 * <p> 070 * Note that when a constructor is used with paramCount=0, indicating that the body of the element is to be passed to 071 * the target method, an empty element will cause an <i>empty string</i> to be passed to the target method, not null. 072 * And if automatic type conversion is being applied (ie if the target function takes something other than a string as a 073 * parameter) then the conversion will fail if the converter class does not accept an empty string as valid input. 074 * </p> 075 * <p> 076 * CallMethodRule has a design flaw which can cause it to fail under certain rule configurations. All CallMethodRule 077 * instances share a single parameter stack, and all CallParamRule instances simply store their data into the 078 * parameter-info structure that is on the top of the stack. This means that two CallMethodRule instances cannot be 079 * associated with the same pattern without getting scrambled parameter data. This same issue also applies when a 080 * CallMethodRule matches some element X, a different CallMethodRule matches a child element Y and some of the 081 * CallParamRules associated with the first CallMethodRule match element Y or one of its child elements. This issue has 082 * been present since the very first release of Digester. Note, however, that this configuration of CallMethodRule 083 * instances is not commonly required. 084 * </p> 085 */ 086 public class CallMethodRule 087 extends Rule 088 { 089 090 // ----------------------------------------------------------- Constructors 091 092 /** 093 * Construct a "call method" rule with the specified method name. The parameter types (if any) default to 094 * java.lang.String. 095 * 096 * @param methodName Method name of the parent method to call 097 * @param paramCount The number of parameters to collect, or zero for a single argument from the body of this 098 * element. 099 */ 100 public CallMethodRule( String methodName, int paramCount ) 101 { 102 this( 0, methodName, paramCount ); 103 } 104 105 /** 106 * Construct a "call method" rule with the specified method name. The parameter types (if any) default to 107 * java.lang.String. 108 * 109 * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester 110 * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on 111 * the stack. 112 * @param methodName Method name of the parent method to call 113 * @param paramCount The number of parameters to collect, or zero for a single argument from the body of this 114 * element. 115 */ 116 public CallMethodRule( int targetOffset, String methodName, int paramCount ) 117 { 118 this.targetOffset = targetOffset; 119 this.methodName = methodName; 120 this.paramCount = paramCount; 121 if ( paramCount == 0 ) 122 { 123 this.paramTypes = new Class[] { String.class }; 124 } 125 else 126 { 127 this.paramTypes = new Class[paramCount]; 128 fill( this.paramTypes, String.class ); 129 } 130 } 131 132 /** 133 * Construct a "call method" rule with the specified method name. The method should accept no parameters. 134 * 135 * @param methodName Method name of the parent method to call 136 */ 137 public CallMethodRule( String methodName ) 138 { 139 this( 0, methodName, 0, (Class[]) null ); 140 } 141 142 /** 143 * Construct a "call method" rule with the specified method name. The method should accept no parameters. 144 * 145 * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester 146 * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on 147 * the stack. 148 * @param methodName Method name of the parent method to call 149 */ 150 public CallMethodRule( int targetOffset, String methodName ) 151 { 152 this( targetOffset, methodName, 0, (Class[]) null ); 153 } 154 155 /** 156 * Construct a "call method" rule with the specified method name and parameter types. If <code>paramCount</code> is 157 * set to zero the rule will use the body of this element as the single argument of the method, unless 158 * <code>paramTypes</code> is null or empty, in this case the rule will call the specified method with no arguments. 159 * 160 * @param methodName Method name of the parent method to call 161 * @param paramCount The number of parameters to collect, or zero for a single argument from the body of ths element 162 * @param paramTypes The Java class names of the arguments (if you wish to use a primitive type, specify the 163 * corresonding Java wrapper class instead, such as <code>java.lang.Boolean</code> for a 164 * <code>boolean</code> parameter) 165 */ 166 public CallMethodRule( String methodName, int paramCount, String[] paramTypes ) 167 { 168 this( 0, methodName, paramCount, paramTypes ); 169 } 170 171 /** 172 * Construct a "call method" rule with the specified method name and parameter types. If <code>paramCount</code> is 173 * set to zero the rule will use the body of this element as the single argument of the method, unless 174 * <code>paramTypes</code> is null or empty, in this case the rule will call the specified method with no arguments. 175 * 176 * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester 177 * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on 178 * the stack. 179 * @param methodName Method name of the parent method to call 180 * @param paramCount The number of parameters to collect, or zero for a single argument from the body of the element 181 * @param paramTypes The Java class names of the arguments (if you wish to use a primitive type, specify the 182 * corresponding Java wrapper class instead, such as <code>java.lang.Boolean</code> for a 183 * <code>boolean</code> parameter) 184 */ 185 public CallMethodRule( int targetOffset, String methodName, int paramCount, String[] paramTypes ) 186 { 187 this.targetOffset = targetOffset; 188 this.methodName = methodName; 189 this.paramCount = paramCount; 190 if ( paramTypes == null ) 191 { 192 this.paramTypes = new Class[paramCount]; 193 fill( this.paramTypes, String.class ); 194 } 195 else 196 { 197 // copy the parameter class names into an array 198 // the classes will be loaded when the digester is set 199 this.paramClassNames = new String[paramTypes.length]; 200 arraycopy( paramTypes, 0, this.paramClassNames, 0, paramTypes.length ); 201 } 202 } 203 204 /** 205 * Construct a "call method" rule with the specified method name and parameter types. If <code>paramCount</code> is 206 * set to zero the rule will use the body of this element as the single argument of the method, unless 207 * <code>paramTypes</code> is null or empty, in this case the rule will call the specified method with no arguments. 208 * 209 * @param methodName Method name of the parent method to call 210 * @param paramCount The number of parameters to collect, or zero for a single argument from the body of the element 211 * @param paramTypes The Java classes that represent the parameter types of the method arguments (if you wish to use 212 * a primitive type, specify the corresponding Java wrapper class instead, such as 213 * <code>java.lang.Boolean.TYPE</code> for a <code>boolean</code> parameter) 214 */ 215 public CallMethodRule( String methodName, int paramCount, Class<?> paramTypes[] ) 216 { 217 this( 0, methodName, paramCount, paramTypes ); 218 } 219 220 /** 221 * Construct a "call method" rule with the specified method name and parameter types. If <code>paramCount</code> is 222 * set to zero the rule will use the body of this element as the single argument of the method, unless 223 * <code>paramTypes</code> is null or empty, in this case the rule will call the specified method with no arguments. 224 * 225 * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester 226 * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on 227 * the stack. 228 * @param methodName Method name of the parent method to call 229 * @param paramCount The number of parameters to collect, or zero for a single argument from the body of the element 230 * @param paramTypes The Java classes that represent the parameter types of the method arguments (if you wish to use 231 * a primitive type, specify the corresponding Java wrapper class instead, such as 232 * <code>java.lang.Boolean.TYPE</code> for a <code>boolean</code> parameter) 233 */ 234 public CallMethodRule( int targetOffset, String methodName, int paramCount, Class<?>[] paramTypes ) 235 { 236 this.targetOffset = targetOffset; 237 this.methodName = methodName; 238 this.paramCount = paramCount; 239 if ( paramTypes == null ) 240 { 241 this.paramTypes = new Class<?>[paramCount]; 242 fill( this.paramTypes, String.class ); 243 } 244 else 245 { 246 this.paramTypes = new Class<?>[paramTypes.length]; 247 arraycopy( paramTypes, 0, this.paramTypes, 0, paramTypes.length ); 248 } 249 } 250 251 // ----------------------------------------------------- Instance Variables 252 253 /** 254 * The body text collected from this element. 255 */ 256 protected String bodyText = null; 257 258 /** 259 * location of the target object for the call, relative to the top of the digester object stack. The default value 260 * of zero means the target object is the one on top of the stack. 261 */ 262 protected int targetOffset = 0; 263 264 /** 265 * The method name to call on the parent object. 266 */ 267 protected String methodName = null; 268 269 /** 270 * The number of parameters to collect from <code>MethodParam</code> rules. If this value is zero, a single 271 * parameter will be collected from the body of this element. 272 */ 273 protected int paramCount = 0; 274 275 /** 276 * The parameter types of the parameters to be collected. 277 */ 278 protected Class<?>[] paramTypes = null; 279 280 /** 281 * The names of the classes of the parameters to be collected. This attribute allows creation of the classes to be 282 * postponed until the digester is set. 283 */ 284 private String[] paramClassNames = null; 285 286 /** 287 * Should <code>MethodUtils.invokeExactMethod</code> be used for reflection. 288 */ 289 private boolean useExactMatch = false; 290 291 // --------------------------------------------------------- Public Methods 292 293 /** 294 * Should <code>MethodUtils.invokeExactMethod</code> be used for the reflection. 295 * 296 * @return true, if <code>MethodUtils.invokeExactMethod</code> Should be used for the reflection, 297 * false otherwise 298 */ 299 public boolean getUseExactMatch() 300 { 301 return useExactMatch; 302 } 303 304 /** 305 * Set whether <code>MethodUtils.invokeExactMethod</code> should be used for the reflection. 306 * 307 * @param useExactMatch The <code>MethodUtils.invokeExactMethod</code> flag 308 */ 309 public void setUseExactMatch( boolean useExactMatch ) 310 { 311 this.useExactMatch = useExactMatch; 312 } 313 314 /** 315 * {@inheritDoc} 316 */ 317 @Override 318 public void setDigester( Digester digester ) 319 { 320 // call superclass 321 super.setDigester( digester ); 322 // if necessary, load parameter classes 323 if ( this.paramClassNames != null ) 324 { 325 this.paramTypes = new Class<?>[paramClassNames.length]; 326 for ( int i = 0; i < this.paramClassNames.length; i++ ) 327 { 328 try 329 { 330 this.paramTypes[i] = digester.getClassLoader().loadClass( this.paramClassNames[i] ); 331 } 332 catch ( ClassNotFoundException e ) 333 { 334 throw new RuntimeException( format( "[CallMethodRule] Cannot load class %s at position %s", 335 this.paramClassNames[i], i ), e ); 336 } 337 } 338 } 339 } 340 341 /** 342 * {@inheritDoc} 343 */ 344 @Override 345 public void begin( String namespace, String name, Attributes attributes ) 346 throws Exception 347 { 348 // Push an array to capture the parameter values if necessary 349 if ( paramCount > 0 ) 350 { 351 Object parameters[] = new Object[paramCount]; 352 fill( parameters, null ); 353 getDigester().pushParams( parameters ); 354 } 355 } 356 357 /** 358 * {@inheritDoc} 359 */ 360 @Override 361 public void body( String namespace, String name, String text ) 362 throws Exception 363 { 364 if ( paramCount == 0 ) 365 { 366 this.bodyText = text.trim(); 367 } 368 } 369 370 /** 371 * {@inheritDoc} 372 */ 373 @Override 374 public void end( String namespace, String name ) 375 throws Exception 376 { 377 // Retrieve or construct the parameter values array 378 Object[] parameters; 379 if ( paramCount > 0 ) 380 { 381 parameters = getDigester().popParams(); 382 383 if ( getDigester().getLogger().isTraceEnabled() ) 384 { 385 for ( int i = 0, size = parameters.length; i < size; i++ ) 386 { 387 getDigester().getLogger().trace( format( "[CallMethodRule]{%s} parameters[%s]=%s", 388 getDigester().getMatch(), 389 i, 390 parameters[i] ) ); 391 } 392 } 393 394 // In the case where the target method takes a single parameter 395 // and that parameter does not exist (the CallParamRule never 396 // executed or the CallParamRule was intended to set the parameter 397 // from an attribute but the attribute wasn't present etc) then 398 // skip the method call. 399 // 400 // This is useful when a class has a "default" value that should 401 // only be overridden if data is present in the XML. I don't 402 // know why this should only apply to methods taking *one* 403 // parameter, but it always has been so we can't change it now. 404 if ( paramCount == 1 && parameters[0] == null ) 405 { 406 return; 407 } 408 409 } 410 else if ( paramTypes != null && paramTypes.length != 0 ) 411 { 412 // Having paramCount == 0 and paramTypes.length == 1 indicates 413 // that we have the special case where the target method has one 414 // parameter being the body text of the current element. 415 416 // There is no body text included in the source XML file, 417 // so skip the method call 418 if ( bodyText == null ) 419 { 420 return; 421 } 422 423 parameters = new Object[] { bodyText }; 424 if ( paramTypes.length == 0 ) 425 { 426 paramTypes = new Class[] { String.class }; 427 } 428 } 429 else 430 { 431 // When paramCount is zero and paramTypes.length is zero it 432 // means that we truly are calling a method with no parameters. 433 // Nothing special needs to be done here. 434 parameters = new Object[0]; 435 paramTypes = new Class<?>[0]; 436 } 437 438 // Construct the parameter values array we will need 439 // We only do the conversion if the param value is a String and 440 // the specified paramType is not String. 441 Object[] paramValues = new Object[paramTypes.length]; 442 for ( int i = 0; i < paramTypes.length; i++ ) 443 { 444 // convert nulls and convert stringy parameters 445 // for non-stringy param types 446 if ( parameters[i] == null 447 || ( parameters[i] instanceof String && !String.class.isAssignableFrom( paramTypes[i] ) ) ) 448 { 449 paramValues[i] = convert( (String) parameters[i], paramTypes[i] ); 450 } 451 else 452 { 453 paramValues[i] = parameters[i]; 454 } 455 } 456 457 // Determine the target object for the method call 458 Object target; 459 if ( targetOffset >= 0 ) 460 { 461 target = getDigester().peek( targetOffset ); 462 } 463 else 464 { 465 target = getDigester().peek( getDigester().getCount() + targetOffset ); 466 } 467 468 if ( target == null ) 469 { 470 throw new SAXException( format( "[CallMethodRule]{%s} Call target is null (targetOffset=%s, stackdepth=%s)", 471 getDigester().getMatch(), targetOffset, getDigester().getCount() ) ); 472 } 473 474 // Invoke the required method on the top object 475 if ( getDigester().getLogger().isDebugEnabled() ) 476 { 477 Formatter formatter = 478 new Formatter().format( "[CallMethodRule]{%s} Call %s.%s(", 479 getDigester().getMatch(), 480 target.getClass().getName(), 481 methodName ); 482 for ( int i = 0; i < paramValues.length; i++ ) 483 { 484 formatter.format( "%s%s/%s", ( i > 0 ? ", " : "" ), paramValues[i], paramTypes[i].getName() ); 485 } 486 formatter.format( ")" ); 487 getDigester().getLogger().debug( formatter.toString() ); 488 } 489 490 Object result = null; 491 if ( useExactMatch ) 492 { 493 // invoke using exact match 494 result = invokeExactMethod( target, methodName, paramValues, paramTypes ); 495 496 } 497 else 498 { 499 // invoke using fuzzier match 500 result = invokeMethod( target, methodName, paramValues, paramTypes ); 501 } 502 503 processMethodCallResult( result ); 504 } 505 506 /** 507 * {@inheritDoc} 508 */ 509 @Override 510 public void finish() 511 throws Exception 512 { 513 bodyText = null; 514 } 515 516 /** 517 * Subclasses may override this method to perform additional processing of the invoked method's result. 518 * 519 * @param result the Object returned by the method invoked, possibly null 520 */ 521 protected void processMethodCallResult( Object result ) 522 { 523 // do nothing 524 } 525 526 /** 527 * {@inheritDoc} 528 */ 529 @Override 530 public String toString() 531 { 532 Formatter formatter = new Formatter().format( "CallMethodRule[methodName=%s, paramCount=%s, paramTypes={", 533 methodName, paramCount ); 534 if ( paramTypes != null ) 535 { 536 for ( int i = 0; i < paramTypes.length; i++ ) 537 { 538 formatter.format( "%s%s", 539 ( i > 0 ? ", " : "" ), 540 ( paramTypes[i] != null ? paramTypes[i].getName() : "null" ) ); 541 } 542 } 543 formatter.format( "}]" ); 544 return ( formatter.toString() ); 545 } 546 547 }