Coverage Report - net.sf.practicalxml.converter.bean.Bean2XmlConverter
 
Classes in this File Line Coverage Branch Coverage Complexity
Bean2XmlConverter
98%
143/145
95%
76/80
3.45
 
 1  
 // Copyright 2008-2014 severally by the contributors
 2  
 //
 3  
 // Licensed under the Apache License, Version 2.0 (the "License");
 4  
 // you may not use this file except in compliance with the License.
 5  
 // You may obtain a copy of the License at
 6  
 //
 7  
 //     http://www.apache.org/licenses/LICENSE-2.0
 8  
 //
 9  
 // Unless required by applicable law or agreed to in writing, software
 10  
 // distributed under the License is distributed on an "AS IS" BASIS,
 11  
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  
 // See the License for the specific language governing permissions and
 13  
 // limitations under the License.
 14  
 
 15  
 package net.sf.practicalxml.converter.bean;
 16  
 
 17  
 import java.lang.reflect.Array;
 18  
 import java.lang.reflect.Method;
 19  
 import java.util.ArrayList;
 20  
 import java.util.Calendar;
 21  
 import java.util.Collection;
 22  
 import java.util.Collections;
 23  
 import java.util.Date;
 24  
 import java.util.EnumSet;
 25  
 import java.util.List;
 26  
 import java.util.Map;
 27  
 
 28  
 import org.w3c.dom.Element;
 29  
 
 30  
 import net.sf.kdgcommons.bean.Introspection;
 31  
 import net.sf.kdgcommons.bean.IntrospectionCache;
 32  
 import net.sf.kdgcommons.codec.Base64Codec;
 33  
 import net.sf.kdgcommons.codec.HexCodec;
 34  
 import net.sf.kdgcommons.lang.StringUtil;
 35  
 import net.sf.practicalxml.DomUtil;
 36  
 import net.sf.practicalxml.XmlUtil;
 37  
 import net.sf.practicalxml.converter.ConversionException;
 38  
 import net.sf.practicalxml.converter.ConversionConstants;
 39  
 import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.Appender;
 40  
 import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.BasicAppender;
 41  
 import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.DirectAppender;
 42  
 import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.IndexedAppender;
 43  
 import net.sf.practicalxml.converter.bean.Bean2XmlAppenders.MapAppender;
 44  
 import net.sf.practicalxml.converter.internal.ConversionUtils;
 45  
 import net.sf.practicalxml.converter.internal.JavaStringConversions;
 46  
 import net.sf.practicalxml.converter.internal.TypeUtils;
 47  
 
 48  
 
 49  
 /**
 50  
  *  Driver class for converting a Java bean into an XML DOM. Normal usage is
 51  
  *  to create a single instance of this class with desired options, then use
 52  
  *  it for multiple conversions. This class is thread-safe.
 53  
  *  <p>
 54  
  *  <em>Note:</em>
 55  
  *  this class is intended to convert simple data transfer beans, using those
 56  
  *  objects' public getters and setters.
 57  
  *
 58  
  *  @since 1.1
 59  
  */
 60  
 public class Bean2XmlConverter
 61  
 {
 62  
     private EnumSet<Bean2XmlOptions> _options;
 63  
     private IntrospectionCache _introspections;
 64  
     private JavaStringConversions _converter;
 65  
     List<ConversionException> _deferredExceptions;
 66  
 
 67  
     private boolean _setAccessible;
 68  
 
 69  
     public Bean2XmlConverter(Bean2XmlOptions... options)
 70  104
     {
 71  104
         _options = EnumSet.noneOf(Bean2XmlOptions.class);
 72  168
         for (Bean2XmlOptions option : options)
 73  64
             _options.add(option);
 74  
 
 75  104
         _introspections = new IntrospectionCache(_options.contains(Bean2XmlOptions.CACHE_INTROSPECTIONS));
 76  104
         _converter = new JavaStringConversions(shouldUseXsdFormatting());
 77  
 
 78  104
         _setAccessible = _options.contains(Bean2XmlOptions.SET_ACCESSIBLE);
 79  104
     }
 80  
 
 81  
 
 82  
 //----------------------------------------------------------------------------
 83  
 //  Public methods
 84  
 //----------------------------------------------------------------------------
 85  
 
 86  
     /**
 87  
      *  Creates an XML DOM with the specified root element name, and fills it
 88  
      *  by introspecting the passed object.
 89  
      *  <p>
 90  
      *  Neither elements nor attributes in the resulting DOM are namespaced.
 91  
      */
 92  
     public Element convert(Object obj, String rootName)
 93  
     {
 94  135
         return convert(obj, null, rootName);
 95  
     }
 96  
 
 97  
 
 98  
     /**
 99  
      *  Creates an XML DOM with the specified root element name and namespace
 100  
      *  URI, and fills it by introspecting the passed object.
 101  
      *  <p>
 102  
      *  All decendent elements will use the same namespace URI (and prefix, if
 103  
      *  provided) as the root. Attributes created by the converter use the
 104  
      *  "conversion" namespace, found in {@link ConversionConstants}.
 105  
      */
 106  
     public Element convert(Object obj, String nsUri, String rootName)
 107  
     {
 108  137
         Element root = DomUtil.newDocument(nsUri, rootName);
 109  137
         doNamespaceHack(root);
 110  137
         convert(obj, rootName, new DirectAppender(_options, root, obj));
 111  135
         return root;
 112  
     }
 113  
 
 114  
 
 115  
     /**
 116  
      *  If the deferred exceptions option has been set, returns the list of
 117  
      *  deferred exceptions. Returns an empty list if the option was not set,
 118  
      *  or if no exceptions were thrown.
 119  
      *  <p>
 120  
      *  The returned list is immutable.
 121  
      */
 122  
     public List<ConversionException> getDeferredExceptions()
 123  
     {
 124  1
         if (_deferredExceptions != null)
 125  1
             return Collections.unmodifiableList(_deferredExceptions);
 126  
         else
 127  0
             return Collections.emptyList();
 128  
     }
 129  
 
 130  
 
 131  
 //----------------------------------------------------------------------------
 132  
 //  Internals
 133  
 //----------------------------------------------------------------------------
 134  
 
 135  
     /**
 136  
      *  Introspects the passed object, and appends its contents to the output.
 137  
      */
 138  
     private void convert(Object obj, String name, Appender appender)
 139  
     {
 140  
         try
 141  
         {
 142  379
             if (obj == null)
 143  8
                 convertAsNull(null, name, appender);
 144  371
             else if (_converter.isConvertableToString(obj))
 145  264
                 convertSimple(obj, name, appender);
 146  107
             else if (obj instanceof Enum<?>)
 147  5
                 convertAsEnum(obj, name, appender);
 148  102
             else if (obj instanceof byte[])
 149  5
                 convertAsByteArray(obj, name, appender);
 150  97
             else if (obj.getClass().isArray())
 151  11
                 convertAsArray(obj, name, appender);
 152  86
             else if (obj instanceof Map)
 153  11
                 convertAsMap(obj, name, appender);
 154  75
             else if (obj instanceof Collection)
 155  23
                 convertAsCollection(obj, name, appender);
 156  52
             else if (obj instanceof Date)
 157  12
                 convertAsDate(obj, name, appender);
 158  40
             else if (obj instanceof Calendar)
 159  2
                 convertAsCalendar(obj, name, appender);
 160  
             else
 161  38
                 convertAsBean(obj, name, appender);
 162  
         }
 163  4
         catch (Exception ex)
 164  
         {
 165  4
             ConversionException ex2 = (ex instanceof ConversionException)
 166  
                                     ? new ConversionException((ConversionException)ex, name)
 167  
                                     : new ConversionException("unable to convert", name, ex);
 168  3
             if (! exceptionDeferred(ex2))
 169  3
                 throw ex2;
 170  375
         }
 171  375
     }
 172  
 
 173  
 
 174  
     private boolean exceptionDeferred(ConversionException ex)
 175  
     {
 176  7
         if (! _options.contains(Bean2XmlOptions.DEFER_EXCEPTIONS))
 177  5
             return false;
 178  
 
 179  2
         if (_deferredExceptions == null)
 180  1
             _deferredExceptions = new ArrayList<ConversionException>();
 181  
 
 182  2
         _deferredExceptions.add(ex);
 183  2
         return true;
 184  
     }
 185  
 
 186  
 
 187  
     private boolean shouldUseXsdFormatting()
 188  
     {
 189  104
         return _options.contains(Bean2XmlOptions.XSD_FORMAT)
 190  
             || _options.contains(Bean2XmlOptions.USE_TYPE_ATTR);
 191  
     }
 192  
 
 193  
 
 194  
     /**
 195  
      *  Introduces namespaces at the root level, because the Xerces serializer
 196  
      *  does not attempt to promote namespace definitions above the element in
 197  
      *  which they first appear. This means that the same declaration may be
 198  
      *  repeated at multiple places throughout a tree.
 199  
      *  <p>
 200  
      *  Will only introduce namespaces appropriate to the options in effect
 201  
      *  (ie, if you don't enable <code>xsi:nil</code>, then there's no need
 202  
      *  to declare the XML Schema Instance namespace).
 203  
      */
 204  
     private void doNamespaceHack(Element root)
 205  
     {
 206  137
         if (_options.contains(Bean2XmlOptions.NULL_AS_XSI_NIL))
 207  
         {
 208  4
             ConversionUtils.setXsiNil(root, false);
 209  
         }
 210  
 
 211  
         // I think it's more clear to express the rules this way, rather than
 212  
         // as an if-condition with nested sub-conditions
 213  137
         boolean addCnvNS = _options.contains(Bean2XmlOptions.USE_INDEX_ATTR);
 214  137
         addCnvNS |= !_options.contains(Bean2XmlOptions.MAP_KEYS_AS_ELEMENT_NAME);
 215  137
         addCnvNS &= _options.contains(Bean2XmlOptions.USE_TYPE_ATTR);
 216  137
         if (addCnvNS)
 217  
         {
 218  27
             ConversionUtils.setAttribute(root, ConversionConstants.AT_DUMMY, "");
 219  
         }
 220  137
     }
 221  
 
 222  
 
 223  
     private void convertAsNull(Class<?> klass, String name, Appender appender)
 224  
     {
 225  21
         appender.appendValue(name, klass, null);
 226  21
     }
 227  
 
 228  
 
 229  
     private void convertSimple(Object obj, String name, Appender appender)
 230  
     {
 231  264
         appender.appendValue(name, obj.getClass(), _converter.stringify(obj));
 232  263
     }
 233  
 
 234  
 
 235  
     private void convertAsDate(Object obj, String name, Appender appender)
 236  
     {
 237  12
         String value = _options.contains(Bean2XmlOptions.XSD_FORMAT)
 238  
                      ? XmlUtil.formatXsdDatetime((Date)obj)
 239  
                      : obj.toString();
 240  12
         appender.appendValue(name, obj.getClass(), value);
 241  12
     }
 242  
 
 243  
 
 244  
     private void convertAsEnum(Object obj, String name, Appender appender)
 245  
     {
 246  5
         String enumName = ((Enum<?>)obj).name();
 247  5
         if (_options.contains(Bean2XmlOptions.ENUM_AS_NAME_AND_VALUE))
 248  
         {
 249  2
             Element elem = appender.appendValue(name, obj.getClass(), obj.toString());
 250  2
             ConversionUtils.setAttribute(
 251  
                         elem,
 252  
                         ConversionConstants.AT_ENUM_NAME,
 253  
                         enumName);
 254  2
         }
 255  
         else
 256  
         {
 257  3
             appender.appendValue(name, obj.getClass(), enumName);
 258  
         }
 259  5
     }
 260  
 
 261  
 
 262  
     private void convertAsByteArray(Object obj, String name, Appender appender)
 263  
     {
 264  5
         if (_options.contains(Bean2XmlOptions.BYTE_ARRAYS_AS_BASE64))
 265  
         {
 266  2
             String value = new Base64Codec().toString((byte[])obj);
 267  2
             Element child = appender.appendValue(name, obj.getClass(), value);
 268  2
             appender.overrideType(child, TypeUtils.XSD_BASE64);
 269  2
         }
 270  3
         else if (_options.contains(Bean2XmlOptions.BYTE_ARRAYS_AS_HEX))
 271  
         {
 272  2
             String value = new HexCodec().toString((byte[])obj);
 273  2
             Element child = appender.appendValue(name, obj.getClass(), value);
 274  2
             appender.overrideType(child, TypeUtils.XSD_HEXBINARY);
 275  2
         }
 276  
         else
 277  1
             convertAsArray(obj, name, appender);
 278  5
     }
 279  
 
 280  
 
 281  
     private void convertAsArray(Object obj, String name, Appender appender)
 282  
     {
 283  12
         String childName = determineChildNameForSequence(name);
 284  12
         Appender childAppender = appender;
 285  12
         if (!_options.contains(Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS))
 286  
         {
 287  10
             Element parent = appender.appendContainer(name, obj.getClass());
 288  10
             childAppender = new IndexedAppender(appender, parent, obj);
 289  
         }
 290  
 
 291  12
         int length = Array.getLength(obj);
 292  54
         for (int idx = 0 ; idx < length ; idx++)
 293  
         {
 294  42
             Object value = Array.get(obj, idx);
 295  42
             convert(value, childName, childAppender);
 296  
         }
 297  12
     }
 298  
 
 299  
 
 300  
     private void convertAsMap(Object obj, String name, Appender appender)
 301  
     {
 302  11
         Element parent = appender.appendContainer(name, obj.getClass());
 303  11
         Appender childAppender = new MapAppender(appender, parent, obj);
 304  11
         for (Map.Entry<?,?> entry : ((Map<?,?>)obj).entrySet())
 305  
         {
 306  23
             convert(entry.getValue(), String.valueOf(entry.getKey()), childAppender);
 307  22
         }
 308  10
     }
 309  
 
 310  
 
 311  
     private void convertAsCollection(Object obj, String name, Appender appender)
 312  
     {
 313  23
         String childName = determineChildNameForSequence(name);
 314  23
         Appender childAppender = appender;
 315  23
         if (!_options.contains(Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS))
 316  
         {
 317  22
             Element parent = appender.appendContainer(name, obj.getClass());
 318  22
             childAppender = new IndexedAppender(appender, parent, obj);
 319  
         }
 320  
 
 321  23
         for (Object value : (Collection<?>)obj)
 322  
         {
 323  68
             convert(value, childName, childAppender);
 324  68
         }
 325  23
     }
 326  
 
 327  
     private void convertAsCalendar(Object obj, String name, Appender appender)
 328  
     {
 329  2
         Element parent = appender.appendContainer(name, obj.getClass());
 330  2
         Appender childAppender = new BasicAppender(appender, parent, obj);
 331  
 
 332  2
         Calendar cal = (Calendar)obj;
 333  2
         convert(cal.getTime(), ConversionConstants.EL_CALENDAR_DATE, childAppender);
 334  2
         convert(cal.getTimeZone(), ConversionConstants.EL_CALENDAR_TIMEZONE, childAppender);
 335  2
         convert(cal.getTimeInMillis(), ConversionConstants.EL_CALENDAR_MILLIS, childAppender);
 336  
         // the Calendar API docs say the following fields are set from locale; since
 337  
         // we can't get that locale from an instance, we need to store them explicitly
 338  2
         convert(Integer.valueOf(cal.getFirstDayOfWeek()), ConversionConstants.EL_CALENDAR_FIRST_DAY, childAppender);
 339  2
         convert(Integer.valueOf(cal.getMinimalDaysInFirstWeek()), ConversionConstants.EL_CALENDAR_MIN_DAYS, childAppender);
 340  2
     }
 341  
 
 342  
 
 343  
     private void convertAsBean(Object obj, String name, Appender appender)
 344  
     {
 345  38
         Element parent = appender.appendContainer(name, obj.getClass());
 346  38
         Appender childAppender = new BasicAppender(appender, parent, obj);
 347  38
         Introspection ispec = _introspections.lookup(obj.getClass(), _setAccessible);
 348  38
         for (String propName : ispec.propertyNames())
 349  
         {
 350  117
             convertBeanProperty(obj, ispec, propName, childAppender);
 351  115
         }
 352  36
     }
 353  
 
 354  
 
 355  
     private void convertBeanProperty(Object bean, Introspection ispec, String propName, Appender appender)
 356  
     {
 357  
         Object value;
 358  
         try
 359  
         {
 360  117
             Method getter = ispec.getter(propName);
 361  117
             value = (getter != null)
 362  
                   ? getter.invoke(bean)
 363  
                   : null;
 364  
 
 365  115
             if (value == null)
 366  13
                 convertAsNull(ispec.type(propName), propName, appender);
 367  102
             else if (appender.shouldSkip(value))
 368  2
                 return;
 369  
             else
 370  99
                 convert(value, propName, appender);
 371  
         }
 372  4
         catch (Exception ex)
 373  
         {
 374  
 
 375  4
             ConversionException ex2 = (ex instanceof ConversionException)
 376  
                                     ? new ConversionException((ConversionException)ex, propName)
 377  
                                     : new ConversionException("unable to retrieve bean property", propName, ex);
 378  4
             if (! exceptionDeferred(ex2))
 379  2
                 throw ex2;
 380  111
         }
 381  113
     }
 382  
 
 383  
 
 384  
     private String determineChildNameForSequence(String parentName)
 385  
     {
 386  35
         if (StringUtil.isEmpty(parentName))
 387  0
             return ConversionConstants.EL_COLLECTION_ITEM;
 388  
 
 389  35
         if (_options.contains(Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS))
 390  3
             return parentName;
 391  
 
 392  32
         if (!_options.contains(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT))
 393  25
             return ConversionConstants.EL_COLLECTION_ITEM;
 394  
 
 395  7
         if (parentName.endsWith("s") || parentName.endsWith("S"))
 396  3
             return parentName.substring(0, parentName.length() - 1);
 397  
 
 398  4
         return parentName;
 399  
     }
 400  
 }