Java tutorial
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; /** * <p> * Rule implementation that calls a method on an object on the stack (normally the top/parent object), passing arguments * collected from subsequent <code>CallParamRule</code> rules or from the body of this element. * </p> * <p> * By using {@link #CallMethodRule(String methodName)} a method call can be made to a method which accepts no arguments. * </p> * <p> * Incompatible method parameter types are converted using <code>org.apache.commons.beanutils.ConvertUtils</code>. * </p> * <p> * 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 <code>UseExactMatch</code> to true reverts to the use of this method. * </p> * <p> * Note that the target method is invoked when the <i>end</i> of the tag the CallMethodRule fired on is encountered, * <i>not</i> 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. * </p> * <p> * 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. * </p> * <p> * 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 <i>empty string</i> 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. * </p> * <p> * 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. * </p> */ 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 <code>paramCount</code> is * set to zero the rule will use the body of this element as the single argument of the method, unless * <code>paramTypes</code> 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 <code>java.lang.Boolean</code> for a * <code>boolean</code> 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 <code>paramCount</code> is * set to zero the rule will use the body of this element as the single argument of the method, unless * <code>paramTypes</code> 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 <code>java.lang.Boolean</code> for a * <code>boolean</code> 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 <code>paramCount</code> is * set to zero the rule will use the body of this element as the single argument of the method, unless * <code>paramTypes</code> 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 * <code>java.lang.Boolean.TYPE</code> for a <code>boolean</code> 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 <code>paramCount</code> is * set to zero the rule will use the body of this element as the single argument of the method, unless * <code>paramTypes</code> 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 * <code>java.lang.Boolean.TYPE</code> for a <code>boolean</code> 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 <code>MethodParam</code> 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 <code>MethodUtils.invokeExactMethod</code> be used for reflection. */ private boolean useExactMatch = false; // --------------------------------------------------------- Public Methods /** * Should <code>MethodUtils.invokeExactMethod</code> be used for the reflection. * * @return true, if <code>MethodUtils.invokeExactMethod</code> Should be used for the reflection, * false otherwise */ public boolean getUseExactMatch() { return useExactMatch; } /** * Set whether <code>MethodUtils.invokeExactMethod</code> should be used for the reflection. * * @param useExactMatch The <code>MethodUtils.invokeExactMethod</code> 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()); } }