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    }