All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.digester3.CallMethodRule Maven / Gradle / Ivy

package org.apache.commons.digester3;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import static java.lang.System.arraycopy;
import static java.lang.String.format;
import static java.util.Arrays.fill;
import static org.apache.commons.beanutils.ConvertUtils.convert;
import static org.apache.commons.beanutils.MethodUtils.invokeExactMethod;
import static org.apache.commons.beanutils.MethodUtils.invokeMethod;

import java.util.Formatter;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * 

* Rule implementation that calls a method on an object on the stack (normally the top/parent object), passing arguments * collected from subsequent CallParamRule rules or from the body of this element. *

*

* By using {@link #CallMethodRule(String methodName)} a method call can be made to a method which accepts no arguments. *

*

* Incompatible method parameter types are converted using org.apache.commons.beanutils.ConvertUtils. *

*

* This rule now uses {@link org.apache.commons.beanutils.MethodUtils#invokeMethod} by default. * This increases the kinds of methods successfully and allows primitives to be matched by passing in wrapper classes. * There are rare cases when {@link org.apache.commons.beanutils.MethodUtils#invokeExactMethod} (the old default) is * required. This method is much stricter in it's reflection. * Setting the UseExactMatch to true reverts to the use of this method. *

*

* Note that the target method is invoked when the end of the tag the CallMethodRule fired on is encountered, * not when the last parameter becomes available. This implies that rules which fire on tags nested within the * one associated with the CallMethodRule will fire before the CallMethodRule invokes the target method. This behavior * is not configurable. *

*

* Note also that if a CallMethodRule is expecting exactly one parameter and that parameter is not available (eg * CallParamRule is used with an attribute name but the attribute does not exist) then the method will not be invoked. * If a CallMethodRule is expecting more than one parameter, then it is always invoked, regardless of whether the * parameters were available or not; missing parameters are converted to the appropriate target type by calling * ConvertUtils.convert. Note that the default ConvertUtils converters for the String type returns a null when passed a * null, meaning that CallMethodRule will passed null for all String parameters for which there is no parameter info * available from the XML. However parameters of type Float and Integer will be passed a real object containing a zero * value as that is the output of the default ConvertUtils converters for those types when passed a null. You can * register custom converters to change this behavior; see the BeanUtils library documentation for more info. *

*

* Note that when a constructor is used with paramCount=0, indicating that the body of the element is to be passed to * the target method, an empty element will cause an empty string to be passed to the target method, not null. * And if automatic type conversion is being applied (ie if the target function takes something other than a string as a * parameter) then the conversion will fail if the converter class does not accept an empty string as valid input. *

*

* CallMethodRule has a design flaw which can cause it to fail under certain rule configurations. All CallMethodRule * instances share a single parameter stack, and all CallParamRule instances simply store their data into the * parameter-info structure that is on the top of the stack. This means that two CallMethodRule instances cannot be * associated with the same pattern without getting scrambled parameter data. This same issue also applies when a * CallMethodRule matches some element X, a different CallMethodRule matches a child element Y and some of the * CallParamRules associated with the first CallMethodRule match element Y or one of its child elements. This issue has * been present since the very first release of Digester. Note, however, that this configuration of CallMethodRule * instances is not commonly required. *

*/ public class CallMethodRule extends Rule { // ----------------------------------------------------------- Constructors /** * Construct a "call method" rule with the specified method name. The parameter types (if any) default to * java.lang.String. * * @param methodName Method name of the parent method to call * @param paramCount The number of parameters to collect, or zero for a single argument from the body of this * element. */ public CallMethodRule( String methodName, int paramCount ) { this( 0, methodName, paramCount ); } /** * Construct a "call method" rule with the specified method name. The parameter types (if any) default to * java.lang.String. * * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on * the stack. * @param methodName Method name of the parent method to call * @param paramCount The number of parameters to collect, or zero for a single argument from the body of this * element. */ public CallMethodRule( int targetOffset, String methodName, int paramCount ) { this.targetOffset = targetOffset; this.methodName = methodName; this.paramCount = paramCount; if ( paramCount == 0 ) { this.paramTypes = new Class[] { String.class }; } else { this.paramTypes = new Class[paramCount]; fill( this.paramTypes, String.class ); } } /** * Construct a "call method" rule with the specified method name. The method should accept no parameters. * * @param methodName Method name of the parent method to call */ public CallMethodRule( String methodName ) { this( 0, methodName, 0, (Class[]) null ); } /** * Construct a "call method" rule with the specified method name. The method should accept no parameters. * * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on * the stack. * @param methodName Method name of the parent method to call */ public CallMethodRule( int targetOffset, String methodName ) { this( targetOffset, methodName, 0, (Class[]) null ); } /** * Construct a "call method" rule with the specified method name and parameter types. If paramCount is * set to zero the rule will use the body of this element as the single argument of the method, unless * paramTypes is null or empty, in this case the rule will call the specified method with no arguments. * * @param methodName Method name of the parent method to call * @param paramCount The number of parameters to collect, or zero for a single argument from the body of ths element * @param paramTypes The Java class names of the arguments (if you wish to use a primitive type, specify the * corresonding Java wrapper class instead, such as java.lang.Boolean for a * boolean parameter) */ public CallMethodRule( String methodName, int paramCount, String[] paramTypes ) { this( 0, methodName, paramCount, paramTypes ); } /** * Construct a "call method" rule with the specified method name and parameter types. If paramCount is * set to zero the rule will use the body of this element as the single argument of the method, unless * paramTypes is null or empty, in this case the rule will call the specified method with no arguments. * * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on * the stack. * @param methodName Method name of the parent method to call * @param paramCount The number of parameters to collect, or zero for a single argument from the body of the element * @param paramTypes The Java class names of the arguments (if you wish to use a primitive type, specify the * corresponding Java wrapper class instead, such as java.lang.Boolean for a * boolean parameter) */ public CallMethodRule( int targetOffset, String methodName, int paramCount, String[] paramTypes ) { this.targetOffset = targetOffset; this.methodName = methodName; this.paramCount = paramCount; if ( paramTypes == null ) { this.paramTypes = new Class[paramCount]; fill( this.paramTypes, String.class ); } else { // copy the parameter class names into an array // the classes will be loaded when the digester is set this.paramClassNames = new String[paramTypes.length]; arraycopy( paramTypes, 0, this.paramClassNames, 0, paramTypes.length ); } } /** * Construct a "call method" rule with the specified method name and parameter types. If paramCount is * set to zero the rule will use the body of this element as the single argument of the method, unless * paramTypes is null or empty, in this case the rule will call the specified method with no arguments. * * @param methodName Method name of the parent method to call * @param paramCount The number of parameters to collect, or zero for a single argument from the body of the element * @param paramTypes The Java classes that represent the parameter types of the method arguments (if you wish to use * a primitive type, specify the corresponding Java wrapper class instead, such as * java.lang.Boolean.TYPE for a boolean parameter) */ public CallMethodRule( String methodName, int paramCount, Class paramTypes[] ) { this( 0, methodName, paramCount, paramTypes ); } /** * Construct a "call method" rule with the specified method name and parameter types. If paramCount is * set to zero the rule will use the body of this element as the single argument of the method, unless * paramTypes is null or empty, in this case the rule will call the specified method with no arguments. * * @param targetOffset location of the target object. Positive numbers are relative to the top of the digester * object stack. Negative numbers are relative to the bottom of the stack. Zero implies the top object on * the stack. * @param methodName Method name of the parent method to call * @param paramCount The number of parameters to collect, or zero for a single argument from the body of the element * @param paramTypes The Java classes that represent the parameter types of the method arguments (if you wish to use * a primitive type, specify the corresponding Java wrapper class instead, such as * java.lang.Boolean.TYPE for a boolean parameter) */ public CallMethodRule( int targetOffset, String methodName, int paramCount, Class[] paramTypes ) { this.targetOffset = targetOffset; this.methodName = methodName; this.paramCount = paramCount; if ( paramTypes == null ) { this.paramTypes = new Class[paramCount]; fill( this.paramTypes, String.class ); } else { this.paramTypes = new Class[paramTypes.length]; arraycopy( paramTypes, 0, this.paramTypes, 0, paramTypes.length ); } } // ----------------------------------------------------- Instance Variables /** * The body text collected from this element. */ protected String bodyText = null; /** * location of the target object for the call, relative to the top of the digester object stack. The default value * of zero means the target object is the one on top of the stack. */ protected int targetOffset = 0; /** * The method name to call on the parent object. */ protected String methodName = null; /** * The number of parameters to collect from MethodParam rules. If this value is zero, a single * parameter will be collected from the body of this element. */ protected int paramCount = 0; /** * The parameter types of the parameters to be collected. */ protected Class[] paramTypes = null; /** * The names of the classes of the parameters to be collected. This attribute allows creation of the classes to be * postponed until the digester is set. */ private String[] paramClassNames = null; /** * Should MethodUtils.invokeExactMethod be used for reflection. */ private boolean useExactMatch = false; // --------------------------------------------------------- Public Methods /** * Should MethodUtils.invokeExactMethod be used for the reflection. * * @return true, if MethodUtils.invokeExactMethod Should be used for the reflection, * false otherwise */ public boolean getUseExactMatch() { return useExactMatch; } /** * Set whether MethodUtils.invokeExactMethod should be used for the reflection. * * @param useExactMatch The MethodUtils.invokeExactMethod flag */ public void setUseExactMatch( boolean useExactMatch ) { this.useExactMatch = useExactMatch; } /** * {@inheritDoc} */ @Override public void setDigester( Digester digester ) { // call superclass super.setDigester( digester ); // if necessary, load parameter classes if ( this.paramClassNames != null ) { this.paramTypes = new Class[paramClassNames.length]; for ( int i = 0; i < this.paramClassNames.length; i++ ) { try { this.paramTypes[i] = digester.getClassLoader().loadClass( this.paramClassNames[i] ); } catch ( ClassNotFoundException e ) { throw new RuntimeException( format( "[CallMethodRule] Cannot load class %s at position %s", this.paramClassNames[i], i ), e ); } } } } /** * {@inheritDoc} */ @Override public void begin( String namespace, String name, Attributes attributes ) throws Exception { // Push an array to capture the parameter values if necessary if ( paramCount > 0 ) { Object parameters[] = new Object[paramCount]; fill( parameters, null ); getDigester().pushParams( parameters ); } } /** * {@inheritDoc} */ @Override public void body( String namespace, String name, String text ) throws Exception { if ( paramCount == 0 ) { this.bodyText = text.trim(); } } /** * {@inheritDoc} */ @Override public void end( String namespace, String name ) throws Exception { // Retrieve or construct the parameter values array Object[] parameters; if ( paramCount > 0 ) { parameters = getDigester().popParams(); if ( getDigester().getLogger().isTraceEnabled() ) { for ( int i = 0, size = parameters.length; i < size; i++ ) { getDigester().getLogger().trace( format( "[CallMethodRule]{%s} parameters[%s]=%s", getDigester().getMatch(), i, parameters[i] ) ); } } // In the case where the target method takes a single parameter // and that parameter does not exist (the CallParamRule never // executed or the CallParamRule was intended to set the parameter // from an attribute but the attribute wasn't present etc) then // skip the method call. // // This is useful when a class has a "default" value that should // only be overridden if data is present in the XML. I don't // know why this should only apply to methods taking *one* // parameter, but it always has been so we can't change it now. if ( paramCount == 1 && parameters[0] == null ) { return; } } else if ( paramTypes != null && paramTypes.length != 0 ) { // Having paramCount == 0 and paramTypes.length == 1 indicates // that we have the special case where the target method has one // parameter being the body text of the current element. // There is no body text included in the source XML file, // so skip the method call if ( bodyText == null ) { return; } parameters = new Object[] { bodyText }; if ( paramTypes.length == 0 ) { paramTypes = new Class[] { String.class }; } } else { // When paramCount is zero and paramTypes.length is zero it // means that we truly are calling a method with no parameters. // Nothing special needs to be done here. parameters = new Object[0]; paramTypes = new Class[0]; } // Construct the parameter values array we will need // We only do the conversion if the param value is a String and // the specified paramType is not String. Object[] paramValues = new Object[paramTypes.length]; for ( int i = 0; i < paramTypes.length; i++ ) { // convert nulls and convert stringy parameters // for non-stringy param types if ( parameters[i] == null || ( parameters[i] instanceof String && !String.class.isAssignableFrom( paramTypes[i] ) ) ) { paramValues[i] = convert( (String) parameters[i], paramTypes[i] ); } else { paramValues[i] = parameters[i]; } } // Determine the target object for the method call Object target; if ( targetOffset >= 0 ) { target = getDigester().peek( targetOffset ); } else { target = getDigester().peek( getDigester().getCount() + targetOffset ); } if ( target == null ) { throw new SAXException( format( "[CallMethodRule]{%s} Call target is null (targetOffset=%s, stackdepth=%s)", getDigester().getMatch(), targetOffset, getDigester().getCount() ) ); } // Invoke the required method on the top object if ( getDigester().getLogger().isDebugEnabled() ) { Formatter formatter = new Formatter().format( "[CallMethodRule]{%s} Call %s.%s(", getDigester().getMatch(), target.getClass().getName(), methodName ); for ( int i = 0; i < paramValues.length; i++ ) { formatter.format( "%s%s/%s", ( i > 0 ? ", " : "" ), paramValues[i], paramTypes[i].getName() ); } formatter.format( ")" ); getDigester().getLogger().debug( formatter.toString() ); } Object result = null; if ( useExactMatch ) { // invoke using exact match result = invokeExactMethod( target, methodName, paramValues, paramTypes ); } else { // invoke using fuzzier match result = invokeMethod( target, methodName, paramValues, paramTypes ); } processMethodCallResult( result ); } /** * {@inheritDoc} */ @Override public void finish() throws Exception { bodyText = null; } /** * Subclasses may override this method to perform additional processing of the invoked method's result. * * @param result the Object returned by the method invoked, possibly null */ protected void processMethodCallResult( Object result ) { // do nothing } /** * {@inheritDoc} */ @Override public String toString() { Formatter formatter = new Formatter().format( "CallMethodRule[methodName=%s, paramCount=%s, paramTypes={", methodName, paramCount ); if ( paramTypes != null ) { for ( int i = 0; i < paramTypes.length; i++ ) { formatter.format( "%s%s", ( i > 0 ? ", " : "" ), ( paramTypes[i] != null ? paramTypes[i].getName() : "null" ) ); } } formatter.format( "}]" ); return ( formatter.toString() ); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy