Package org.apache.stratum.xo

Source Code of org.apache.stratum.xo.Mapper

package org.apache.stratum.xo;

/* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2001 The Apache Software Foundation.  All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in
*    the documentation and/or other materials provided with the
*    distribution.
*
* 3. The end-user documentation included with the redistribution,
*    if any, must include the following acknowledgment:
*       "This product includes software developed by the
*        Apache Software Foundation (http://www.apache.org/)."
*    Alternately, this acknowledgment may appear in the software itself,
*    if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Apache" and "Apache Software Foundation" and
*    "Apache Turbine" must not be used to endorse or promote products
*    derived from this software without prior written permission. For
*    written permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
*    "Apache Turbine", nor may "Apache" appear in their name, without
*    prior written permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation.  For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

import java.util.HashMap;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.CDATA;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.Strings;

import java.lang.reflect.Method;
import org.apache.stratum.introspection.Introspector;

/**
* Map an XML document to a JavaBean. The XML document is
* assumed to be in a common format:
*
* <p>
* <pre>
* &lt;person&gt;
*   &lt;firstName&gt;Jason&lt;/firstName&gt;
*   &lt;lastName&gt;van Zyl&lt;/lastName&gt;
*   &lt;hobbies&gt;
*     &lt;hobby&gt;somnambulism&lt;/hobby&gt;
*     &lt;hobby&gt;squash&lt;/hobby&gt;
*   &lt;/hobbies&gt;
*   &lt;address&gt;
*     &lt;street&gt;50 King&lt;/street&gt;
*     &lt;city&gt;Guelph&lt;/city&gt;
*     &lt;country&gt;Canada&lt;/country&gt;
*   &lt;/address&gt;
* &lt;/person&gt;
* </pre>
* <p>
* These are some of the assumptions employed to
* make mapping the object simpler:
*
* <p>
* <ul>
*   <li>
*     Any nested objects that are created are assumed to
*     to be in the same package as the parent object.
*   </li>
*   <li>
*   </li>
* </ul>
*
* Processing Rules:
* <pre>
* Does Element refers to a noun plural?
*
* Yes:
*   treewalk
* No:
*   Does the Element have children?
*     Yes:
*       Create object and attach to parent,
*       have to look for inclusion rules as we might need
*       to create an object from another XML file.
*
*       Is the parent Element a plural noun?
*         Yes:
*           We have a collection:
*           addX(x) is executed
*         No:
*           We have a simple property:
*           setProperty(x) is executed
*     No:
*       Is there an inclusion rule?
*         Yes:
*           Create object from an XML file and attach
*         No:
*           Simple String value to attach
* </pre>
* <p>
* This class is not thread safe.
*
* @author <a href="mailto:jvanzyl@zenplex.com">Jason van Zyl</a>
* @author <a href="mailto:dlr@collab.net">Daniel Rall</a>
* @version $Id: Mapper.java,v 1.17 2002/02/28 23:22:10 jvanzyl Exp $
*/

// How to use the resources package to pull in the
// XML documents. This will take care of any type specific
// issues and will allow multiple sources.

// Try to merge any behaviour in the introspector with bean
// utils so that bean utils can be used exclusively. Don't need
// two introspection packages.

// Element name mapping for non conformant XML that you
// want to parse anyway.

// Jumping stages so that documents like RSS
// that don't have noun plural wrappers can still
// be parsed. Not necessary but would be nice.

public class Mapper
{
    /**
     * The element and element attribute name which indicates the
     * class name of the next object in the model.
     */
    protected static final String CLASS_NAME = "className";

    /**
     * The element attribute that indicates an external
     * XML document is to be process and part of the construction
     * of object model.
     */
    protected static final String EID = "id";

    /**
     * dom4j object model representation of a xml document. Note:
     * We use the interface(!) not its implementation
     */
    private Document document;

    /**
     * Instructions for the inclusion of external files.
     * If there is a rule that matches the element than files
     * will be included from an external source.
     */
    private Map inclusionRules;

    /**
     * Base directory where the initial XML file is read
     * from if files are being used.
     */
    private String basePath;

    /**
     * Base package of all the classes we may need to create instances
     * of.
     */
    private String basePackage;

    /**
     * Turn on debugging or not.
     */
    private boolean debug = false;

    /**
     * Introspector that this mapper uses to determine
     * properties and methods required to build up the
     * object model.
     */
    private Introspector introspector;

    /**
     * Default constructor.
     */
    public Mapper()
    {
        inclusionRules = new HashMap();
        introspector = new Introspector();
    }

    /**
     * Reset the mapper so that it can be reused.
     */
    public void reset()
    {
        inclusionRules.clear();
        introspector.clearCache();
    }

    /**
     * Loads a XML document (from the file system), maps it to an
     * instance of the JavaBean <code>beanClass</code>, and returns
     * the instance.
     *
     * @param xmlInput The file system path to the XML data file to
     * process.
     * @see #map(InputStream, String)
     */
    public Object map(File xmlInput, String beanClass)
        throws Exception
    {
        // Get the base directory
        basePath = FileUtils.dirname(xmlInput.getAbsolutePath());

        return map(new FileInputStream(xmlInput), beanClass);
    }

    /**
     * Loads a XML document (from the file system), maps it to
     * a live instance of the JavaBean <code>bean</code>, and returns
     * the modified instance.
     *
     * @param xmlInput The file system path to the XML data file to
     * process.
     * @see #map(InputStream, String)
     */
    public Object map(File xmlInput, Object bean)
        throws Exception
    {
        return map(new FileInputStream(xmlInput), bean);
    }

    /**
     * Loads a XML document (first trying the classpath, then the file
     * system), maps it to an instance of the JavaBean
     * <code>beanClass</code>, and returns the instance.
     *
     * @param xmlInput The path (either classpath resource or file
     * system) to the XML data file to process.
     * @see #map(InputStream, String)
     */
    public Object map(String xmlInput, String beanClass)
        throws Exception
    {
        InputStream xmlStream = findXmlStream(xmlInput);

        // Get the base directory
        basePath = FileUtils.dirname(xmlInput);

        return map(xmlStream, beanClass);
    }

    /**
     * Loads a XML document (first trying the classpath, then the file
     * system), maps it to a live instance of the JavaBean
     * <code>bean</code>, and returns the modified instance.
     *
     * @param xmlInput The path (either classpath resource or file
     * system) to the XML data file to process.
     * @see #map(InputStream, Object)
     */
    public Object map(String xmlInput, Object bean)
        throws Exception
    {
        InputStream xmlStream = findXmlStream(xmlInput);
        return map(xmlStream, bean);
    }

    /**
     * Find the specified XML resource by looking in the
     * classpath first, and subsequently the file system.
     *
     * @param xmlInput The XML resource to find.
     * @return The found InputStream.
     */
    private InputStream findXmlStream(String xmlInput)
        throws Exception
    {
        // Use the source path to grab a stream from the classpath.
        InputStream xmlStream =
            getClass().getClassLoader().getResourceAsStream(xmlInput);
        if (xmlStream == null)
        {
            // Couldn't load from the classpath -- try the file system.
            xmlStream = new FileInputStream(xmlInput);
        }

        return xmlStream;
    }

    /**
     * Loads a XML document from <code>xmlInput</code>, maps it to a
     * live instance of the JavaBean <code>bean</code>, and returns
     * the modified instance.
     *
     * @param xmlInput A stream of the XML data to process.
     * @param bean The live instance of the parent bean.
     * bean, or <code>null</code> to read it from the
     * <code>className</code> attribute of the first <code>Node</code>
     * in the XML document.
     * @return The modified instance of <code>bean</code>.
     * @exception DocumentException If the buildprocess fails.
     */
    public Object map(InputStream xmlInput, Object bean)
        throws Exception
    {
        return map(xmlInput,bean,null);
    }

    /**
     * Loads a XML document from <code>xmlInput</code>, maps it to an
     * instance of the JavaBean <code>beanClass</code>, and returns
     * the instance.
     *
     * @param xmlInput A stream of the XML data to process.
     * @param beanClass The fully qualified class name of the parent
     * bean, or <code>null</code> to read it from the
     * <code>className</code> attribute of the first <code>Node</code>
     * in the XML document.
     * @return The fleshed-out instance of <code>beanClass</code>.
     * @exception DocumentException If the build process fails.
     */
    public Object map(InputStream xmlInput, String beanClass)
        throws Exception
    {
        return map(xmlInput,null,beanClass);
    }

    /**
     * Loads a XML document from <code>xmlInput</code>, maps it to
     * a newly created object if <code>bean</code> is <code>null</null>,
     * or maps it to a live <code>bean</bean>.
     *
     * @param xmlInput A Stream of the XML data to process.
     * @param bean A live object instance to modify if not <code>null</code>
     * @param beanClass Full qualified name of class to instantiate
     * and populate.
     * @return The modified <code>bean</code> instance or the fleshed-out
     * instance of <code>beanClass</code>
     * @exception DocumentException If the build process fails.
     */
    protected Object map(InputStream xmlInput, Object bean, String beanClass)
        throws Exception
    {
        SAXReader xmlReader = new SAXReader();
        document = xmlReader.read(xmlInput);

        // Create the parent bean.
        Element rootElement = document.getRootElement();

        if (bean == null)
        {
            bean = createInstance(rootElement, beanClass);
        }

        // Assure we have the fully qualified class of the bean.
        if (beanClass == null)
        {
            beanClass = bean.getClass().getName();
        }

        // Infer the base package to use for object creation when
        // CLASS_NAME attribute is not specified from bean class.
        basePackage = parsePackage(beanClass);

        if (basePath == null)
        {
            // Overloads were not called, assuming classpath loading.
            basePath = Strings.replace(basePackage, ".", "/");
        }

        // Determine the base package to use for object creation.
        return treeWalk(rootElement, bean);
    }

    /**
     * Turn debugging on/off.
     *
     * @param debug
     */
    public void setDebug(boolean debug)
    {
        this.debug = debug;
    }

    /**
     * Walk down a Element node processing the children.
     *
     * @param element Element to process
     * @param bean Java object that is to be populated.
     * @return Object The Java object assembled by the process
     */
    private Object treeWalk(Element element, Object bean)
        throws Exception
    {
        for ( int i = 0, size = element.nodeCount(); i < size; i++ )
        {
            Node node = element.node(i);
            if ( node instanceof Element )
            {
                String name = node.getName();

                // If the element refers to a noun plural than we
                // have something like <items> or <hobbies> and we
                // want to process the child elements.
                if (isPlural(name))
                {
                    treeWalk((Element) node, bean);
                }
                else
                {
                    // Now we are dealing with elements that refer
                    // to a noun singular and these elements have
                    // children. An example would be something like:
                    //
                    // (1): <address>                         ---+
                    // (2):   <street>50 King Street</street>    |
                    // (3):   <city>Guelph</city>                +- (A)
                    // (4):   <country>Canada</country>          |
                    // (5): </address>                        ---+
                    if (hasChildren(node) && !containsCDATA(node))
                    {
                        debug("Node " + node.getName() + " has children");

                        // We now have the situation where we need to
                        // instantiate a parent object so that
                        // elements that are subsequently processed
                        // can have their values assigned to the above
                        // mentioned object.  Using the example listed
                        // above, we would want to create an Address
                        // object (1) so that the 'street', 'city',
                        // and 'country' properties can be populated.

                        // We have to check for inclusion rules and build
                        // and object from an external XML file if that's
                        // the case.
                        Object o = inclusion((Element) node);

                        if (o == null)
                        {
                            o = createInstance((Element) node, null);
                        }

                        // Now we have to attach the newly created object
                        // to its parent object.
                        if (isPlural(node.getParent().getName()))
                        {
                            // Here we have the case where (A) is enclose by
                            // a parent element <addresses> so we need to add
                            // Address object to a List of Address objects.
                            addObject(bean,name,o);
                        }
                        else
                        {
                            // Here we have the case where (A) is NOT enclosed
                            // by a parent element <addresses> so we only need
                            // to set the simple property.
                            setProperty(bean,name,o);
                        }

                        debug("Attached " + o + " to parent " + bean);

                        // Now we walk the tree so that (2), (3), and (4)
                        // above are assigned to the newly attached object.
                        treeWalk((Element) node, o);
                    }
                    else
                    {
                        debug("Node " + node.getName() + " has no children");

                        Object o = null;
                        // We will check for the special case where a
                        // class name is being specified as an attribute.
                        if (((Element)node).attributeValue(CLASS_NAME) != null)
                        {
                            debug("Using className metadata to build object");

                            o = createInstance((Element) node, null);
                            debug("Created new object -> " +
                                  o.getClass().getName());
                        }
                        else
                        {
                            // Now we are dealing with elements that refer
                            // to a noun singular and these elements have
                            // no children.

                            o = inclusion((Element) node);
                            Class type = null;

                            if (o == null)
                            {
                                type = PropertyUtils.getPropertyType(bean,name);
                                o = ConvertUtils.convert(node.getText(), type);
                            }
                        }

                        if (isPlural(node.getParent().getName()))
                        {
                            // This noun singular element's parent element
                            // is a noun plural so we have something like:
                            //
                            // (1): <hobbies> [ parent element ]
                            // (2):   <hobby>somnambulism</hobby>
                            // (3):   <hobby>squash</hobby>
                            // (4): </hobbies>
                            //
                            // So the noun singular values of 'somnambulism'
                            // and 'squash' are added to a 'hobbies' list. We
                            // assume a List of Strings with the above pattern.
                            addObject(bean, name, o);
                        }
                        else
                        {
                            // This noun singular element's parent element
                            // is a noun singular so we have something like:
                            //
                            // (1): <person>
                            // (2):   <firstName>Jason</firstName>
                            // (3):   <lastName>van  Zyl</lastName>
                            // (4): </person>
                            //
                            // So with the noun singular values 'Jason' and
                            // 'van Zyl' we attempt to set simple bean
                            // properties.
                            setProperty(bean,name,o);
                        }
                    }
                }
            }
        }

        return bean;
    }

    private boolean containsCDATA(Node node)
    {
        return ((Element)node).node(1) instanceof CDATA;
    }

    /**
     * Set an inclusion rule which controls how external
     * XML documents are processed.
     *
     * @param elementName Element name for which the rule will apply
     * @param rule The rule for processing the external XML file.
     */
    public void setInclusionRule(String elementName, String rule)
    {
        inclusionRules.put(elementName, rule);
    }

    /**
     * Returns an instance of the class represented by
     * <code>element</code>.  First tries the (fully qualified) class
     * name from any <code>className</code> attribute, or if that
     * doesn't exist, then assumes the class is in the same package as
     * the enclosing element and is named using the name of the
     * current node with its first letter capitalized.
     *
     * @param element The XML element to create an instance of.
     * @param className An override for the name of the class to use,
     * or <code>null</code> to try heuristics.
     * @return The newly created instance.
     * @exception Exception Unable to determine class name or create
     * instance.
     */
    protected Object createInstance(Element element, String className)
        throws Exception
    {
        // Should probably use the factory manager.
        try
        {
            if (className == null || className.length() == 0)
            {
                className = element.attributeValue(CLASS_NAME);
                debug(CLASS_NAME + " attribute -> " + className);
            }
            return Class.forName(className).newInstance();
        }
        catch (Exception noAttrib)
        {
            debug("Using " + className + " unsuccessful: " + noAttrib);
           
            // We assume here that the object that we are creating is
            // in the same package as the parent object.
            String nodeName = element.getName();
            if (basePackage == null)
            {
                throw new Exception("Base package not known and " + CLASS_NAME +
                                    " attribute not specified (or wrong)");
            }
           
            className = (basePackage.length() == 0 ? "" : basePackage + '.') +
                nodeName.substring(0, 1).toUpperCase() + nodeName.substring(1);
           
            // TODO: Check class name validity
            try
            {
                debug("Attempting to load " + className);
                return Class.forName(className).newInstance();
            }
            catch (Exception unknownClass)
            {
                throw new Exception("No class name for node '" +
                    element.getName() + "': This may be resolved by adding a " +
                        CLASS_NAME + " attribute");
            }
        }
    }

    // -------------------------------------------------------------------
    // P R I V A T E  M E T H O D S
    // -------------------------------------------------------------------


    /**
     * Parses the default package from <code>className</code>.
     */
    private final String parsePackage(String className)
    {
        int i = className.lastIndexOf('.');
        return (i != -1 ? className.substring(0, i) : null);
    }

    /**
     * Does the node in question have child nodes?
     *
     * @param node The element to check for children.
     */
    private boolean hasChildren(Node node)
    {
        return (((Element) node).nodeCount() > 1);
    }

    /**
     * Build up and object by pulling in an external XML
     * document.
     *
     * @param node The <code>Element</code> currently being processed.
     * @return The object build up by the inclusion process.
     */
    private Object inclusion(Element node)
        throws Exception
    {
        String inclusionRule = (String) inclusionRules.get(node.getName());
        if (inclusionRule == null)
        {
            return null;
        }

        StringBuffer path = new StringBuffer(basePath).append('/');
        if (inclusionRule.length() > 0)
        {
            path.append(inclusionRule).append('/');
        }
        String id = node.attributeValue(EID);
        if (id == null || id.length() == 0)
        {
            return null;
        }
        path.append(id).append(".xml");

        // Passing null as the second arg will tell map() to look for
        // a className attribute on the root element.
        Object x = map(new File(path.toString()), null);

        if (x == null)
        {
            debug("ERROR: Problem loading -> " + node.getName() + ':' + x);
        }
        return x;
    }

    /**
     * Add an object to a parent object
     *
     * @param bean
     * @param elementName
     * @param value
     */
    private void addObject(Object bean, String elementName, Object value)
        throws Exception
    {
        debug(makeMethodName("add", elementName) + '(' + value + ')');
        try
        {
            Object[] args = new Object[] { value };
            String methodName = makeMethodName("add", elementName);
            Method m = introspector.getMethod(bean.getClass(), methodName, args);

            if (m == null)
            {
                debugMethod(bean, elementName, "add", value);
                return;
            }

            m.invoke(bean,args);
        }
        catch (Exception e)
        {
        }
    }

    /**
     * Set a standard simple bean property.
     *
     * @param bean
     * @param elementName
     * @param value
     */
    private void setProperty(Object bean, String elementName, Object value)
    {
        debug(makeMethodName("set", elementName) + '(' + value + ')');
        try
        {
            PropertyUtils.setSimpleProperty(bean,elementName,value);
        }
        catch (Exception e)
        {
            debugMethod(bean, elementName, "set", value);
        }
    }

    /**
     * Debug method that displays any problems there might be
     * with the object model. By this we mean properties that
     * might not be defined, or addX(x) methods that might
     * not be defined.
     *
     * @param bean
     * @param elementName
     * @param methodType
     * @param value
     */
    private void  debugMethod(Object bean,
                              String elementName,
                              String methodType,
                              Object value)
    {
        String methodName = bean.getClass().getName() + "." +
            makeMethodName(methodType, elementName);

        debug("ERROR: " + methodName + "(" + value.getClass().getName() + ") not found!");
    }

    /**
     * A very simple plural stemmer to determine whether
     * a noun is of singular or plural form.
     * <p>
     * FIXME: This cannot be easily I18N'd
     *
     * @param String name to test for plurality
     * @return boolean
     */
    private boolean isPlural(String name)
    {
        if (name.endsWith("ies") &&
            (!name.endsWith("eies") || !name.endsWith("aies")))
        {
            return true;
        }
        else if (name.endsWith("es") &&
            (!name.endsWith("aes") && !name.endsWith("ees") &&
                !name.endsWith("oes")))
        {
            return true;
        }
        else if (name.endsWith("s") &&
            (!name.endsWith("us") && !name.endsWith("ss")))
        {
            return true;
        }

        // None of the tests detected a plural
        return false;
    }

    /**
     * Makes the first letter capital and leaves the rest as is.
     *
     * @param prefix The method prefix (generally an action word).
     * @param text The text to modify.
     * @return The modified text.
     */
    private String makeMethodName(String prefix, String text)
    {
        return (text == null ? null : prefix +
                text.substring(0, 1).toUpperCase() + text.substring(1));
    }

    /**
     * Simple debug printer.
     */
    private void debug(String message)
    {
        if (debug)
        {
            System.out.println(message);
        }
    }
}
TOP

Related Classes of org.apache.stratum.xo.Mapper

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.