All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdom2.contrib.beans.BeanMapper Maven / Gradle / Ivy

Go to download

A complete, Java-based solution for accessing, manipulating, and outputting XML data

The newest version!
/*--

 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.jdom2.contrib.beans;

import java.lang.reflect.*;
import java.util.*;
import java.beans.*;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Attribute;
import org.jdom2.Namespace;

/**
 * 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.jdom2.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]) **/ @SuppressWarnings("javadoc") 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 type = value.getClass(); String classname = type.getName(); // todo: allow per-type callback to convert (if toString() is // inadequate) -- extend stringconverter? if (classname.startsWith("java.lang.") || classname.equals("java.util.Date") ) { result = value.toString(); } else if (type.isArray()) { // it's an array - use java.lang.reflect.Array to extract // items (or wrappers thereof) List list = new ArrayList(); for (int i=0; i it = ((List)value).iterator(); it.hasNext(); ) { Object item = it.next(); if (child == null) { child = createElement(elementName); parent.addContent(child); } setElementValue(propertyName, elementName, parent, child, item); // this'll be weird if it's an array of arrays child = null; } } else throw new BeanMapperException( "Unknown result type for property " + propertyName + ": " + value); } // JDOM -> 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()); Mapping mapping; String propertyName; Set alreadySet = new HashSet(); // map Attributes of parent first if (element.hasAttributes()) { for (Attribute attribute : element.getAttributes()) { debug("Mapping " + attribute); mapping = getMappingForAttribute(null, attribute.getName()); propertyName = (mapping==null) ? attribute.getName() : mapping.property; setProperty(bean, propertyName, attribute.getValue()); } } // map child Elements //debug(element.toString() + " has " + children.size() + " children"); for (Element child : element.getChildren()) { 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 for (Attribute attribute : child.getAttributes()) { 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 pbeanPackage, String elementName) { return (pbeanPackage == null ? "" : (pbeanPackage + ".")) + 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 param = params[0]; if (param != property.getPropertyType()) debug("Weird: setter takes " + param + ", property is " + property.getPropertyType()); debug("Invoking setter: " + setter.getName() + "(" + valueObject + ")"); setter.invoke(bean, new Object[] { valueObject }); return true; } catch (BeanMapperException e) { throw e; } catch (Exception e) { throw new BeanMapperException("Setting property " + property.getName() + "=" + value + " in " + bean.getClass(), e); } } protected Object convertString(String value, Class type) { if (value == null) return null; if (type == String.class) return value; return stringconverter.parse(value, type); } protected Object convertJDOMValue(Object value, Class type) throws BeanMapperException { Object valueObject; // Null value if (value == null) valueObject = null; // String value else if (value instanceof String) { valueObject = convertString((String)value, type); } // Element value else if (value instanceof Element) { Element element = (Element)value; // if the setter actually takes a JDOM element, pass it if (type == Element.class) valueObject = value; // no children, must be a text node else if (element.getChildren().isEmpty()) { valueObject = convertString(element.getText(), type); } // we have to convert it into a bean else if (element.getChildren().size() == 1) { // Make a recursive call to this BeanMapper to map // the child element // Note that toBean could return a subclass of the // property type, so just let it figure out the // right type valueObject = toBean(element.getChildren().get(0)); } else { // element with multiple children -- must be an // array property throw new BeanMapperException( "Mapping of multiple child elements not implemented for " + element.getName()); } } else { throw new BeanMapperException("Can't map unknown type: " + value.getClass() + "=" + value); } return valueObject; } // convert JDOMValue /** * @return an array of the appropriate type **/ protected Object buildArray(PropertyDescriptor property, List children) throws BeanMapperException { Class arrayClass = property.getPropertyType(); Class itemClass = arrayClass.getComponentType(); if (itemClass == null) { throw new BeanMapperException("Can't instantiate array of type " + arrayClass); } // use java.lang.reflect.Array Object array = Array.newInstance(itemClass, children.size()); // fill it 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) { for (Mapping m : mappings) { if (m.property != null && m.property.equals(property)) { return m; } } return null; } public Mapping getMappingForElement(String element) { for (Mapping m : mappings) { if (m.element.equals(element)) { return m; } } return null; } public Mapping getMappingForAttribute(String element, String attribute) { for (Mapping m : mappings) { 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); } }