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>
* <person>
* <firstName>Jason</firstName>
* <lastName>van Zyl</lastName>
* <hobbies>
* <hobby>somnambulism</hobby>
* <hobby>squash</hobby>
* </hobbies>
* <address>
* <street>50 King</street>
* <city>Guelph</city>
* <country>Canada</country>
* </address>
* </person>
* </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);
}
}
}