/*  Copyright (c) 2000 by Oracle Corporation. All Rights Reserved. */

package oracle.portal.utils.xml.v1;

import java.lang.reflect.*;
import java.io.*;
import java.text.*;
import java.util.*;

import org.w3c.dom.*;
import org.xml.sax.*;
import oracle.xml.parser.v2.*;

/**
 * A <code>DefaultNodeHandler</code> is capable of translating an XML DOM
 * object tree into an analogous hierarchy of application objects, whose
 * classes, attributes and associations all reflect the configuration of the
 * nodes in the XML tree. The class therefore provides an extensible mechanism
 * for configuring a potentially complex system of Java objects through an XML
 * document.<p>
 *
 * The main entry point is the {@link #processNode(Node,Object)} method, which
 * recursively processes the nodes in the XML document and builds up a
 * corresponding tree of application objects. Each call to the method processes
 * all the children of a given XML node in the context of a particular
 * application object represented by that node. For each of the given node's
 * child nodes in turn, the method proceeds as follows:
 *
 * <ul>
 *
 * <li>If the child node is a <i>complex</i> element, that is, an element node
 * with either further element nodes as children, or no element or text node
 * children at all, it is considered to correspond to a new application object.
 * The precise type of this application object is determined by the overridable
 * {@link #getApplicationObjectForElement(Element)} method, which returns a new
 * instance of an appropriate application object class. Given this new object,
 * <code>processNode</code> records a parent-child association between the
 * context application object and the new object. Finally, a recursive call to
 * <processNode> is made in order to process the child node in the context of
 * the new application object. However, this call may actually be redirected to
 * the <code>processNode</code> method of a different {@link NodeHandler}
 * object, depending on the value the overridable {@link
 * #getHandlerForElement(Element)} method returns for the complex element. This
 * mechanism is designed to allow the 'delegation' of the processing of certain
 * complex elements to customized <code>NodeHandler</code>s.</li>
 *
 * <li>If the child node is a <i>simple</i> element, that is, an element node
 * which has a text node as a child and no other element children, it is
 * considered to correspond to a particular property of the context application
 * object. In this case, the overridable {@link
 * #handleSimpleElement(Element,Object)} method is used to set a corresponding
 * property on the context application object to the 'value' of the simple
 * element (derived from the String value of the element's first text node
 * child).</li>
 *
 * <li>If the child node is any other type of node, then it is ignored.</li>
 *
 * </ul>
 *
 * In this default implementation, the following restrictions are placed on
 * complex elements and the classes of the objects they describe:
 *
 * <ul>
 *
 *   <li>Each complex element <b>must</b> have a <code>class</code> attribute,
 *   containing the fully qualified name of the corresponding application
 *   object class.</li>
 *
 *   <li>The application object class should have a public zero-argument
 *   constructor.</li>
 *
 *   <li>If initialization of an application object requires knowledge of its
 *   'context' in the tree of application objects, its class should implement
 *   the {@link InitializableXMLObject} interface. Initialization to be
 *   performed when the object is first created, perhaps relating the new
 *   object to its parent, should be placed in a
 *   <code>preInitialize(Object)</code> method, and any extra initialization
 *   and validation to be performed once the object's own children have been
 *   initialized should be placed in a <code>postInitialize()</code>
 *   method.</li>
 *
 *   <li>For each possible child of a complex element that is itself a complex
 *   element, the application object class should have a public method to
 *   record a parent-child association with the application object
 *   corresponding to the child element. The method should take the child
 *   application object as a single Object parameter, and should have a name
 *   prefixed by "set" or "add" and suffixed by the name of the child element
 *   (with initial captital letter).</li>
 *
 *   <li>For each possible child of a complex element that is a simple element,
 *   the application object class should have a method to set a corresponding
 *   property. The method should take the String value of the first text node
 *   child of the simple element as a parameter. If the parameter is to be
 *   stored as a non-String value (e.g. an int), then the method will also be
 *   responsible for converting the parameter to a value of the appropriate
 *   type, and possibly throwing an exception should the conversion fail. The
 *   same naming restrictions for the method as above apply.</li>
 *
 *   <li>If a complex element is to be handled by a {@link NodeHandler} other
 *   than the current one, then it should have a <code>handler</code>
 *   attribute, containing the fully qualified name of a class implementing
 *   <code>NodeHandler</code>.</li>
 *
 * </ul>
 *
 * In the example XML below:
 *
 * <ul>
 *
 *   <li><code>complex1</code> and <code>complex2</code> are complex elements.
 *   <code>complex1</code> describes an object of class
 *   <code>oracle.portal.ApplicationClass</code>, while <code>complex2</code>
 *   describes an object of class <code>my.custom.ApplicationClass</code>.</li>
 *
 *   <li><code>complex1</code> has no <code>handler</code> attribute, so will
 *   be processed by the same <code>NodeHandler</code> that processed its
 *   parent (the root document element). <code>complex2</code> specifies
 *   <code>my.custom.HandlerClass</code> in its <code>handler</code> attribute,
 *   so its processing will be delegated to a <code>NodeHandler</code> object
 *   of the specified class.</li>
 *
 *   <li><code>simple1</code> and <code>simple2</code> are simple elements.
 *   <code>simple1</code> controls a single property in the
 *   <code>oracle.portal.ApplicationClass</code> object, while
 *   <code>simple2</code> controls a collection of values in the same object. A
 *   corresponding method of <code>oracle.portal.ApplicationClass</code>,
 *   <code>setSimple1(String)</code> might set a single member variable to the
 *   given string value, while another method <code>addSimple2(String)</code>
 *   might add the given value to list of values held within the object.</li>
 *
 * </ul>
 *
 *   <code>
 *   <pre>
 *    &lt;complex1 class="oracle.portal.ApplicationClass">
 *      &lt;simple1>value&lt;/simple1>
 *      &lt;simple2>value1&lt;/item>
 *      &lt;simple2>value2&lt;/item>
 *      &lt;simple2>value3&lt;/item>
 *      &lt;complex2 class="my.custom.ApplicationClass" handler="my.custom.HandlerClass"/>
 *    &lt;/complex1>
 *   </pre>
 *   </code>
 */
public class DefaultNodeHandler implements NodeHandler
{
    private static final String ATTRIBUTE_CLASS = "class";
    private static final String ATTRIBUTE_HANDLER = "handler";
    private static final String ATTRIBUTE_VERSION = "version";
    private static final String ADD_METHOD_PREFIX = "add";
    private static final String SET_METHOD_PREFIX = "set";

    protected static Class[] sCustomParamType = {String.class};
    protected static Class[] sObjectParamType = {Object.class};

    public DefaultNodeHandler()
    {
    }

    /**
     * Recursively processes all the children of a given XML node in the
     * context of an application object represented by that node, in order to
     * populate the application object and create and populate other 'child'
     * application objects.
     * @param n current node whose children will be processed to populate the
     * context application object.
     * @param o the context application object.
     * @exception NodeHandlerException if unable to instantiate a child object
     * or handler object, find a method corresponding to a simple element, or
     * an exception occurs while calling a method on a child object.
     */
    public void processNode(Node n, Object o) throws NodeHandlerException
    {
        for (Node childNode = n.getFirstChild();
             childNode != null;
             childNode = childNode.getNextSibling())
        {
            if (childNode.getNodeType() != Node.ELEMENT_NODE)
            {
                continue;
            }             
            Element e = (Element)childNode;

            // If the element has further elements as children or is an 'empty'
            // element (without any element or text node children), we will
            // consider it as a 'complex' element, and call
            // getApplicationObjectForElement() and getHandlerForElement() to
            // get the corresponding application and handler objects
            // respectively.
            if(isComplexElement(e))
            {
                Object childObject = getApplicationObjectForElement(e);
                if(childObject == null)
                {
                    throw new NodeHandlerException("XML processing error: Could not determine class for object represented by element \"" + 
                                                   e.getTagName() + "\"");
                }
                NodeHandler handler = getHandlerForElement(e);
                if(handler == null)
                {
                    handler = this;
                }

                // Now attach the new application object to our hierarchy and process the
                // element's children in the context of this object

                //Invoke the pre-loading initialization of the object
                invokePreInitialize(childObject,o);

                //Load this object with a recursive call to processNode
                handler.processNode(childNode,childObject);

                //Invoke the post-loading initialization of the object
                invokePostInitialize(childObject);

                //Place this object back on parent object.
                attachObject(o,childObject,e);
            }
            // If the element does not have any element children, but has at
            // least one text node child, we will consider it as a 'simple'
            // element
            else // !hasChildElements(e) && hasChildTextNodes(e)
            {
                handleSimpleElement(e, o);
            }
        }
    }

    /**
     * Determines whether the Element is complex.<br>
     * In this implementation, if the element has further
     * elements as children or is an 'empty' element (without any element or
     * text node children), we will consider it as a 'complex' element
     */
    protected boolean isComplexElement(Element e)
    {
        return hasChildElements(e) || !hasChildTextNodes(e);
    }
    

    /** 
     * For a given 'complex' element, returns an instance of the type of object
     * it represents. In this default implementation, the method simply
     * attempts to create an instance of the class named in the element's
     * <code>class</code> attribute, if it has one. Users should override if
     * the type of object corresponding to a particular complex element can be
     * determined in a different way.
     * @param e the 'complex' element for which a corresponing object is to be
     * created
     * @return an object of the class corresponding to the element, or
     * <code>null</code> if no corresponding class can be determined for the
     * element
     * @exception NodeHandlerException if an error occurs while trying to
     * create an appropriate object.
     */
    public Object getApplicationObjectForElement(Element e)
        throws NodeHandlerException
    {
        String className = e.getAttribute(ATTRIBUTE_CLASS);
        if (className != null && className.length()!=0)
        {
            //Instantiate an object of the given class
            return createObject(className);
        }
        return null;
    }

    /** 
     * For a given 'complex' element, returns the {@link NodeHandler} that
     * should be used to process it. In this default implementation, the method
     * simply returns an instance of the class named in the element's
     * <code>handler</code> attribute, if it has one, or the current
     * <code>DefaultNodeHandler</code> otherwise. Users should override if the
     * handler for a particular complex element can be determined in a
     * different way.
     * @param e the 'complex' element whose handler is to be determined
     * @return a handler for the element
     * @exception NodeHandlerException if an error occurs while trying to
     * create an appropriate handler object.
     */
    public NodeHandler getHandlerForElement(Element e)
        throws NodeHandlerException
    {
        String handlerName = e.getAttribute(ATTRIBUTE_HANDLER);
        
        if (handlerName != null && handlerName.length() != 0 &&
            !handlerName.equals(getClass().getName()))
        {
            //The handler for this node is different to the current handler
            //so we need to make a new one.
            return createHandler(handlerName);
        }
        return this;
    }

    /** 
     * For a given 'simple' element, and the current context application
     * object, performs the steps necessary to set the value of a property of
     * the application object corresponding to the simple element, to the value
     * held by the element. In this default implementation, the method looks
     * for a method on the application object with a name prefixed by "set" or
     * "add" and suffixed by the tag name of the simple element (with initial
     * captital letter), taking a single String parameter. If such a method is
     * found, it is called with the String value of the simple element's first
     * child text node as an argument. Users should override if application
     * object properties should be set in a different way.
     * @param e a 'simple' element corresponding to a property of the
     * application object
     * @param o the application object on which the property is to be set
     * @exception NodeHandlerException if an error occurs while trying to
     * locate and set the property on the application object
     */
    public void handleSimpleElement(Element e, Object o)
        throws NodeHandlerException
    {
        //Attempt to set the attribute based on the element.
        String attrName = e.getTagName();
        String attrValue = getStringValue(e);

        if (attrName == null || attrName.length() == 0 ||
            attrValue == null || attrValue.length() == 0) {
            return;
        }

        setCustomValue(attrName, attrValue, o);
    }

    /**
     * Performs any steps required to initialize an application object after it
     * has been created, perhaps relating the new object to its parent. In this
     * default implementation, the method simply calls the object's
     * <code>preInitialize</code> method with the parent object as an argument,
     * if it implements {@link InitializableXMLObject}. Users should override
     * if there are further actions to be performed on their application
     * objects.
     * @param o the application object to be initialized
     * @param parent the application object that will become this object's parent
     * @exception NodeHandlerException if an error occurs while initializing
     * the application object
     */
    public void invokePreInitialize(Object o, Object parent) throws NodeHandlerException
    {
        if (o instanceof InitializableXMLObject)
        {
            ((InitializableXMLObject)o).preInitialize(parent);
        }
    }

    /**
     * Performs any extra steps required to initialize and validate an
     * application object once its child application objects have been created.
     * In this default implementation, the method simply calls the object's
     * <code>postInitialize</code> method, if it implements {@link
     * InitializableXMLObject}. Users should override if there are further
     * post-initialization steps to be performed on their application objects.
     * @param o the application object to be initialized
     * @param parent the application object that will become this object's parent
     * @exception NodeHandlerException if an error occurs while initializing or
     * validating the application object
     */
    public void invokePostInitialize(Object o) throws NodeHandlerException
    {
        if (o instanceof InitializableXMLObject)
        {
            ((InitializableXMLObject)o).postInitialize();
        }
    }

    protected NodeHandler createHandler(String handlerName) throws NodeHandlerException
    {
        Object candidateHandler = createObject(handlerName);

        if (!(candidateHandler instanceof NodeHandler))
        {
            throw new NodeHandlerException(
                                        "XML processing error: " + handlerName +
                                        "is not a valid NodeHandler class");
        }
        return (NodeHandler)candidateHandler;
    }

    protected Object createObject(String classname) throws NodeHandlerException
    {
        Class c;
        Object o;

        try
        {
            c = Class.forName(classname);
        }
        catch (ClassNotFoundException cnfe)
        {
            throw new NodeHandlerException(
                                        "XML processing error: unable to create object of type " +
                                        classname + " due to the following exception: " +
                                        cnfe.toString());
        }

        try
        {
            o = c.newInstance();
        }
        catch (Exception e)
        {
            throw new NodeHandlerException(
                                        "XML processing error: unable to create object of type " +
                                        classname + " due to the following exception: " +
                                        e.toString());
        }
        return o;
    }

    

    protected Method findMethod(Object o, String tagName, Class[] paramTypes)
        throws NodeHandlerException
    {
        String methodName;
        try
        {
            methodName = getMethodName(SET_METHOD_PREFIX, tagName);
            return o.getClass().getMethod(methodName, paramTypes);
        }
        catch (NoSuchMethodException nssme)
        {
            try
            {
                methodName = getMethodName(ADD_METHOD_PREFIX, tagName);
                return o.getClass().getMethod(methodName, paramTypes);
            }
            catch (NoSuchMethodException nsame)
            {
                throw new NodeHandlerException("Class " + o.getClass().getName() +
                                               " has no set or add method for tag \"" +
                                               tagName + "\"");
            }
        }
    }
    
    protected String getMethodName(String prefix, String name)
    {
        if (name.length() > 1) {
            return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
        }
        else
        {
            return prefix + name.toUpperCase();
        }
    }

    protected void setCustomValue(String name, String value, Object o) throws NodeHandlerException
    {
        Method m = findMethod(o, name, sCustomParamType);
        Object[] params = {value};
        try
        {
            m.invoke(o, params);
        }
        // 'unwrap' InvocationTargetExceptions so that we can get at their message
        catch (InvocationTargetException ite)
        {
            throw new NodeHandlerException(ite.getTargetException());
        }
        catch (Exception ex)
        {
            throw new NodeHandlerException(ex);
        }
    }

    protected void attachObject(Object o, Object childObject, Element e) throws NodeHandlerException
    {
        String name = e.getTagName();
        Method m = findMethod(o, name, sObjectParamType);
        Object[] params = {childObject};
        try
        {
            m.invoke(o, params);
        }
        // 'unwrap' InvocationTargetExceptions so that we can get at their message
        catch (InvocationTargetException ite)
        {
            throw new NodeHandlerException(ite.getTargetException());
        }
        catch (Exception ex)
        {
            throw new NodeHandlerException(ex);
        }
    }

    protected boolean hasChildElements(Node n)
    {
        return hasChildNodesOfType(n, Node.ELEMENT_NODE);
    }

    protected boolean hasChildTextNodes(Node n)
    {
        return hasChildNodesOfType(n, Node.TEXT_NODE);
    }

    protected boolean hasChildNodesOfType(Node n, short nodeType)
    {
        for(Node childNode = n.getFirstChild();
            childNode != null;
            childNode = childNode.getNextSibling())
        {
            if (childNode.getNodeType() == nodeType)
            {
                return true;
            }
        }
        return false;
    }

    private String getStringValue(Element e)
    {
        NodeList nl = e.getChildNodes();

        for (int i = 0; i < nl.getLength(); i++) {
            Node n = nl.item(i);
            if (n.getNodeType() == Node.TEXT_NODE) {
                return n.getNodeValue();
            }
        }
        return null;
    }

}

