001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.xml;
014
015import static org.apache.juneau.internal.ArrayUtils.*;
016import static org.apache.juneau.xml.XmlSerializer.*;
017import static org.apache.juneau.xml.XmlSerializerSession.ContentResult.*;
018import static org.apache.juneau.xml.XmlSerializerSession.JsonType.*;
019import static org.apache.juneau.xml.annotation.XmlFormat.*;
020
021import java.lang.reflect.*;
022import java.util.*;
023
024import org.apache.juneau.*;
025import org.apache.juneau.internal.*;
026import org.apache.juneau.serializer.*;
027import org.apache.juneau.transform.*;
028import org.apache.juneau.xml.annotation.*;
029
030/**
031 * Session object that lives for the duration of a single use of {@link XmlSerializer}.
032 * 
033 * <p>
034 * This class is NOT thread safe.
035 * It is typically discarded after one-time use although it can be reused within the same thread.
036 */
037@SuppressWarnings({"unchecked","rawtypes"})
038public class XmlSerializerSession extends WriterSerializerSession {
039
040   final boolean
041      autoDetectNamespaces,
042      enableNamespaces,
043      addNamespaceUrlsToRoot,
044      addBeanTypeProperties;
045
046   Namespace
047      defaultNamespace;
048   final Namespace
049      xsNamespace;
050
051   Namespace[] namespaces = new Namespace[0];
052
053   /**
054    * Create a new session using properties specified in the context.
055    * 
056    * @param ctx
057    *    The context creating this session object.
058    *    The context contains all the configuration settings for this object.
059    * @param args
060    *    Runtime arguments.
061    *    These specify session-level information such as locale and URI context.
062    *    It also include session-level properties that override the properties defined on the bean and
063    *    serializer contexts.
064    */
065   protected XmlSerializerSession(XmlSerializer ctx, SerializerSessionArgs args) {
066      super(ctx, args);
067      enableNamespaces = getProperty(XML_enableNamespaces, boolean.class, ctx.enableNamespaces);
068      autoDetectNamespaces = getProperty(XML_autoDetectNamespaces, boolean.class, ctx.autoDetectNamespaces);
069      addNamespaceUrlsToRoot = getProperty(XML_addNamespaceUrisToRoot, boolean.class, ctx.addNamespaceUrlsToRoot);
070      addBeanTypeProperties = getProperty(XML_addBeanTypeProperties, boolean.class, ctx.addBeanTypeProperties);
071      namespaces = getInstanceArrayProperty(XML_namespaces, Namespace.class, ctx.namespaces);
072      defaultNamespace = findDefaultNamespace(getInstanceProperty(XML_defaultNamespace, Namespace.class, ctx.defaultNamespace));
073      xsNamespace = getInstanceProperty(XML_xsNamespace, Namespace.class, ctx.xsNamespace);
074   }
075
076   private Namespace findDefaultNamespace(Namespace n) {
077      if (n == null)
078         return null;
079      if (n.name != null && n.uri != null)
080         return n;
081      if (n.uri == null) {
082         for (Namespace n2 : namespaces)
083            if (n2.name.equals(n.name))
084               return n2;
085      }
086      if (n.name == null) {
087         for (Namespace n2 : namespaces)
088            if (n2.uri.equals(n.uri))
089               return n2;
090      }
091      return n;
092   }
093   
094   @Override /* Session */
095   public ObjectMap asMap() {
096      return super.asMap()
097         .append("XmlSerializerSession", new ObjectMap()
098            .append("addBeanTypeProperties", addBeanTypeProperties)
099            .append("addNamespaceUrlsToRoot", addNamespaceUrlsToRoot)
100            .append("autoDetectNamespaces", autoDetectNamespaces)
101            .append("defaultNamespace", defaultNamespace)
102            .append("enableNamespaces", enableNamespaces)
103            .append("namespaces", namespaces)
104            .append("xsNamespace", xsNamespace)
105         );
106   }
107
108   /*
109    * Add a namespace to this session.
110    * 
111    * @param ns The namespace being added.
112    */
113   private void addNamespace(Namespace ns) {
114      if (ns == defaultNamespace)
115         return;
116
117      for (Namespace n : namespaces)
118         if (n == ns)
119            return;
120
121      if (defaultNamespace != null && (ns.uri.equals(defaultNamespace.uri) || ns.name.equals(defaultNamespace.name)))
122         defaultNamespace = ns;
123      else
124         namespaces = append(namespaces, ns);
125   }
126
127   /**
128    * Returns the {@link XmlSerializer#XML_addBeanTypeProperties} setting value for this session.
129    * 
130    * @return The {@link XmlSerializer#XML_addBeanTypeProperties} setting value for this session.
131    */
132   @Override /* SerializerSession */
133   protected boolean isAddBeanTypeProperties() {
134      return addBeanTypeProperties;
135   }
136
137   /**
138    * Returns <jk>true</jk> if we're serializing HTML.
139    * 
140    * <p>
141    * The difference in behavior is how empty non-void elements are handled.
142    * The XML serializer will produce a collapsed tag, whereas the HTML serializer will produce a start and end tag.
143    * 
144    * @return <jk>true</jk> if we're generating HTML.
145    */
146   protected boolean isHtmlMode() {
147      return false;
148   }
149
150   /**
151    * Converts the specified output target object to an {@link XmlWriter}.
152    * 
153    * @param out The output target object.
154    * @return The output target object wrapped in an {@link XmlWriter}.
155    * @throws Exception
156    */
157   public final XmlWriter getXmlWriter(SerializerPipe out) throws Exception {
158      Object output = out.getRawOutput();
159      if (output instanceof XmlWriter)
160         return (XmlWriter)output;
161      XmlWriter w = new XmlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), getUriResolver(), enableNamespaces, defaultNamespace);
162      out.setWriter(w);
163      return w;
164   }
165
166   @Override /* Serializer */
167   protected void doSerialize(SerializerPipe out, Object o) throws Exception {
168      if (enableNamespaces && autoDetectNamespaces)
169         findNsfMappings(o);
170      serializeAnything(getXmlWriter(out), o, getExpectedRootType(o), null, null, enableNamespaces && addNamespaceUrlsToRoot, XmlFormat.DEFAULT, false, false, null);
171   }
172
173   /**
174    * Recursively searches for the XML namespaces on the specified POJO and adds them to the serializer context object.
175    * 
176    * @param o The POJO to check.
177    * @throws SerializeException
178    */
179   protected final void findNsfMappings(Object o) throws SerializeException {
180      ClassMeta<?> aType = null;                // The actual type
181      aType = push(null, o, null);
182
183      if (aType != null) {
184         Namespace ns = aType.getExtendedMeta(XmlClassMeta.class).getNamespace();
185         if (ns != null) {
186            if (ns.uri != null)
187               addNamespace(ns);
188            else
189               ns = null;
190         }
191      }
192
193      // Handle recursion
194      if (aType != null && ! aType.isPrimitive()) {
195
196         BeanMap<?> bm = null;
197         if (aType.isBeanMap()) {
198            bm = (BeanMap<?>)o;
199         } else if (aType.isBean()) {
200            bm = toBeanMap(o);
201         } else if (aType.isDelegate()) {
202            ClassMeta<?> innerType = ((Delegate<?>)o).getClassMeta();
203            Namespace ns = innerType.getExtendedMeta(XmlClassMeta.class).getNamespace();
204            if (ns != null) {
205               if (ns.uri != null)
206                  addNamespace(ns);
207               else
208                  ns = null;
209            }
210
211            if (innerType.isBean()) {
212               for (BeanPropertyMeta bpm : innerType.getBeanMeta().getPropertyMetas()) {
213                  if (bpm.canRead()) {
214                     ns = bpm.getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace();
215                     if (ns != null && ns.uri != null)
216                        addNamespace(ns);
217                  }
218               }
219
220            } else if (innerType.isMap()) {
221               for (Object o2 : ((Map<?,?>)o).values())
222                  findNsfMappings(o2);
223            } else if (innerType.isCollection()) {
224               for (Object o2 : ((Collection<?>)o))
225                  findNsfMappings(o2);
226            }
227
228         } else if (aType.isMap()) {
229            for (Object o2 : ((Map<?,?>)o).values())
230               findNsfMappings(o2);
231         } else if (aType.isCollection()) {
232            for (Object o2 : ((Collection<?>)o))
233               findNsfMappings(o2);
234         } else if (aType.isArray() && ! aType.getElementType().isPrimitive()) {
235            for (Object o2 : ((Object[])o))
236               findNsfMappings(o2);
237         }
238         if (bm != null) {
239            for (BeanPropertyValue p : bm.getValues(isTrimNulls())) {
240
241               Namespace ns = p.getMeta().getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace();
242               if (ns != null && ns.uri != null)
243                  addNamespace(ns);
244
245               try {
246                  findNsfMappings(p.getValue());
247               } catch (Throwable x) {
248                  // Ignore
249               }
250            }
251         }
252      }
253
254      pop();
255   }
256
257   /**
258    * Workhorse method.
259    * 
260    * @param out The writer to send the output to.
261    * @param o The object to serialize.
262    * @param eType The expected type if this is a bean property value being serialized.
263    * @param elementName The root element name.
264    * @param elementNamespace The namespace of the element.
265    * @param addNamespaceUris Flag indicating that namespace URIs need to be added.
266    * @param format The format to serialize the output to.
267    * @param isMixed We're serializing mixed content, so don't use whitespace.
268    * @param preserveWhitespace
269    *    <jk>true</jk> if we're serializing {@link XmlFormat#MIXED_PWS} or {@link XmlFormat#TEXT_PWS}.
270    * @param pMeta The bean property metadata if this is a bean property being serialized.
271    * @return The same writer passed in so that calls to the writer can be chained.
272    * @throws Exception If a problem occurred trying to convert the output.
273    */
274   protected XmlWriter serializeAnything(
275         XmlWriter out,
276         Object o,
277         ClassMeta<?> eType,
278         String elementName,
279         Namespace elementNamespace,
280         boolean addNamespaceUris,
281         XmlFormat format,
282         boolean isMixed,
283         boolean preserveWhitespace,
284         BeanPropertyMeta pMeta) throws Exception {
285
286      JsonType type = null;              // The type string (e.g. <type> or <x x='type'>
287      int i = isMixed ? 0 : indent;       // Current indentation
288      ClassMeta<?> aType = null;     // The actual type
289      ClassMeta<?> wType = null;     // The wrapped type (delegate)
290      ClassMeta<?> sType = object(); // The serialized type
291
292      aType = push(elementName, o, eType);
293
294      if (eType == null)
295         eType = object();
296
297      // Handle recursion
298      if (aType == null) {
299         o = null;
300         aType = object();
301      }
302
303      if (o != null) {
304
305         if (aType.isDelegate()) {
306            wType = aType;
307            eType = aType = ((Delegate<?>)o).getClassMeta();
308         }
309
310         sType = aType;
311
312         // Swap if necessary
313         PojoSwap swap = aType.getPojoSwap(this);
314         if (swap != null) {
315            o = swap.swap(this, o);
316            sType = swap.getSwapClassMeta(this);
317
318            // If the getSwapClass() method returns Object, we need to figure out
319            // the actual type now.
320            if (sType.isObject())
321               sType = getClassMetaForObject(o);
322         }
323      } else {
324         sType = eType.getSerializedClassMeta(this);
325      }
326
327      // Does the actual type match the expected type?
328      boolean isExpectedType = true;
329      if (o == null || ! eType.same(aType)) {
330         if (eType.isNumber())
331            isExpectedType = aType.isNumber();
332         else if (eType.isMap())
333            isExpectedType = aType.isMap();
334         else if (eType.isCollectionOrArray())
335            isExpectedType = aType.isCollectionOrArray();
336         else
337            isExpectedType = false;
338      }
339
340      String resolvedDictionaryName = isExpectedType ? null : aType.getDictionaryName();
341
342      // Note that the dictionary name may be specified on the actual type or the serialized type.
343      // HTML templates will have them defined on the serialized type.
344      String dictionaryName = aType.getDictionaryName();
345      if (dictionaryName == null)
346         dictionaryName = sType.getDictionaryName();
347
348      // char '\0' is interpreted as null.
349      if (o != null && sType.isChar() && ((Character)o).charValue() == 0)
350         o = null;
351
352      boolean isCollapsed = false;     // If 'true', this is a collection and we're not rendering the outer element.
353      boolean isRaw = (sType.isReader() || sType.isInputStream()) && o != null;
354
355      // Get the JSON type string.
356      if (o == null) {
357         type = NULL;
358      } else if (sType.isCharSequence() || sType.isChar()) {
359         type = STRING;
360      } else if (sType.isNumber()) {
361         type = NUMBER;
362      } else if (sType.isBoolean()) {
363         type = BOOLEAN;
364      } else if (sType.isMapOrBean()) {
365         isCollapsed = sType.getExtendedMeta(XmlClassMeta.class).getFormat() == COLLAPSED;
366         type = OBJECT;
367      } else if (sType.isCollectionOrArray()) {
368         isCollapsed = (format == COLLAPSED && ! addNamespaceUris);
369         type = ARRAY;
370      } else {
371         type = STRING;
372      }
373
374      if (format.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT) && type.isOneOf(NULL,STRING,NUMBER,BOOLEAN))
375         isCollapsed = true;
376
377      // Is there a name associated with this bean?
378      if (elementName == null && dictionaryName != null) {
379         elementName = dictionaryName;
380         isExpectedType = true;
381      }
382
383      if (enableNamespaces) {
384         if (elementNamespace == null)
385            elementNamespace = sType.getExtendedMeta(XmlClassMeta.class).getNamespace();
386         if (elementNamespace == null)
387            elementNamespace = aType.getExtendedMeta(XmlClassMeta.class).getNamespace();
388         if (elementNamespace != null && elementNamespace.uri == null)
389            elementNamespace = null;
390         if (elementNamespace == null)
391            elementNamespace = defaultNamespace;
392      } else {
393         elementNamespace = null;
394      }
395
396      // Do we need a carriage return after the start tag?
397      boolean cr = o != null && (sType.isMapOrBean() || sType.isCollectionOrArray()) && ! isMixed;
398
399      String en = elementName;
400      if (en == null && ! isRaw) {
401         en = type.toString();
402         type = null;
403      }
404      boolean encodeEn = elementName != null;
405      String ns = (elementNamespace == null ? null : elementNamespace.name);
406      String dns = null, elementNs = null;
407      if (enableNamespaces) {
408         dns = elementName == null && defaultNamespace != null ? defaultNamespace.name : null;
409         elementNs = elementName == null ? dns : ns;
410         if (elementName == null)
411            elementNamespace = null;
412      }
413
414      // Render the start tag.
415      if (! isCollapsed) {
416         if (en != null) {
417            out.oTag(i, elementNs, en, encodeEn);
418            if (addNamespaceUris) {
419               out.attr((String)null, "xmlns", defaultNamespace.getUri());
420
421               for (Namespace n : namespaces)
422                  out.attr("xmlns", n.getName(), n.getUri());
423            }
424            if (! isExpectedType) {
425               if (resolvedDictionaryName != null)
426                  out.attr(dns, getBeanTypePropertyName(eType), resolvedDictionaryName);
427               else if (type != null && type != STRING)
428                  out.attr(dns, getBeanTypePropertyName(eType), type);
429            }
430         } else {
431            out.i(i);
432         }
433         if (o == null) {
434            if ((sType.isBoolean() || sType.isNumber()) && ! sType.isNullable())
435               o = sType.getPrimitiveDefault();
436         }
437
438         if (o != null && ! (sType.isMapOrBean() || en == null))
439            out.append('>');
440
441         if (cr && ! (sType.isMapOrBean()))
442            out.nl(i+1);
443      }
444
445      ContentResult rc = CR_ELEMENTS;
446
447      // Render the tag contents.
448      if (o != null) {
449         if (sType.isUri() || (pMeta != null && pMeta.isUri())) {
450            out.textUri(o);
451         } else if (sType.isCharSequence() || sType.isChar()) {
452            if (format == XMLTEXT)
453               out.append(o);
454            else
455               out.text(o, preserveWhitespace);
456         } else if (sType.isNumber() || sType.isBoolean()) {
457            out.append(o);
458         } else if (sType.isMap() || (wType != null && wType.isMap())) {
459            if (o instanceof BeanMap)
460               rc = serializeBeanMap(out, (BeanMap)o, elementNamespace, isCollapsed, isMixed);
461            else
462               rc = serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), isMixed);
463         } else if (sType.isBean()) {
464            rc = serializeBeanMap(out, toBeanMap(o), elementNamespace, isCollapsed, isMixed);
465         } else if (sType.isCollection() || (wType != null && wType.isCollection())) {
466            if (isCollapsed)
467               this.indent--;
468            serializeCollection(out, o, sType, eType, pMeta, isMixed);
469            if (isCollapsed)
470               this.indent++;
471         } else if (sType.isArray()) {
472            if (isCollapsed)
473               this.indent--;
474            serializeCollection(out, o, sType, eType, pMeta, isMixed);
475            if (isCollapsed)
476               this.indent++;
477         } else if (sType.isReader() || sType.isInputStream()) {
478            IOUtils.pipe(o, out);
479         } else {
480            if (format == XMLTEXT)
481               out.append(toString(o));
482            else
483               out.text(toString(o));
484         }
485      }
486
487      pop();
488
489      // Render the end tag.
490      if (! isCollapsed) {
491         if (en != null) {
492            if (rc == CR_EMPTY) {
493               if (isHtmlMode())
494                  out.append('>').eTag(elementNs, en, encodeEn);
495               else
496                  out.append('/').append('>');
497            } else if (rc == CR_VOID || o == null) {
498               out.append('/').append('>');
499            }
500            else
501               out.ie(cr && rc != CR_MIXED ? i : 0).eTag(elementNs, en, encodeEn);
502         }
503         if (! isMixed)
504            out.nl(i);
505      }
506
507      return out;
508   }
509
510   private ContentResult serializeMap(XmlWriter out, Map m, ClassMeta<?> sType,
511         ClassMeta<?> eKeyType, ClassMeta<?> eValueType, boolean isMixed) throws Exception {
512
513      m = sort(m);
514
515      ClassMeta<?> keyType = eKeyType == null ? sType.getKeyType() : eKeyType;
516      ClassMeta<?> valueType = eValueType == null ? sType.getValueType() : eValueType;
517
518      boolean hasChildren = false;
519      for (Iterator i = m.entrySet().iterator(); i.hasNext();) {
520         Map.Entry e = (Map.Entry)i.next();
521
522         Object k = e.getKey();
523         if (k == null) {
524            k = "\u0000";
525         } else {
526            k = generalize(k, keyType);
527            if (isTrimStrings() && k instanceof String)
528               k = k.toString().trim();
529         }
530
531         Object value = e.getValue();
532
533         if (! hasChildren) {
534            hasChildren = true;
535            out.append('>').nlIf(! isMixed, indent);
536         }
537         serializeAnything(out, value, valueType, toString(k), null, false, XmlFormat.DEFAULT, isMixed, false, null);
538      }
539      return hasChildren ? CR_ELEMENTS : CR_EMPTY;
540   }
541
542   private ContentResult serializeBeanMap(XmlWriter out, BeanMap<?> m,
543         Namespace elementNs, boolean isCollapsed, boolean isMixed) throws Exception {
544      boolean hasChildren = false;
545      BeanMeta<?> bm = m.getMeta();
546
547      List<BeanPropertyValue> lp = m.getValues(isTrimNulls());
548
549      XmlBeanMeta xbm = bm.getExtendedMeta(XmlBeanMeta.class);
550
551      Set<String>
552         attrs = xbm.getAttrPropertyNames(),
553         elements = xbm.getElementPropertyNames(),
554         collapsedElements = xbm.getCollapsedPropertyNames();
555      String
556         attrsProperty = xbm.getAttrsPropertyName(),
557         contentProperty = xbm.getContentPropertyName();
558
559      XmlFormat cf = null;
560
561      Object content = null;
562      ClassMeta<?> contentType = null;
563      for (BeanPropertyValue p : lp) {
564         String n = p.getName();
565         if (attrs.contains(n) || attrs.contains("*") || n.equals(attrsProperty)) {
566            BeanPropertyMeta pMeta = p.getMeta();
567            if (pMeta.canRead()) {
568               ClassMeta<?> cMeta = p.getClassMeta();
569
570               String key = p.getName();
571               Object value = p.getValue();
572               Throwable t = p.getThrown();
573               if (t != null)
574                  onBeanGetterException(pMeta, t);
575
576               if (canIgnoreValue(cMeta, key, value))
577                  continue;
578
579               Namespace ns = (enableNamespaces && pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace() != elementNs ? pMeta.getExtendedMeta(XmlBeanPropertyMeta.class).getNamespace() : null);
580
581               if (pMeta.isUri()  ) {
582                  out.attrUri(ns, key, value);
583               } else if (n.equals(attrsProperty)) {
584                  if (value instanceof BeanMap) {
585                     BeanMap<?> bm2 = (BeanMap)value;
586                     for (BeanPropertyValue p2 : bm2.getValues(true)) {
587                        String key2 = p2.getName();
588                        Object value2 = p2.getValue();
589                        Throwable t2 = p2.getThrown();
590                        if (t2 != null)
591                           onBeanGetterException(pMeta, t);
592                        out.attr(ns, key2, value2);
593                     }
594                  } else /* Map */ {
595                     Map m2 = (Map)value;
596                     for (Map.Entry e : (Set<Map.Entry>)(m2.entrySet())) {
597                        out.attr(ns, toString(e.getKey()), e.getValue());
598                     }
599                  }
600               } else {
601                  out.attr(ns, key, value);
602               }
603            }
604         }
605      }
606
607      boolean
608         hasContent = false,
609         preserveWhitespace = false,
610         isVoidElement = xbm.getContentFormat() == VOID;
611
612      for (BeanPropertyValue p : lp) {
613         BeanPropertyMeta pMeta = p.getMeta();
614         if (pMeta.canRead()) {
615            ClassMeta<?> cMeta = p.getClassMeta();
616
617            String n = p.getName();
618            if (n.equals(contentProperty)) {
619               content = p.getValue();
620               contentType = p.getClassMeta();
621               hasContent = true;
622               cf = xbm.getContentFormat();
623               if (cf.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT))
624                  isMixed = true;
625               if (cf.isOneOf(MIXED_PWS, TEXT_PWS))
626                  preserveWhitespace = true;
627               if (contentType.isCollection() && ((Collection)content).isEmpty())
628                  hasContent = false;
629               else if (contentType.isArray() && Array.getLength(content) == 0)
630                  hasContent = false;
631            } else if (elements.contains(n) || collapsedElements.contains(n) || elements.contains("*") || collapsedElements.contains("*") ) {
632               String key = p.getName();
633               Object value = p.getValue();
634               Throwable t = p.getThrown();
635               if (t != null)
636                  onBeanGetterException(pMeta, t);
637
638               if (canIgnoreValue(cMeta, key, value))
639                  continue;
640
641               if (! hasChildren) {
642                  hasChildren = true;
643                  out.appendIf(! isCollapsed, '>').nlIf(! isMixed, indent);
644               }
645
646               XmlBeanPropertyMeta xbpm = pMeta.getExtendedMeta(XmlBeanPropertyMeta.class);
647               serializeAnything(out, value, cMeta, key, xbpm.getNamespace(), false, xbpm.getXmlFormat(), isMixed, false, pMeta);
648            }
649         }
650      }
651      if (! hasContent)
652         return (hasChildren ? CR_ELEMENTS : isVoidElement ? CR_VOID : CR_EMPTY);
653      out.append('>').nlIf(! isMixed, indent);
654
655      // Serialize XML content.
656      if (content != null) {
657         if (contentType == null) {
658         } else if (contentType.isCollection()) {
659            Collection c = (Collection)content;
660            for (Iterator i = c.iterator(); i.hasNext();) {
661               Object value = i.next();
662               serializeAnything(out, value, contentType.getElementType(), null, null, false, cf, isMixed, preserveWhitespace, null);
663            }
664         } else if (contentType.isArray()) {
665            Collection c = toList(Object[].class, content);
666            for (Iterator i = c.iterator(); i.hasNext();) {
667               Object value = i.next();
668               serializeAnything(out, value, contentType.getElementType(), null, null, false, cf, isMixed, preserveWhitespace, null);
669            }
670         } else {
671            serializeAnything(out, content, contentType, null, null, false, cf, isMixed, preserveWhitespace, null);
672         }
673      } else {
674         if (! isTrimNulls()) {
675            if (! isMixed)
676               out.i(indent);
677            out.text(content);
678            if (! isMixed)
679               out.nl(indent);
680         }
681      }
682      return isMixed ? CR_MIXED : CR_ELEMENTS;
683   }
684
685   private XmlWriter serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType,
686         ClassMeta<?> eType, BeanPropertyMeta ppMeta, boolean isMixed) throws Exception {
687
688      ClassMeta<?> seType = sType.getElementType();
689      if (seType == null)
690         seType = object();
691      ClassMeta<?> eeType = eType.getElementType();
692
693      Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in));
694
695      c = sort(c);
696
697      String type2 = null;
698
699      String eName = type2;
700      Namespace eNs = null;
701
702      if (ppMeta != null) {
703         XmlBeanPropertyMeta xbpm = ppMeta.getExtendedMeta(XmlBeanPropertyMeta.class);
704         eName = xbpm.getChildName();
705         eNs = xbpm.getNamespace();
706      }
707
708      for (Iterator i = c.iterator(); i.hasNext();) {
709         Object value = i.next();
710         serializeAnything(out, value, eeType, eName, eNs, false, XmlFormat.DEFAULT, isMixed, false, null);
711      }
712      return out;
713   }
714
715   static enum JsonType {
716      STRING("string"),BOOLEAN("boolean"),NUMBER("number"),ARRAY("array"),OBJECT("object"),NULL("null");
717
718      private final String value;
719      private JsonType(String value) {
720         this.value = value;
721      }
722
723      @Override
724      public String toString() {
725         return value;
726      }
727
728      boolean isOneOf(JsonType...types) {
729         for (JsonType type : types)
730            if (type == this)
731               return true;
732         return false;
733      }
734   }
735
736   /**
737    * Identifies what the contents were of a serialized bean.
738    */
739   static enum ContentResult {
740      CR_VOID,      // No content...append "/>" to the start tag.
741      CR_EMPTY,     // No content...append "/>" to the start tag if XML, "/></end>" if HTML.
742      CR_MIXED,     // Mixed content...don't add whitespace.
743      CR_ELEMENTS   // Elements...use normal whitespace rules.
744   }
745}