[Practicalxml-commits] SF.net SVN: practicalxml:[110] branches/dev-1.1/src
Brought to you by:
kdgregory
From: Auto-Generated S. C. M. <pra...@li...> - 2009-08-17 17:26:48
|
Revision: 110 http://practicalxml.svn.sourceforge.net/practicalxml/?rev=110&view=rev Author: kdgregory Date: 2009-08-17 17:26:40 +0000 (Mon, 17 Aug 2009) Log Message: ----------- repackage Modified Paths: -------------- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/BeanConverter.java branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlAppenders.java branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlDriver.java branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBeanConverter.java branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestXml2BeanDriver.java Added Paths: ----------- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlAppenders.java branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlDriver.java branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlOptions.java branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanDriver.java branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanHandler.java branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanOptions.java Removed Paths: ------------- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean2xml/ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/xml2bean/ Modified: branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/BeanConverter.java =================================================================== --- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/BeanConverter.java 2009-08-17 17:13:36 UTC (rev 109) +++ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/BeanConverter.java 2009-08-17 17:26:40 UTC (rev 110) @@ -16,8 +16,8 @@ import org.w3c.dom.Document; -import net.sf.practicalxml.converter.bean2xml.Bean2XmlDriver; -import net.sf.practicalxml.converter.bean2xml.Bean2XmlOptions; +import net.sf.practicalxml.converter.bean.Bean2XmlDriver; +import net.sf.practicalxml.converter.bean.Bean2XmlOptions; /** Copied: branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlAppenders.java (from rev 100, branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean2xml/Bean2XmlAppenders.java) =================================================================== --- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlAppenders.java (rev 0) +++ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlAppenders.java 2009-08-17 17:26:40 UTC (rev 110) @@ -0,0 +1,235 @@ +// Copyright 2008-2009 severally by the contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package net.sf.practicalxml.converter.bean; + +import java.util.EnumSet; + +import javax.xml.XMLConstants; + +import org.w3c.dom.Element; + +import net.sf.practicalxml.DomUtil; + + +/** + * Packaging class used for XML output appenders. This class is a temporary + * hack, as I move intelligence into {@link Bean2XmlDriver}; the contained + * classes will end up in a new package, once I figure out what the package + * structure should be. + */ +public abstract class Bean2XmlAppenders +{ + /** + * An <code>Appender</code> appends children to a single node of the + * output tree. The driver is responsible for creating new appenders + * for each compound element, including the root, and providing those + * appenders with options to control output generation. + */ + public interface Appender + { + /** + * Appends a value element to the current element. Value elements have + * associated text, but no other children. + * + * @param name Name to be associated with the node. + * @param type Type to be associated with the node; may or may + * not be written to the output, depending on the + * appender's options. + * @param value The node's value. May be <code>null</code>, in + * which case the appender decides whether or not + * to actually append the node. + * + * @return The appended element. This is a convenience for subclasses, + * which may want to set additional attributes after their + * super has done the work of appending the element. + * @throws ConversionException if unable to append the node. + */ + public Element appendValue(String name, String type, String value); + + + /** + * Appends a container element to the current element. Container + * elements have other elements as children, and may have a type, + * but do not have an associated value. + */ + public Element appendContainer(String name, String type); + } + + + /** + * Base class for XML appenders, providing helper methods for subclasses. + */ + private static abstract class AbstractAppender + implements Appender + { + private EnumSet<Bean2XmlOptions> _options; + + public AbstractAppender(EnumSet<Bean2XmlOptions> options) + { + _options = options; + } + + protected boolean isOptionSet(Bean2XmlOptions option) + { + return _options.contains(option); + } + + protected boolean shouldSkip(Object value) + { + return (value == null) && !_options.contains(Bean2XmlOptions.XSI_NIL); + } + + protected void setType(Element elem, String type) + { + if (isOptionSet(Bean2XmlOptions.XSI_TYPE)) + elem.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "type", type); + } + + protected void setValue(Element elem, String value) + { + if (value != null) + DomUtil.setText(elem, value); + else if (isOptionSet(Bean2XmlOptions.XSI_NIL)) + elem.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil", "true"); + } + } + + + /** + * Basic appender, which appends new elements to a parent. + */ + public static class BasicAppender + extends AbstractAppender + { + private Element _parent; + + public BasicAppender(Element parent, EnumSet<Bean2XmlOptions> options) + { + super(options); + _parent = parent; + } + + public Element appendValue(String name, String type, String value) + { + if (shouldSkip(value)) + return null; + + Element child = DomUtil.appendChildInheritNamespace(_parent, name); + setType(child, type); + setValue(child, value); + return child; + } + + public Element appendContainer(String name, String type) + { + Element child = DomUtil.appendChildInheritNamespace(_parent, name); + setType(child, type); + return child; + } + } + + + /** + * Appender for children of an indexed/iterated item (array, list, or set). + * Each element will have an incremented <code>index</code> attribute that + * indicates the position of the element within the iteration. + */ + public static class IndexedAppender + extends BasicAppender + { + int _index = 0; + + public IndexedAppender(Element parent, EnumSet<Bean2XmlOptions> options) + { + super(parent, options); + } + + + @Override + public Element appendValue(String name, String type, String value) + { + Element child = super.appendValue(name, type, value); + if (child != null) + child.setAttribute("index", String.valueOf(_index++)); + return child; + } + } + + + /** + * Appender for children of a <code>Map</code>. Depending on options, + * will either create children named after the key, or a generic "data" + * child with the key as an attribute. + */ + public static class MapAppender + extends BasicAppender + { + public MapAppender(Element parent, EnumSet<Bean2XmlOptions> options) + { + super(parent, options); + } + + + @Override + public Element appendValue(String name, String type, String value) + { + Element child = null; + if (isOptionSet(Bean2XmlOptions.INTROSPECT_MAPS)) + { + child = super.appendValue(name, type, value); + } + else + { + child = super.appendValue("data", type, value); + if (child != null) + child.setAttribute("key", name); + } + return child; + } + } + + + /** + * An appender that sets values directly on the "parent" element. Used for + * the conversion root element. + */ + public static class DirectAppender + extends AbstractAppender + { + private Element _elem; + + public DirectAppender(Element elem, EnumSet<Bean2XmlOptions> options) + { + super(options); + _elem = elem; + } + + public Element appendValue(String name, String type, String value) + { + if (!shouldSkip(value)) + { + setType(_elem, type); + setValue(_elem, value); + } + return _elem; + } + + public Element appendContainer(String name, String type) + { + setType(_elem, type); + return _elem; + } + } +} Copied: branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlDriver.java (from rev 108, branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean2xml/Bean2XmlDriver.java) =================================================================== --- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlDriver.java (rev 0) +++ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlDriver.java 2009-08-17 17:26:40 UTC (rev 110) @@ -0,0 +1,259 @@ +// Copyright 2008-2009 severally by the contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package net.sf.practicalxml.converter.bean; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Map; + +import javax.xml.XMLConstants; + +import org.w3c.dom.Element; + +import net.sf.practicalxml.DomUtil; +import net.sf.practicalxml.converter.ConversionException; +import net.sf.practicalxml.converter.ConversionHelper; +import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.*; + + +/** + * Driver class for converting a Java bean into an XML DOM. Normal usage is + * to create a single instance of this class with desired options, then use + * it for multiple conversions. This class is thread-safe. + */ +public class Bean2XmlDriver +{ + private ConversionHelper _helper; + private EnumSet<Bean2XmlOptions> _options = EnumSet.noneOf(Bean2XmlOptions.class); + + public Bean2XmlDriver(Bean2XmlOptions... options) + { + for (Bean2XmlOptions option : options) + _options.add(option); + _helper = new ConversionHelper(shouldUseXsdFormatting()); + } + + +//---------------------------------------------------------------------------- +// Public methods +//---------------------------------------------------------------------------- + + /** + * Creates an XML DOM with the specified root element name, and fills it + * by introspecting the passed object (see {@link #introspect} for + * treatment of simple objects). + */ + public Element convert(Object obj, String rootName) + { + return convert(obj, null, rootName); + } + + + /** + * Creates an XML DOM with the specified root element name and namespace + * URI, and fills it by introspecting the passed object (see {@link + * #introspect} for treatment of simple objects). The namespace URI (and + * prefix, if provided) will be used for all child elements. + */ + public Element convert(Object obj, String nsUri, String rootName) + { + Element root = DomUtil.newDocument(nsUri, rootName); + doXsiNamespaceHack(root); + convert(obj, "", new DirectAppender(root, _options)); + return root; + } + + + /** + * Introspects the passed object, and appends its contents to the output. + * This method is public to allow non-standard conversions, such as + * appending into an existing tree, or (in the future, if we introduce an + * appender factory) producing non-XML output. + */ + public void convert(Object obj, String name, Appender appender) + { + // these methods have side effects! + // empty blocks and comments are there to keep Eclipse happy + if (tryToConvertAsPrimitiveOrNull(obj, null, name, appender)) + { /* it was converted */ } + else if (tryToConvertAsArray(obj, name, appender)) + { /* it was converted */ } + else if (tryToConvertAsMap(obj, name, appender)) + { /* it was converted */ } + else if (tryToConvertAsCollection(obj, name, appender)) + { /* it was converted */ } + else if (tryToConvertAsBean(obj, name, appender)) + { /* it was converted */ } + else + throw new ConversionException("unable to convert: " + obj.getClass().getName()); + } + + +//---------------------------------------------------------------------------- +// Internals +//---------------------------------------------------------------------------- + + private boolean shouldUseXsdFormatting() + { + return _options.contains(Bean2XmlOptions.XSD_FORMAT) + || _options.contains(Bean2XmlOptions.XSI_TYPE); + } + + + private String getJavaXsiType(Object obj) + { + return "java:" + obj.getClass().getName(); + } + + + /** + * Introduces the XML Schema Instance namespace into the DOM tree using a + * meaningless attribute. The Xerces serializer does not attempt to promote + * namespace definitions above the subtree in which they first appear, which + * means that the XSI definition could be repeated many times throughout the + * serialized tree, adding bulk to the serialized representation. + * <p> + * By putting "nil=false" at the root element, we will keep the serializer + * from inserting all these definitions. This has to happen <em>before</em> + * any actual conversion, in case some bozo passes <code>null</code> to + * the top-level conversion routine. + * <p> + * Note that we only do this if <code>xsi:nil</code> is enabled by itself. + * If <code>xsi:type</code> is enabled, the converter will attach that + * attribute to the root instead, thereby establishing the namespace context. + */ + private void doXsiNamespaceHack(Element root) + { + if (_options.contains(Bean2XmlOptions.XSI_NIL) + && !_options.contains(Bean2XmlOptions.XSI_TYPE)) + { + root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil", "false"); + } + } + + + private boolean tryToConvertAsPrimitiveOrNull( + Object obj, Class<?> klass, String name, Appender appender) + { + if (obj != null) + klass = obj.getClass(); + + String objType = _helper.getXsdType(klass); + if ((obj == null) || (objType != null)) + { + appender.appendValue(name, objType, _helper.stringify(obj)); + return true; + } + + return false; + } + + + private boolean tryToConvertAsArray(Object array, String name, Appender appender) + { + if (!array.getClass().isArray()) + return false; + + Element parent = appender.appendContainer(name, getJavaXsiType(array)); + Appender childAppender = new IndexedAppender(parent, _options); + int length = Array.getLength(array); + for (int idx = 0 ; idx < length ; idx++) + { + Object value = Array.get(array, idx); + convert(value, "data", childAppender); + } + return true; + } + + + private boolean tryToConvertAsMap(Object obj, String name, Appender appender) + { + if (!(obj instanceof Map)) + return false; + + Element parent = appender.appendContainer(name, getJavaXsiType(obj)); + Appender childAppender = new MapAppender(parent, _options); + for (Map.Entry<?,?> entry : ((Map<?,?>)obj).entrySet()) + { + convert(entry.getValue(), String.valueOf(entry.getKey()), childAppender); + } + return true; + } + + + private boolean tryToConvertAsCollection(Object obj, String name, Appender appender) + { + if (!(obj instanceof Collection)) + return false; + + Element parent = appender.appendContainer(name, getJavaXsiType(obj)); + Appender childAppender = new IndexedAppender(parent, _options); + for (Object value : (Collection<?>)obj) + { + convert(value, "data", childAppender); + } + return true; + } + + + private boolean tryToConvertAsBean(Object bean, String name, Appender appender) + { + Element parent = appender.appendContainer(name, getJavaXsiType(bean)); + Appender childAppender = new BasicAppender(parent, _options); + try + { + BeanInfo info = Introspector.getBeanInfo(bean.getClass(), Object.class); + PropertyDescriptor[] props = info.getPropertyDescriptors(); + for (int ii = 0 ; ii < props.length ; ii++) + convertBeanProperty(bean, props[ii], childAppender); + } + catch (IntrospectionException ee) + { + throw new ConversionException("introspection failure", ee); + } + return true; + } + + + private void convertBeanProperty( + Object bean, PropertyDescriptor propDesc, Appender appender) + { + String name = propDesc.getName(); + Class<?> type = propDesc.getPropertyType(); + Object value; + try + { + Method getter = propDesc.getReadMethod(); + if (getter == null) + return; + value = getter.invoke(bean); + } + catch (Exception ee) + { + throw new ConversionException("unable to retrieve bean value", ee); + } + + if (value == null) + tryToConvertAsPrimitiveOrNull(value, type, name, appender); + else + convert(value, name, appender); + } +} Copied: branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlOptions.java (from rev 100, branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean2xml/Bean2XmlOptions.java) =================================================================== --- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlOptions.java (rev 0) +++ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Bean2XmlOptions.java 2009-08-17 17:26:40 UTC (rev 110) @@ -0,0 +1,55 @@ +// Copyright 2008-2009 severally by the contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package net.sf.practicalxml.converter.bean; + + +/** + * Options used by {@link Bean2XmlHandler} to control the structure of the + * DOM tree. + */ +public enum Bean2XmlOptions +{ + /** + * Outputs values using formats defined by XML Schema, rather than Java's + * <code>String.valueOf()</code> method. Note that these formats are not + * flagged in the element, so sender and receiver will have to agree on + * the format. + */ + XSD_FORMAT, + + /** + * Will add an <code>xsi:type</code> attribute to each element. For values + * covered by the XML Schema simple types, this attribute's value will be + * "<code>xsd:XXX</code>", where XXX is the XSD type name. For complex + * types, this attribute's value will be "<code>java:XXX</code>", where + * XXX is the fully-qualified classname. + * <p> + * <em>This option implies {@link #XSD_FORMAT} for simple types</em>. + */ + XSI_TYPE, + + /** + * Report null values using the <code>xsi:nil="true"</code> attribute. If + * not present, null values are ignored, and not added to DOM tree. + */ + XSI_NIL, + + /** + * Output maps in an "introspected" format, where the name of each item + * is the map key (rather than "data"), and the "key" attribute is omitted. + * If any key is not a valid XML identifier, the converter will throw. + */ + INTROSPECT_MAPS +} Copied: branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanDriver.java (from rev 109, branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/xml2bean/Xml2BeanDriver.java) =================================================================== --- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanDriver.java (rev 0) +++ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanDriver.java 2009-08-17 17:26:40 UTC (rev 110) @@ -0,0 +1,447 @@ +// Copyright 2008-2009 severally by the contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package net.sf.practicalxml.converter.bean; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.xml.XMLConstants; + +import net.sf.practicalxml.DomUtil; +import net.sf.practicalxml.converter.ConversionException; +import net.sf.practicalxml.converter.ConversionHelper; +import net.sf.practicalxml.internal.StringUtils; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + + +/** + * Driver class for converting an XML DOM into a Java bean. Normal usage is + * to create a single instance of this class with desired options, then use + * it for multiple conversions. This class is thread-safe. + * <p> + * This class assumes that the source XML will be in the form produced by + * {@link Bean2XmlDriver}: each element either contains child elements, or + * a text node containing the element's value. However, conversion is + * driven by the parameters passed to {@link #convert}, not by the content + * of the XML document; this can lead to some unexpected behavior: + * <ul> + * <li> Bean classes are introspected, and recursively processed base on + * the property descriptors. If the bean has multiple setter methods + * for a property, the method selected is arbitrarily chosen by the + * JavaBeans introspector. You can pass an option that looks for a + * setters using <code>String</code>, but this is not recommended + * from a performance perspective. + * <li> JDK collection types do not carry type information, so the only + * way to properly convert them is using an <code>xsi:type</code> + * attribute on the child elements. If this attribute is missing + * or cannot be interpreted, the element will be processed as if + * it is a <code>String</code>. + * <li> The converter will to pick an appropriate implementation class + * if given one of the JDK collections interfaces: <code> + * ArrayList</code> for <code>List</code> or <code>Collection</code>, + * <code>TreeSet</code> for <code>SortedSet</code>, <code>HashSet</code> + * for any other <code>Set</code>, and likewise <code>TreeMap</code> and + * <code>HashMap</code> for <code>SortedMap</code> and <code>Map</code>. + * </ul> + */ +public class Xml2BeanDriver +{ + private EnumSet<Xml2BeanOptions> _options; + private ConversionHelper _helper; + private Map<Class<?>,Map<String,Method>> _introspectedClasses; + + + public Xml2BeanDriver(Xml2BeanOptions... options) + { + _options = EnumSet.noneOf(Xml2BeanOptions.class); + for (Xml2BeanOptions option : options) + _options.add(option); + + _helper = new ConversionHelper(_options.contains(Xml2BeanOptions.EXPECT_XSD_FORMAT)); + _introspectedClasses = new HashMap<Class<?>,Map<String,Method>>(); + } + + +//---------------------------------------------------------------------------- +// Public Methods +//---------------------------------------------------------------------------- + + /** + * Attempts to convert the passed DOM subtree into an object of the + * specified class. + */ + public <T> T convert(Element elem, Class<T> klass) + { + return klass.cast(convertWithoutCast(elem, klass)); + } + + +//---------------------------------------------------------------------------- +// Internal Conversion Methods +//---------------------------------------------------------------------------- + + + + /** + * Attempts to convert the passed DOM subtree into an object of the + * specified class. Note that this version does not use generics, + * and does not try to cast the result, whereas the public version + * does. Internally, we want to treat <code>Integer.TYPE</code> the + * same as <code>Integer.class</code>, and the cast prevents that. + */ + public Object convertWithoutCast(Element elem, Class<?> klass) + { + validateXsiType(elem, klass); + if (isAllowableNull(elem)) + return null; + + Object obj = tryConvertAsPrimitive(elem, klass); + if (obj == null) + obj = tryConvertAsArray(elem, klass); + if (obj == null) + obj = tryConvertAsSimpleCollection(elem, klass); + if (obj == null) + obj = tryConvertAsMap(elem, klass); + if (obj == null) + obj = tryConvertAsBean(elem, klass); + return obj; + } + + + private boolean isAllowableNull(Element elem) + { + String text = getText(elem); + if ((text != null) || hasElementChildren(elem)) + return false; + + if (_options.contains(Xml2BeanOptions.REQUIRE_XSI_NIL)) + { + String attr = elem.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil"); + if (!attr.equals("true")) + throw new ConversionException( + "missing xsi:nil: " + DomUtil.getAbsolutePath(elem)); + } + + return true; + } + + + private Object tryConvertAsPrimitive(Element elem, Class<?> klass) + { + if (_helper.getXsdType(klass) == null) + return null; + + if (hasElementChildren(elem)) + throw new ConversionException( + "expecting primitive; has children: " + DomUtil.getAbsolutePath(elem)); + + return _helper.parse(getText(elem), klass); + } + + + private Object tryConvertAsArray(Element elem, Class<?> klass) + { + Class<?> childKlass = klass.getComponentType(); + if (childKlass == null) + return null; + + List<Element> children = DomUtil.getChildren(elem); + Object result = Array.newInstance(childKlass, children.size()); + int idx = 0; + for (Element child : children) + { + Array.set(result, idx++, convertWithoutCast(child, childKlass)); + } + return result; + } + + + private Object tryConvertAsSimpleCollection(Element elem, Class<?> klass) + { + Collection<Object> result = instantiateCollection(klass); + if (result == null) + return null; + + List<Element> children = DomUtil.getChildren(elem); + for (Element child : children) + { + Class<?> childClass = getClassFromXsiType(child); + if (childClass == null) + childClass = String.class; + result.add(convertWithoutCast(child, childClass)); + } + return result; + } + + + private Object tryConvertAsMap(Element elem, Class<?> klass) + { + Map<Object,Object> result = instantiateMap(klass); + if (result == null) + return null; + + List<Element> children = DomUtil.getChildren(elem); + for (Element child : children) + { + String key = child.getAttribute("key"); + if (StringUtils.isEmpty(key)) + key = DomUtil.getLocalName(child); + Class<?> childClass = getClassFromXsiType(child); + if (childClass == null) + childClass = String.class; + result.put(key, convertWithoutCast(child, childClass)); + } + return result; + } + + + private Object tryConvertAsBean(Element elem, Class<?> klass) + { + Object bean = instantiateBean(elem, klass); + + List<Element> children = DomUtil.getChildren(elem); + for (Element child : children) + { + Method setter = getSetterMethod(klass, child); + if (setter == null) + continue; + + Class<?> childClass = setter.getParameterTypes()[0]; + Object childValue = convertWithoutCast(child, childClass); + invokeSetter(elem, bean, setter, childValue); + } + return bean; + } + + +//---------------------------------------------------------------------------- +// Other Internals +//---------------------------------------------------------------------------- + + /** + * Returns the text content of an element, applying appropriate options. + */ + private String getText(Element elem) + { + String text = DomUtil.getText(elem); + if (StringUtils.isBlank(text) + && _options.contains(Xml2BeanOptions.CONVERT_BLANK_AS_NULL)) + text = null; + return text; + } + + + /** + * Returns the <code>xsi:type</code> attribute value, <code>null</code> if + * it's not set. + */ + private String getXsiType(Element elem) + { + String xsiType = elem.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "type"); + return StringUtils.isEmpty(xsiType) + ? null + : xsiType; + } + + + /** + * Examines an element's <code>xsi:type</code> attribute, if any, and + * returns the Java class corresponding to it. Used when converting + * collection types, which don't have type information that can be + * introspected, and also to validate non-XSD types. + */ + private Class<?> getClassFromXsiType(Element elem) + { + String xsiType = getXsiType(elem); + if (xsiType == null) + return null; + + if (xsiType.startsWith("java:")) + { + String javaType = xsiType.substring(5); + try + { + return Class.forName(javaType); + } + catch (ClassNotFoundException ee) + { + throw new ConversionException( + "invalid Java type specification (" + javaType + "): " + + DomUtil.getAbsolutePath(elem), + ee); + } + } + return _helper.getJavaType(xsiType); + } + + + private void validateXsiType(Element elem, Class<?> klass) + { + if (!_options.contains(Xml2BeanOptions.REQUIRE_XSI_TYPE)) + return; + + String xsiType = elem.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "type"); + if (StringUtils.isEmpty(xsiType)) + throw new ConversionException( + "missing xsi:type: " + DomUtil.getAbsolutePath(elem)); + + if (xsiType.equals(_helper.getXsdType(klass))) + return; + + Class<?> xsiKlass = getClassFromXsiType(elem); + if (klass.isAssignableFrom(xsiKlass)) + return; + + throw new ConversionException( + "invalid xsi:type (\"" + xsiType + "\" for " + klass.getName() + "): " + + DomUtil.getAbsolutePath(elem)); + } + + + private boolean hasElementChildren(Element elem) + { + Node child = elem.getFirstChild(); + while (child != null) + { + if (child instanceof Element) + return true; + child = child.getNextSibling(); + } + return false; + } + + + private Method getSetterMethod(Class<?> beanKlass, Element child) + { + Map<String,Method> methodMap = _introspectedClasses.get(beanKlass); + if (methodMap == null) + methodMap = introspect(beanKlass); + + Method setter = methodMap.get(DomUtil.getLocalName(child)); + if ((setter == null) && !_options.contains(Xml2BeanOptions.IGNORE_MISSING_PROPERTIES)) + { + throw new ConversionException( + "can't find property setter in " + beanKlass.getName() + ": " + + DomUtil.getAbsolutePath(child)); + } + + return setter; + } + + + private Map<String,Method> introspect(Class<?> klass) + { + Map<String,Method> methodMap = new HashMap<String,Method>(); + try + { + BeanInfo info = Introspector.getBeanInfo(klass, Object.class); + for (PropertyDescriptor propDesc : info.getPropertyDescriptors()) + { + Method setter = propDesc.getWriteMethod(); + if (setter != null) + methodMap.put(propDesc.getName(), setter); + } + } + catch (IntrospectionException e) + { + throw new ConversionException("unable to introspect", e); + } + + _introspectedClasses.put(klass, methodMap); + return methodMap; + } + + + /** + * Attempts to create a <code>Collection</code> instance appropriate for + * the passed class, returns <code>null</code> if unable. + */ + private Collection<Object> instantiateCollection(Class<?> klass) + { + if (SortedSet.class.isAssignableFrom(klass)) + return new TreeSet<Object>(); + else if (Set.class.isAssignableFrom(klass)) + return new HashSet<Object>(); + else if (List.class.isAssignableFrom(klass)) + return new ArrayList<Object>(); + else if (Collection.class.isAssignableFrom(klass)) + return new ArrayList<Object>(); + else + return null; + } + + + /** + * Attempts to create a <code>Map</code> instance appropriate for the + * passed class, returns <code>null</code> if unable. + */ + private Map<Object,Object> instantiateMap(Class<?> klass) + { + if (SortedMap.class.isAssignableFrom(klass)) + return new TreeMap<Object,Object>(); + else if (Map.class.isAssignableFrom(klass)) + return new HashMap<Object,Object>(); + else + return null; + } + + + private Object instantiateBean(Element elem, Class<?> klass) + { + try + { + return klass.newInstance(); + } + catch (Exception ee) + { + throw new ConversionException( + "unable to instantiate bean: " + DomUtil.getAbsolutePath(elem), + ee); + } + } + + + private void invokeSetter(Element elem, Object bean, Method setter, Object value) + { + try + { + setter.invoke(bean, value); + } + catch (Exception ee) + { + throw new ConversionException( + "unable to set property: " + DomUtil.getAbsolutePath(elem), + ee); + } + } +} Copied: branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanHandler.java (from rev 105, branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/xml2bean/Xml2BeanHandler.java) =================================================================== --- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanHandler.java (rev 0) +++ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanHandler.java 2009-08-17 17:26:40 UTC (rev 110) @@ -0,0 +1,194 @@ +// Copyright 2008-2009 severally by the contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package net.sf.practicalxml.converter.bean; + +import net.sf.practicalxml.DomUtil; +import net.sf.practicalxml.XmlException; +import net.sf.practicalxml.XmlUtil; +import net.sf.practicalxml.converter.ConversionException; +import net.sf.practicalxml.converter.ConversionHelper; +import net.sf.practicalxml.internal.StringUtils; + +import java.util.Arrays; +import java.util.Date; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.xml.XMLConstants; + +import org.w3c.dom.Element; + + +/** + * Invoked by {@link Xml2BeanDriver} to convert a DOM <code>Element</code> + * into the appropriate Java object. Unlike {@link Bean2XmlHandler}, there + * will only be one instance of this object created during conversion; all + * intermediate data can be held on the stack. + */ +public class Xml2BeanHandler +{ + private EnumSet<Xml2BeanOptions> _options; + private ConversionHelper _primitiveHelper; + + + /** + * Public constructor, allowing various options specifications. + */ + public Xml2BeanHandler(Xml2BeanOptions... options) + { + _options = EnumSet.noneOf(Xml2BeanOptions.class); + for (Xml2BeanOptions option : options) + _options.add(option); + + _primitiveHelper = new ConversionHelper(true); + } + + +//---------------------------------------------------------------------------- +// Public Methods +//---------------------------------------------------------------------------- + + public Boolean convertBoolean(Element elem) + { + return (Boolean)_primitiveHelper.parse(getText(elem), Boolean.class); + } + + + public Byte convertByte(Element elem) + { + return (Byte)_primitiveHelper.parse(getText(elem), Byte.class); + } + + + public Character convertCharacter(Element elem) + { + return (Character)_primitiveHelper.parse(getText(elem), Character.class); + } + + + public Date convertDate(Element elem) + { + throw new UnsupportedOperationException("not implemented yet"); + } + + + public Double convertDouble(Element elem) + { + return (Double)_primitiveHelper.parse(getText(elem), Double.class); + } + + + public Float convertFloat(Element elem) + { + return (Float)_primitiveHelper.parse(getText(elem), Float.class); + } + + + public Integer convertInteger(Element elem) + { + return (Integer)_primitiveHelper.parse(getText(elem), Integer.class); + } + + + public Long convertLong(Element elem) + { + return (Long)_primitiveHelper.parse(getText(elem), Long.class); + } + + + public Number convertNumber(Element elem) + { + throw new UnsupportedOperationException("not implemented yet"); + } + + + public Short convertShort(Element elem) + { + return (Short)_primitiveHelper.parse(getText(elem), Short.class); + } + + + public String convertString(Element elem) + { + return (String)_primitiveHelper.parse(getText(elem), String.class); + } + + + public List<?> convertList(Element elem) + { + throw new UnsupportedOperationException("not implemented yet"); + } + + + public Set<?> convertSet(Element elem) + { + throw new UnsupportedOperationException("not implemented yet"); + } + + + public Map<?,?> convertMap(Element elem) + { + throw new UnsupportedOperationException("not implemented yet"); + } + + + public Object convertObject(Element elem) + { + throw new UnsupportedOperationException("not implemented yet"); + } + + + public Object[] convertObjectArray(Element elem) + { + throw new UnsupportedOperationException("not implemented yet"); + } + + +//---------------------------------------------------------------------------- +// Internals +//---------------------------------------------------------------------------- + + /** + * Returns the text from a passed element, applying any low-level options + * along the way. + */ + private String getText(Element elem) + { + String text = DomUtil.getText(elem); + if (_options.contains(Xml2BeanOptions.CONVERT_BLANK_AS_NULL) && StringUtils.isBlank(text)) + text = null; + validateXsiNil(elem, text); + return text; + } + + + /** + * Checks for elements that require <code>xsi:nil</code>, and throws if missing. + */ + private void validateXsiNil(Element elem, String text) + { + if (text != null) + return; + if (!_options.contains(Xml2BeanOptions.REQUIRE_XSI_NIL)) + return; + + String attr = elem.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil"); + if ((attr == null) || !attr.equals("true")) + throw new ConversionException("empty element without required xsi:nil"); + } + +} Copied: branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanOptions.java (from rev 109, branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/xml2bean/Xml2BeanOptions.java) =================================================================== --- branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanOptions.java (rev 0) +++ branches/dev-1.1/src/main/java/net/sf/practicalxml/converter/bean/Xml2BeanOptions.java 2009-08-17 17:26:40 UTC (rev 110) @@ -0,0 +1,69 @@ +// Copyright 2008-2009 severally by the contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package net.sf.practicalxml.converter.bean; + + + +/** + * Options used by {@link Xml2BeanHandler} to control the way that DOM trees + * are translated to Java beans. + */ +public enum Xml2BeanOptions +{ + /** + * If present, the converter will treat all elements with empty text nodes + * as if they were empty elements -- in other words, <code>null</code>. + * Note that this flag will interact with <code>REQUIRE_XSI_NIL</code>. + */ + CONVERT_BLANK_AS_NULL, + + + /** + * Expect data (in particular, dates) to be formatted per XML Schema spec. + */ + EXPECT_XSD_FORMAT, + + + /** + * If present, the converter ignores elements that don't correspond to + * settable properties of the bean. + */ + IGNORE_MISSING_PROPERTIES, + + + /** + * If present, the converter will look for a setter method taking a + * <code>String</code>, in preference to a non-string method returned + * from the bean introspector. + */ + PREFER_STRING_SETTER, + + + /** + * If present, the converter requires an <code>xsi:nil</code> attribute + * on any empty nodes, and will throw if it's not present. Default is to + * treat empty nodes as <code>null</code>. + */ + REQUIRE_XSI_NIL, + + + /** + * If present, the converter requires an <code>xsi:type</code> attribute + * on each element, and will throw if it's not present. Default behavior + * uses the <code>xsi:type</code> value to choose between different setter + * methods, but otherwise ignores it. + */ + REQUIRE_XSI_TYPE +} Modified: branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlAppenders.java =================================================================== --- branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlAppenders.java 2009-08-17 17:13:36 UTC (rev 109) +++ branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlAppenders.java 2009-08-17 17:26:40 UTC (rev 110) @@ -20,9 +20,9 @@ import org.w3c.dom.Element; import net.sf.practicalxml.DomUtil; -import net.sf.practicalxml.converter.bean2xml.Bean2XmlOptions; +import net.sf.practicalxml.converter.bean.Bean2XmlOptions; -import static net.sf.practicalxml.converter.bean2xml.Bean2XmlAppenders.*; +import static net.sf.practicalxml.converter.bean.Bean2XmlAppenders.*; public class TestBean2XmlAppenders Modified: branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlDriver.java =================================================================== --- branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlDriver.java 2009-08-17 17:13:36 UTC (rev 109) +++ branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBean2XmlDriver.java 2009-08-17 17:26:40 UTC (rev 110) @@ -27,8 +27,8 @@ import net.sf.practicalxml.DomUtil; import net.sf.practicalxml.OutputUtil; -import net.sf.practicalxml.converter.bean2xml.Bean2XmlDriver; -import net.sf.practicalxml.converter.bean2xml.Bean2XmlOptions; +import net.sf.practicalxml.converter.bean.Bean2XmlDriver; +import net.sf.practicalxml.converter.bean.Bean2XmlOptions; import net.sf.practicalxml.junit.DomAsserts; Modified: branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBeanConverter.java =================================================================== --- branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBeanConverter.java 2009-08-17 17:13:36 UTC (rev 109) +++ branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestBeanConverter.java 2009-08-17 17:26:40 UTC (rev 110) @@ -16,7 +16,7 @@ import net.sf.practicalxml.DomUtil; import net.sf.practicalxml.OutputUtil; -import net.sf.practicalxml.converter.bean2xml.Bean2XmlOptions; +import net.sf.practicalxml.converter.bean.Bean2XmlOptions; import javax.xml.XMLConstants; import org.w3c.dom.Document; Modified: branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestXml2BeanDriver.java =================================================================== --- branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestXml2BeanDriver.java 2009-08-17 17:13:36 UTC (rev 109) +++ branches/dev-1.1/src/test/java/net/sf/practicalxml/converter/TestXml2BeanDriver.java 2009-08-17 17:26:40 UTC (rev 110) @@ -27,8 +27,8 @@ import javax.xml.XMLConstants; import org.w3c.dom.Element; -import net.sf.practicalxml.converter.xml2bean.Xml2BeanDriver; -import net.sf.practicalxml.converter.xml2bean.Xml2BeanOptions; +import net.sf.practicalxml.converter.bean.Xml2BeanDriver; +import net.sf.practicalxml.converter.bean.Xml2BeanOptions; import static net.sf.practicalxml.builder.XmlBuilder.*; This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |