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 }