View Javadoc

1   package org.apache.commons.digester3;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import static java.lang.System.arraycopy;
23  import static java.lang.String.format;
24  import static java.util.Arrays.fill;
25  import static org.apache.commons.beanutils.ConvertUtils.convert;
26  import static org.apache.commons.beanutils.MethodUtils.invokeExactMethod;
27  import static org.apache.commons.beanutils.MethodUtils.invokeMethod;
28  
29  import java.util.Formatter;
30  
31  import org.xml.sax.Attributes;
32  import org.xml.sax.SAXException;
33  
34  /**
35   * <p>
36   * Rule implementation that calls a method on an object on the stack (normally the top/parent object), passing arguments
37   * collected from subsequent <code>CallParamRule</code> rules or from the body of this element.
38   * </p>
39   * <p>
40   * By using {@link #CallMethodRule(String methodName)} a method call can be made to a method which accepts no arguments.
41   * </p>
42   * <p>
43   * Incompatible method parameter types are converted using <code>org.apache.commons.beanutils.ConvertUtils</code>.
44   * </p>
45   * <p>
46   * This rule now uses {@link org.apache.commons.beanutils.MethodUtils#invokeMethod} by default.
47   * This increases the kinds of methods successfully and allows primitives to be matched by passing in wrapper classes.
48   * There are rare cases when {@link org.apache.commons.beanutils.MethodUtils#invokeExactMethod} (the old default) is
49   * required. This method is much stricter in it's reflection.
50   * Setting the <code>UseExactMatch</code> to true reverts to the use of this method.
51   * </p>
52   * <p>
53   * Note that the target method is invoked when the <i>end</i> of the tag the CallMethodRule fired on is encountered,
54   * <i>not</i> when the last parameter becomes available. This implies that rules which fire on tags nested within the
55   * one associated with the CallMethodRule will fire before the CallMethodRule invokes the target method. This behavior
56   * is not configurable.
57   * </p>
58   * <p>
59   * Note also that if a CallMethodRule is expecting exactly one parameter and that parameter is not available (eg
60   * CallParamRule is used with an attribute name but the attribute does not exist) then the method will not be invoked.
61   * If a CallMethodRule is expecting more than one parameter, then it is always invoked, regardless of whether the
62   * parameters were available or not; missing parameters are converted to the appropriate target type by calling
63   * ConvertUtils.convert. Note that the default ConvertUtils converters for the String type returns a null when passed a
64   * null, meaning that CallMethodRule will passed null for all String parameters for which there is no parameter info
65   * available from the XML. However parameters of type Float and Integer will be passed a real object containing a zero
66   * value as that is the output of the default ConvertUtils converters for those types when passed a null. You can
67   * register custom converters to change this behavior; see the BeanUtils library documentation for more info.
68   * </p>
69   * <p>
70   * Note that when a constructor is used with paramCount=0, indicating that the body of the element is to be passed to
71   * the target method, an empty element will cause an <i>empty string</i> to be passed to the target method, not null.
72   * And if automatic type conversion is being applied (ie if the target function takes something other than a string as a
73   * parameter) then the conversion will fail if the converter class does not accept an empty string as valid input.
74   * </p>
75   * <p>
76   * CallMethodRule has a design flaw which can cause it to fail under certain rule configurations. All CallMethodRule
77   * instances share a single parameter stack, and all CallParamRule instances simply store their data into the
78   * parameter-info structure that is on the top of the stack. This means that two CallMethodRule instances cannot be
79   * associated with the same pattern without getting scrambled parameter data. This same issue also applies when a
80   * CallMethodRule matches some element X, a different CallMethodRule matches a child element Y and some of the
81   * CallParamRules associated with the first CallMethodRule match element Y or one of its child elements. This issue has
82   * been present since the very first release of Digester. Note, however, that this configuration of CallMethodRule
83   * instances is not commonly required.
84   * </p>
85   */
86  public class CallMethodRule
87      extends Rule
88  {
89  
90      // ----------------------------------------------------------- Constructors
91  
92      /**
93       * Construct a "call method" rule with the specified method name. The parameter types (if any) default to
94       * java.lang.String.
95       *
96       * @param methodName Method name of the parent method to call
97       * @param paramCount The number of parameters to collect, or zero for a single argument from the body of this
98       *            element.
99       */
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 }