001package 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
022import static java.lang.System.arraycopy;
023import static java.lang.String.format;
024import static java.util.Arrays.fill;
025import static org.apache.commons.beanutils.ConvertUtils.convert;
026import static org.apache.commons.beanutils.MethodUtils.invokeExactMethod;
027import static org.apache.commons.beanutils.MethodUtils.invokeMethod;
028
029import java.util.Formatter;
030
031import org.xml.sax.Attributes;
032import 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 */
086public 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}