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

com.feilong.lib.digester3.CallMethodRule Maven / Gradle / Ivy

Go to download

feilong is a suite of core and expanded libraries that include utility classes, http, excel,cvs, io classes, and much much more.

There is a newer version: 4.0.8
Show newest version
package com.feilong.lib.digester3;

import static java.lang.String.format;

/*
 * 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.util.Arrays.fill;

import java.util.Formatter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

import com.feilong.lib.beanutils.ConvertUtils;
import com.feilong.lib.beanutils.MethodUtils;

/**
 * 

* 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{ /** The Constant log. */ private static final Logger LOGGER = LoggerFactory.getLogger(CallMethodRule.class); // ----------------------------------------------------------- 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 (LOGGER.isTraceEnabled()){ for (int i = 0, size = parameters.length; i < size; i++){ LOGGER.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] = ConvertUtils.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 (LOGGER.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(")"); LOGGER.debug(formatter.toString()); } Object result = null; if (useExactMatch){ // invoke using exact match result = MethodUtils.invokeExactMethod(target, methodName, paramValues, paramTypes); }else{ // invoke using fuzzier match result = MethodUtils.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(@SuppressWarnings("unused") 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