Package org.apache.beehive.netui.tags.databinding.invoke

Source Code of org.apache.beehive.netui.tags.databinding.invoke.AbstractCallMethod

/*
* Copyright 2004 The Apache Software Foundation.
*
* Licensed 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.
*
* $Header:$
*/
package org.apache.beehive.netui.tags.databinding.invoke;

import org.apache.beehive.netui.util.internal.InternalStringBuilder;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.jsp.JspException;

import org.apache.beehive.netui.tags.AbstractClassicTag;
import org.apache.beehive.netui.util.Bundle;
import org.apache.beehive.netui.util.logging.Logger;
import org.apache.beehive.netui.util.type.TypeUtils;

/**
* <p>
* An abstract base class for tags that are capable of reflectively invoking methods.  Specializations of this
* tag provide method implementations that locate the object on which to invoke the method and that handle
* any return value from the invoked method.
* <p/>
* <p>
* The <code>CallMethod</code> tag can have child tags of type {@link MethodParameter}; these tags must be in the same
* order as the parameter list in the method signature of the method that will be invoked.  To invoke an overloaded
* method, the {@link MethodParameter#setType(String)} property must be set to the String name of the type to pass
* to the method.  If the type attribute values on nested {@link MethodParameter} tags do not match any method
* signature, an error will be reported in the page.
* </p>
*/
public abstract class AbstractCallMethod
        extends AbstractClassicTag {

    private static final Logger LOGGER = Logger.getInstance(AbstractCallMethod.class);

    private static final Object[] EMPTY_ARGS = new Object[0];
    private static final String EMPTY_STRING = "";
    private static final List EMPTY_LIST = new ArrayList();

    private List _parameters = null;
    private String _method = null;
    private boolean _failOnError = true;
    private String _resultId = null;
    private boolean _verifyTypes = false;

    /**
     * Sets the identifier at which the result of invoking the method will stored.  Once stored, the
     * result of the reflective invocation will be available via the JSP EL implicit object
     * <code>${pageScope}</code> with the attribute name set via this property.
     *
     * @param resultId a String that names an attribute in the PageContext's
     *                 attribute map where any resulting object will be stored.
     * @jsptagref.attributedescription
     * Sets the identifier at which the result of invoking the method will stored.  Once stored, the
     * result of the reflective invocation will be available via the JSP EL implicit object
     * <code>${pageScope}</code> with the attribute name set via this property.
     * @netui:attribute required="false"
     */
    public void setResultId(String resultId) {
        _resultId = resultId;
    }

    /**
     * Sets whether or not to report exceptions to the page when errors occur invoking a method on an object.
     *
     * @param failOnError a boolean that defines whether or not exceptions should be thrown when invocation fails.
     * @jsptagref.attributedescription
     * Sets whether or not to report exceptions to the page when errors occur invoking a method on an object.
     * @netui:attribute required="false"
     */
    public void setFailOnError(boolean failOnError) {
        _failOnError = failOnError;
    }

    /**
     * Sets the name of a method to invoke on the target object.
     *
     * @param method the name of the method to invoke
     * @jsptagref.attributedescription
     * Sets the name of a method to invoke on the target object.
     * @netui:attribute required="true"
     */
    public void setMethod(String method) {
        _method = method;
    }

    /**
     * Add a paramter that will be passed as an argument to the method that will be invoked.  This method
     * is implemented to allow the the {@link MethodParameter} tags to register their parameters.  This
     * object is passed in the position that it appeared in the set of child {@link MethodParameter} tags.
     *
     * @param type      a String of the type or class name of this parameter
     * @param parameter an object that should be passed as an argument to the invoked method
     * @see MethodParameter
     */
    public void addParameter(String type, Object parameter) {
        if(_parameters == null)
            _parameters = new ArrayList();

        // only check the types if necessary
        if(type != null) _verifyTypes = true;

        ParamNode pn = new ParamNode();
        pn.typeName = type;
        pn.paramValue = parameter;

        _parameters.add(pn);
    }

    /**
     * Causes the body of this tag to be rendered; only {@link MethodParameter} tags are allowed to be
     * contained inside of this tag.  The output of rendering the body is never written into the
     * output stream of the servlet.
     *
     * @return {@link #EVAL_BODY_BUFFERED}
     */
    public int doStartTag()
            throws JspException {
        return EVAL_BODY_BUFFERED;
    }

    /**
     * Reflectively invokes the method specified by the <code>method</code> attribute,
     * {@link #findMethod(Object, String, boolean)}.  The arguments passed to the method are taken from any nested
     * {@link MethodParameter} tags.  When the parameters which are added by the
     * {@link MethodParameter} tags are {@link java.lang.String} types, an attempt is made to convert each of
     * these parameters into the type expected by the method.  This conversion is done using the
     * {@link TypeUtils#convertToObject(java.lang.String, java.lang.Class)} method.  If a String can not
     * be converted do the type expected by the method, an exception is thrown and the error is reported
     * in the tag.  Any return value that results from invoking the given method is passed to the
     * subclass implementation of the method {@link #handleReturnValue(java.lang.Object)}.
     *
     * @return {@link #EVAL_PAGE} to continue evaluating the page
     * @throws JspException if there are errors.  All exceptions that may be thrown
     *                      in the process of reflectively invoking the method and performing type
     *                      conversion are reported as {@link JspException}
     * @see #findMethod(Object, String, boolean)
     * @see #handleReturnValue(java.lang.Object)
     * @see MethodParameter
     * @see ObjectNotFoundException
     * @see TypeUtils#convertToObject(java.lang.String, java.lang.Class)
     * @see java.lang.String
     */
    public int doEndTag()
            throws JspException {
        // find the object on which to invoke the method
        Object object = null;
        try {
            object = resolveObject();
        }
        catch(ObjectNotFoundException onf) {
            Throwable cause = (onf.getCause() != null ? onf.getCause() : onf);
            String msg = Bundle.getErrorString("Tags_AbstractCallMethod_noSuchObject", new Object[]{getObjectName(), _method, cause});
            registerTagError(msg, null);
        }

        // if this tag can accept null invocation targets,
        if(object == null) {
            if(allowNullInvocationTarget()) {
                // each implementation does this on their own
                handleReturnValue(null);
                localRelease();
                return EVAL_PAGE;
            }
            else {
                String msg = Bundle.getErrorString("Tags_AbstractCallMethod_objectIsNull", new Object[]{getObjectName(), _method});
                registerTagError(msg, null);
            }
        }

        if(hasErrors()) {
            reportErrors();
            localRelease();
            return EVAL_PAGE;
        }

        Method m = findMethod(object, _method, _verifyTypes);

        if(m == null) {
            String msg = null;
            if(_verifyTypes) {
                String paramString = prettyPrintParameterTypes(_parameters);
                msg = Bundle.getErrorString("Tags_AbstractCallMethod_noSuchMethodWithTypes",
                        new Object[]{_method,
                                     (_parameters != null ? new Integer(_parameters.size()) : new Integer(0)),
                                     paramString,
                                     getObjectName()});
            }
            else
                msg = Bundle.getErrorString("Tags_AbstractCallMethod_noSuchMethod",
                        new Object[]{_method,
                                     (_parameters != null ? new Integer(_parameters.size()) : new Integer(0)),
                                     getObjectName()});

            registerTagError(msg, null);
            reportErrors();
            localRelease();
            return EVAL_PAGE;
        }

        Object[] args = null;
        try {
            args = getArguments(m.getParameterTypes());
        }
        catch(IllegalArgumentException iae) {
            registerTagError(iae.getMessage(), null);
            reportErrors();
            localRelease();
            return EVAL_PAGE;
        }
       
        // invoke method
        Object result = null;
        try {
            if(LOGGER.isDebugEnabled()) {
                LOGGER.debug("method: " + m.toString());
                for(int i = 0; i < args.length; i++)
                    LOGGER.debug("arg[" + i + "]: " + (args[i] != null ? args[i].getClass().getName() : "null"));
            }

            result = m.invoke(object, args);
        }
        catch(Exception e) {
            assert e instanceof IllegalAccessException || e instanceof InvocationTargetException || e instanceof IllegalArgumentException;

            if(LOGGER.isErrorEnabled())
                LOGGER.error("Could not invoke method \"" + _method + "\" on the object named \"" + getObjectName() + "\" because: " + e, e);

            if(_failOnError) {
                String msg = Bundle.getErrorString("Tags_AbstractCallMethod_invocationError", new Object[]{_method, getObjectName(), e});
                registerTagError(msg, null);
                reportErrors();
                localRelease();
                return EVAL_PAGE;
            }
        }

        if(LOGGER.isDebugEnabled()) {
            LOGGER.debug((result != null ?
                    "return value is non-null and is of type \"" + result.getClass().getName() + "\"" :
                    "return value is null."));
        }

        // each implementation handles this differently
        handleReturnValue(result);

        localRelease();

        return EVAL_PAGE;
    }

    /**
     * Reset all of the fields of this tag.
     */
    protected void localRelease() {
        super.localRelease();
        _parameters = null;
        _method = null;
        _failOnError = true;
        _resultId = null;
        _verifyTypes = false;
    }

    /**
     * <p/>
     * Resolve the object on which the method should be invoked.  If there are errors resolving this object,
     * this method will throw an {@link ObjectNotFoundException}.
     * </p>
     * <p>
     * If the object is not found but no exception occurred, this method returns <code>null</code>.
     * </p>
     *
     * @return the object on which to reflectively invoke the method or <code>null</code> if the
     *         object was not found and no exception occurred.
     * @throws ObjectNotFoundException if an exception occurred attempting to resolve an object
     */
    protected abstract Object resolveObject() throws ObjectNotFoundException, JspException;

    /**
     * Get the name of the object that is the target of the invocation.  This is a generic method for this
     * tag that enables more specific error reporting.
     *
     * @return a name for the object on which the method will be invoked.
     */
    protected abstract String getObjectName();

    /**
     * When implemented to return true, this method allows a tag invoking a method to
     * accept a null invocation target and simply return null.  The default
     * implementation returns false.
     *
     * @return true if the object on which to invoke the method can be null; false otherwise.
     */
    protected boolean allowNullInvocationTarget() {
        return false;
    }

    /**
     * <p/>
     * A method that allows concrete classes to handle the result of the
     * reflective invocation in an implementation specific way.
     * </p>
     * <p/>
     * The default beahavior is to set the return value resulting from invoking the method
     * in the {@link javax.servlet.jsp.PageContext} attribute map of the current JSP page.
     * The result is set as an attribute if the <code>result</code> is not null and the
     * {@link CallMethod#setResultId(java.lang.String)} String is not null.  If the value returned
     * from calling a method is null and the {@link CallMethod#setResultId(java.lang.String)} is non-null,
     * the {@link javax.servlet.jsp.PageContext#removeAttribute(java.lang.String)}
     * is called to remove the attribute from the attribute map.
     * </p>
     *
     * @param result the object that was returned by calling the method on the object
     */
    protected void handleReturnValue(Object result) {
        if(_resultId != null) {
            if(result != null) {
                if(LOGGER.isInfoEnabled() && pageContext.getAttribute(_resultId) != null)
                    LOGGER.info("Overwriting attribute named \"" + _resultId + "\" in the PageContext with a new attribute of type \"" +
                            result.getClass().getName() + "\" returned from calling the method \"" + _method + "\" on an object named \"" +
                            getObjectName() + "\".");

                pageContext.setAttribute(_resultId, result);
            }
            else {
                if(LOGGER.isInfoEnabled())
                    LOGGER.info("Removing attribute named \"" + _resultId + "\" from the PageContext.  " +
                            "The value returned from calling the method \"" + _method + "\" on an object named \"" +
                            getObjectName() + "\" is null.");

                pageContext.removeAttribute(_resultId);
            }
        }
    }

    /**
     * Internal, read-only property used by subclasses to get
     * the list of parameters to be used when reflectively
     * invoking a method.  If the method takes no parameters, this
     * list will be of size zero.
     *
     * @return the list of parameters
     */
    protected List getParameterNodes() {
        if(_parameters != null)
            return _parameters;
        else
            return EMPTY_LIST;
    }

    /**
     * The default findMethod implementation is an uncached search of all
     * of the methods available on the Class of the <code>target</code>
     *
     * @param target      the object from which to find the method
     * @param methodName  the name of the method to find
     * @param verifyTypes a boolean that if true will match the type names in addition to the String method name
     * @return a Method object matching the methodName and types, if <code>verifyTypes</code> is true.
     *         <code>null</code> otherwise.
     */
    protected Method findMethod(Object target, String methodName, boolean verifyTypes) {
        int paramCount = (_parameters != null ? _parameters.size() : 0);
        Method[] methods = target.getClass().getMethods();

        for(int i = 0; i < methods.length; i++) {
            if(methods[i].getName().equals(_method) && methods[i].getParameterTypes().length == paramCount) {
                if(LOGGER.isDebugEnabled()) {
                    LOGGER.debug("found method: " + methods[i]);
                    LOGGER.debug("check types: " + verifyTypes);
                }

                // page asked to check types
                if(verifyTypes) {
                    boolean match = true;
                    // the lengths of these match b/c of the check above
                    Class[] parameterTypes = methods[i].getParameterTypes();
                    for(int j = 0; j < parameterTypes.length; j++) {
                        if(LOGGER.isDebugEnabled()) {
                            LOGGER.debug("parameterTypes[" + j + "]: " + parameterTypes[j]);
                            LOGGER.debug("paramNode[" + j + "]: " + ((ParamNode)_parameters.get(j)).typeName);
                        }

                        // check the name of the class and the name of the parameter type
                        if(!parameterTypes[j].getName().equals(((ParamNode)_parameters.get(j)).typeName)) {
                            match = false;
                            break;
                        }

                    }
                    if(match) return methods[i];
                }
                else
                    return methods[i];
            }
        }

        return null;
    }

    /**
     * Convert the arguments for a method from Strings set as attributes
     * on JSP tags to the types represented by teh list of Class[] objects
     * provided here.
     *
     * @return an Object[] that contains the parameters to pass to the method
     * @throws IllegalArgumentException if an error occurs converting an
     *                                  argument to a specific type.
     */
    private final Object[] getArguments(Class[] paramTypes) {
        if(_parameters == null)
            return EMPTY_ARGS;

        Object[] args = new Object[paramTypes.length];

        for(int i = 0; i < _parameters.size(); i++) {
            ParamNode pn = (ParamNode)_parameters.get(i);
            // if the parameter should have been null, leave it null
            if(pn.paramValue == MethodParameter.NULL_ARG)
                continue;

            Object value = pn.paramValue;
            try {
                // if the value wasn't a String, it may have come from EL so don't try to convert it
                if(!(value instanceof String) || value == null)
                    args[i] = value;
                // here, there's a non-null String value
                else
                    args[i] = TypeUtils.convertToObject((String)value, paramTypes[i]);
            }
                    // catch Exception here because almost anything can be thrown by TypeUtils.convertToObject().
            catch(Exception e) {
                String msg = Bundle.getErrorString("Tags_AbstractCallMethod_parameterError",
                        new Object[]{paramTypes[i], new Integer(i), value, e.toString()});
                throw new IllegalArgumentException(msg);
            }

            if(LOGGER.isDebugEnabled()) LOGGER.debug("argTypes[" + i + "]: " + paramTypes[i]);
        }

        return args;
    }

    /**
     * Utility method that pretty-prints the types of the parameters
     * passed to a method; this is used in debugging.
     *
     * @param parameters the list of parameters
     * @return a String that represents the types of each of these paramters in order
     */
    private static final String prettyPrintParameterTypes(List parameters) {
        InternalStringBuilder paramString = null;
        if(parameters != null) {
            paramString = new InternalStringBuilder(128);
            paramString.append("(");
            for(int i = 0; i < parameters.size(); i++) {
                if(i > 0)
                    paramString.append(", ");

                paramString.append(((ParamNode)parameters.get(i)).typeName);
            }
            paramString.append(")");

            return paramString.toString();
        }
        else
            return EMPTY_STRING;
    }

    /**
     * An internal struct that represents a parameter that will be passed to a
     * reflective method invocation call.  Instances of <code>ParamNode</code>
     * map 1:1 to the methodParameter tags that appear within the body of
     * an AbstrctCallMethod tag.
     *
     * @exclude
     */
    protected class ParamNode {

        /**
         * The fully qualified class name of the parameter type.  This value
         * can be null if parameter type checking does not need to occur.
         */
        public String typeName = null;

        /**
         * The value of the parameter.  Often, this is a String expression
         * which is evaluated later and converted into some Object
         * type such as Integer or Foobar.
         */
        public Object paramValue = null;

        public String toString() {
            InternalStringBuilder buf = new InternalStringBuilder(32);
            buf.append("typeName: ").append(typeName);
            return buf.toString();
        }
    }
}
TOP

Related Classes of org.apache.beehive.netui.tags.databinding.invoke.AbstractCallMethod

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.