![JAR search and dependency download from the Maven repository](/logo.png)
org.jdom.contrib.beans.BeanMapper Maven / Gradle / Ivy
Show all versions of jdom-contrib Show documentation
/*--
$Id: BeanMapper.java,v 1.5 2004/12/11 00:06:40 jhunter Exp $
Copyright (C) 2000-2004 Jason Hunter & Brett McLaughlin & Alex Chaffee.
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 disclaimer that follows
these conditions in the documentation and/or other materials
provided with the distribution.
3. The name "JDOM" must not be used to endorse or promote products
derived from this software without prior written permission. For
written permission, please contact .
4. Products derived from this software may not be called "JDOM", nor
may "JDOM" appear in their name, without prior written permission
from the JDOM Project Management .
In addition, we request (but do not require) that you include in the
end-user documentation provided with the redistribution and/or in the
software itself an acknowledgement equivalent to the following:
"This product includes software developed by the
JDOM Project (http://www.jdom.org/)."
Alternatively, the acknowledgment may be graphical using the logos
available at http://www.jdom.org/images/logos.
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 JDOM AUTHORS OR THE PROJECT
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 JDOM Project and was originally
created by Jason Hunter and
Brett McLaughlin . For more information
on the JDOM Project, please see .
*/
package org.jdom.contrib.beans;
import java.lang.reflect.*;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.*;
import java.beans.*;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Attribute;
import org.jdom.Namespace;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
/**
* Maps a JavaBean to an XML tree and vice versa. (Yes, it's yet
* another XML Data Binding solution.) Given a JavaBean, it will
* produce a JDOM tree whose elements correspond to the bean's
* property values. Given a JDOM tree, it will return a new instance
* of that bean whose properties have been set using the corresponding
* values in the JDOM tree.
*
* By default, it assumes each element maps to a property of the same
* name, subject to normal capitalization rules. That is, an element
* <foo> will map to the methods setFoo and getFoo. You can
* change this behavior by calling the various addMapping
* methods. For instance, to map a an element <date> to the
* property "birthDate" (using methods setBirthDate and getBirthDate),
* call
mapper.addMapping("birthDate", "date");
You can
* also map a property to an attribute, either of a child or of the
* parent (see JavaDoc for
*
* addMapping(String property, String element, String attribute)
* for details).
*
* During Bean -> JDOM conversion, if a BeanInfo object is found, it
* will be respected. See JavaDoc for java.beans.Introspector.
*
* If a given property, element, or attribute is to be skipped
* (ignored) during conversion, call the appropriate ignore method
* (ignoreProperty, ignoreElement, or ignoreAttribute). This is also
* appropriate if your bean has multiple accessors (properties) for
* the same underlying data.
*
* Support for Namespaces is rudimentary at best and has not been
* well-tested; if you specify a Namespace then all created elements
* and attributes will be in that namespace (or all elements not in
* that namespace will be skipped, for JDOM->Bean mapping).
*
* If a bean property type is a Java array, its items will be mapped
* to multiple child elements of the bean element (multiple children
* at the base level, not one child). This is to provide an easier
* transition for XML documents whose elements contain multiple
* children with the same name.
*
* Please try this out on your own beans. If there is a case that
* fails to do what you like (for instance, properties with custom
* class types), let me know and I'll try to work it out.
*
*
TODO:
* support list properties (collections other than Java arrays)
* allow lists/arrays to map to either scattered elements or a single nested element
* sort XML elements in some order other than random BeanInfo order
* support for BeanInfoSearchPaths
* multiple packages searched for beans
* better support for Namespaces (hard)
* allow stringconverter to map object -> string (not just string -> object)
* id/idref support for cyclical references
* more type converters
* custom property converters
* known issue: inner class bean can't be found jdom->bean (workaround: define mapping containing class name)
*
*
* Example:
* TestBean
*
* public class TestBean implements java.io.Serializable {
* public String getName() { ... }
* public int getAge() { ... }
* public Date getBirthdate() { ... }
* public TestBean getFriend() { ... }
* public void setName(String name) { ... }
* public void setAge(int age) { ... }
* public void setBirthdate(Date birthdate) { ... }
* public void setFriend(TestBean friend) { ... }
* public String toString() { ... }
* }
*
* XML Representation
* <testBean>
* <dob age="31">Fri Aug 08 00:00:00 EDT 1969</dob>
* <name>Alex</name>
* <friend>
* <testBean>
* <dob age="25">Thu May 01 00:00:00 EDT 1975</dob>
* <name>Amy</name>
* </testBean>
* </friend>
* </testBean>
*
* Mapping code
* BeanMapper mapper = new BeanMapper();
* mapper.addMapping("birthdate", "dob"); // element mapping
* mapper.addMapping("age", "dob", "age"); // attribute mapping
* mapper.setBeanPackage("org.jdom.contrib.beans");
*
* Converting Bean to JDOM
* Document doc = mapper.toDocument(alex);
* Converting JDOM to Bean
* TestBean alex = mapper.toBean(doc);
*
* @author Alex Chaffee ([email protected])
**/
public class BeanMapper {
protected String beanPackage;
protected Namespace namespace;
protected boolean ignoreMissingProperties = false;
protected boolean ignoreNullProperties = true;
protected List mappings = new ArrayList();
protected StringConverter stringconverter = new StringConverter();
/**
* Default constructor. If you are only doing bean -> XML
* mapping, you may use the mapper immediately. Otherwise, you
* must call setBeanPackage.
**/
public BeanMapper()
{
}
// Properties
/**
* @param beanPackage the name of the package in which to find the
* JavaBean classes to instantiate
**/
public void setBeanPackage(String beanPackage)
{
this.beanPackage = beanPackage;
}
/**
* Use this namespace when creating the XML element and all
* child elements.
**/
public void setNamespace(Namespace namespace)
{
this.namespace = namespace;
}
/**
* Get the object responsible for converting a string to a known
* type. Once you get this, you can call its setFactory() method
* to add a converter for a custom type (for which toString() does
* not suffice). The default string converter has a factory that
* recognizes several date formats (including ISO8601 -
* e.g. "2000-09-18 18:51:22-0600" or some substring thereof).
**/
public StringConverter getStringConverter() {
return stringconverter;
}
/**
* Set a custom string converter.
**/
public void setStringConverter(StringConverter stringconverter) {
this.stringconverter = stringconverter;
}
/**
* In mapping from Bean->JDOM, if we encounter an property with a
* null value, should
* we ignore it or add an empty child element/attribute (default: true)?
* @param b true = ignore, false = empty element
**/
public void setIgnoreNullProperties(boolean b) {
ignoreNullProperties = b;
}
/**
* In mapping from JDOM->Bean, if we encounter an element or
* attribute without a corresponding property in the bean, should
* we ignore it or throw an exception (default: false)?
* @param b true = ignore, false = throw exception
**/
public void setIgnoreMissingProperties(boolean b) {
ignoreMissingProperties = b;
}
// Bean -> JDOM Mapping
/**
* Converts the given bean to a JDOM Document.
* @param bean the bean from which to extract values
**/
public Document toDocument(Object bean) throws BeanMapperException {
return toDocument(bean, null);
}
/**
* Converts the given bean to a JDOM Document.
* @param bean the bean from which to extract values
* @param name the name of the root element (null => use bean class name)
**/
public Document toDocument(Object bean, String elementName)
throws BeanMapperException {
Element root = toElement(bean, elementName);
Document doc = new Document(root);
return doc;
}
/**
* Converts the given bean to a JDOM Element.
* @param bean the bean from which to extract values
* @param elementName the name of the element (null => use bean class name)
**/
public Element toElement(Object bean) throws BeanMapperException
{
return toElement(bean, null);
}
/**
* Converts the given bean to a JDOM Element.
* @param bean the bean from which to extract values
* @param elementName the name of the element (null => use bean class name)
**/
public Element toElement(Object bean, String elementName)
throws BeanMapperException {
BeanInfo info;
try {
// cache this?
info = Introspector.getBeanInfo(bean.getClass());
}
catch (IntrospectionException e) {
throw new BeanMapperException("Mapping bean " + bean, e);
}
// create element
Element element;
String beanname;
if (elementName != null) {
element = createElement(elementName);
}
else {
Class beanclass = info.getBeanDescriptor().getBeanClass();
beanname = unpackage(beanclass.getName());
element = createElement(beanname);
}
// get all properties, set as child-elements
PropertyDescriptor[] properties = info.getPropertyDescriptors();
for (int i=0; i Bean
/**
* Converts the given JDOM Document to a bean
* @param document the document from which to extract values
**/
public Object toBean(Document document) throws BeanMapperException {
return toBean(document.getRootElement());
}
public Object toBean(Element element) throws BeanMapperException {
Object bean = instantiateBean(element.getName());
Iterator i;
Mapping mapping;
String propertyName;
List attributes;
List children;
Set alreadySet = new HashSet();
// map Attributes of parent first
attributes = element.getAttributes();
for (i=attributes.iterator(); i.hasNext(); ) {
Attribute attribute = (Attribute)i.next();
debug("Mapping " + attribute);
mapping = getMappingForAttribute(null, attribute.getName());
propertyName = (mapping==null) ?
attribute.getName() : mapping.property;
setProperty(bean, propertyName, attribute.getValue());
}
// map child Elements
children = element.getChildren();
debug(element.toString() + " has " + children.size() + " children");
for (i=children.iterator(); i.hasNext(); ) {
Element child = (Element)i.next();
debug("Mapping " + child);
mapping = getMappingForElement(child.getName());
propertyName = (mapping==null) ? child.getName() : mapping.property;
// set bean property from element
PropertyDescriptor property =
findPropertyDescriptor(bean, propertyName);
if (property != null) {
if (!alreadySet.contains(child.getName())) {
if (property.getPropertyType().isArray())
setProperty(bean, property, element, child);
else
setProperty(bean, property, element, child);
}
}
// Now map all attributes of this child
attributes = child.getAttributes();
for (Iterator iatt=attributes.iterator(); iatt.hasNext(); ) {
Attribute attribute = (Attribute)iatt.next();
debug("Mapping " + attribute);
mapping = getMappingForAttribute(child.getName(),
attribute.getName());
propertyName = (mapping==null) ?
attribute.getName() : mapping.property;
setProperty(bean, propertyName, attribute.getValue());
} // for attributes
alreadySet.add(child.getName());
} // for children
return bean;
} // toBean
/**
* return a fresh new object of the appropriate bean type for
* the given element name.
* @return the bean
**/
protected Object instantiateBean(String elementName)
throws BeanMapperException {
// todo: search multiple packages
String className = null;
Class beanClass;
try {
Mapping mapping = getMappingForElement(elementName);
if (mapping != null &&
mapping.type != null) {
beanClass = mapping.type;
}
else {
className = getBeanClassName(beanPackage, elementName);
beanClass = Class.forName(className);
}
Object bean = beanClass.newInstance();
return bean;
}
catch (ClassNotFoundException e) {
throw new BeanMapperException("Class " + className +
" not found instantiating " + elementName +
" - maybe you need to add a mapping, or add a bean package", e);
}
catch (Exception e) {
throw new BeanMapperException("Instantiating " + elementName, e);
}
}
protected String getBeanClassName(String beanPackage, String elementName) {
return (beanPackage == null ? "" : (beanPackage + ".")) +
Character.toUpperCase(elementName.charAt(0)) +
elementName.substring(1);
}
protected PropertyDescriptor findPropertyDescriptor(Object bean,
String propertyName)
throws BeanMapperException {
try {
// cache this?
BeanInfo info = Introspector.getBeanInfo(bean.getClass());
PropertyDescriptor[] properties = info.getPropertyDescriptors();
for (int i=0; i parent element
* @param attribute the name of the attribute. null => set as element
**/
public void addMapping(String property, String element, String attribute) {
addMapping(new Mapping(property, null, element, attribute));
}
/**
* Map a property name to an element or an attribute. Can also
* specify which bean class to instantiate (applies to JDOM->Bean
* mapping).
*
* @param property the name of the property. null => look for property
* with same name as the element
* @param type Always convert this element name to this class.
* null => look for a bean with the same name as the element
* @param element the name of the element containing the attribute.
* null => parent element
* @param attribute the name of the attribute. null => set as element
**/
public void addMapping(String property, Class type,
String element, String attribute) {
addMapping(new Mapping(property, type, element, attribute));
}
public void addMapping(Mapping mapping) {
mappings.add(mapping);
}
public Mapping getMappingForProperty(String property) {
Iterator i = mappings.iterator();
while (i.hasNext()) {
Mapping m = (Mapping)i.next();
if (m.property != null && m.property.equals(property)) {
return m;
}
}
return null;
}
public Mapping getMappingForElement(String element) {
Iterator i = mappings.iterator();
while (i.hasNext()) {
Mapping m = (Mapping)i.next();
if (m.element.equals(element)) {
return m;
}
}
return null;
}
public Mapping getMappingForAttribute(String element, String attribute) {
Iterator i = mappings.iterator();
while (i.hasNext()) {
Mapping m = (Mapping)i.next();
if (m.element != null &&
m.attribute != null &&
m.element.equals(element) &&
m.attribute.equals(attribute))
{
return m;
}
}
return null;
}
public class Mapping {
public String property;
public Class type;
public String element;
public String attribute;
/**
* @param property the name of the property. null => look for
* property with same name as the element
* @param type Always convert this element name to this class.
* null => look for a bean with the same name as the element
* @param element the name of the element containing the attribute.
* null => parent element
* @param attribute the name of the attribute. null => set as element
**/
public Mapping(String property, Class type,
String element, String attribute) {
this.property = property;
this.type = type;
this.element = element;
this.attribute = attribute;
}
}
// Hiding
protected Set ignoredProperties = new HashSet();
protected Set ignoredElements = new HashSet();
protected Set ignoredAttributes = new HashSet();
public void ignoreProperty(String property) {
ignoredProperties.add(property);
}
public boolean isIgnoredProperty(String property) {
return ignoredProperties.contains(property);
}
public void ignoreElement(String element) {
ignoredElements.add(element);
}
public boolean isIgnoredElement(String element) {
return ignoredElements.contains(element);
}
static protected String toAttributeString(String element,
String attribute) {
return (element == null ? "." : element) +
"/@" + attribute;
}
public void ignoreAttribute(String element, String attribute) {
ignoredAttributes.add(toAttributeString(element, attribute));
}
public boolean isIgnoredAttribute(String element, String attribute) {
return ignoredAttributes.contains(
toAttributeString(element, attribute));
}
// Utilities
protected Element createElement(String elementName) {
return namespace == null ? new Element(elementName) :
new Element(elementName, namespace);
}
protected static String unpackage(String classname) {
int dot = Math.max(classname.lastIndexOf("."),
classname.lastIndexOf("$"));
if (dot > -1) {
classname = classname.substring(dot+1);
}
classname = Introspector.decapitalize(classname);
return classname;
}
public static int debug = 0;
protected static void debug(String msg) {
if (debug > 0)
System.err.println("BeanMapper: " + msg);
}
}