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.io.IOException;
022import java.lang.reflect.*;
023import java.util.*;
024
025import org.apache.juneau.*;
026import org.apache.juneau.collections.*;
027import org.apache.juneau.internal.*;
028import org.apache.juneau.serializer.*;
029import org.apache.juneau.transform.*;
030import org.apache.juneau.xml.annotation.*;
031
032/**
033 * Session object that lives for the duration of a single use of {@link XmlSerializer}.
034 *
035 * <p>
036 * This class is NOT thread safe.
037 * It is typically discarded after one-time use although it can be reused within the same thread.
038 */
039@SuppressWarnings({"unchecked","rawtypes"})
040public class XmlSerializerSession extends WriterSerializerSession {
041
042   private final XmlSerializer ctx;
043   private Namespace
044      defaultNamespace;
045   private Namespace[] namespaces = new Namespace[0];
046
047   /**
048    * Create a new session using properties specified in the context.
049    *
050    * @param ctx
051    *    The context creating this session object.
052    *    The context contains all the configuration settings for this object.
053    * @param args
054    *    Runtime arguments.
055    *    These specify session-level information such as locale and URI context.
056    *    It also include session-level properties that override the properties defined on the bean and
057    *    serializer contexts.
058    */
059   protected XmlSerializerSession(XmlSerializer ctx, SerializerSessionArgs args) {
060      super(ctx, args);
061      this.ctx = ctx;
062      namespaces = getInstanceArrayProperty(XML_namespaces, Namespace.class, ctx.getNamespaces());
063      defaultNamespace = findDefaultNamespace(getInstanceProperty(XML_defaultNamespace, Namespace.class, ctx.getDefaultNamespace()));
064   }
065
066   private Namespace findDefaultNamespace(Namespace n) {
067      if (n == null)
068         return null;
069      if (n.name != null && n.uri != null)
070         return n;
071      if (n.uri == null) {
072         for (Namespace n2 : getNamespaces())
073            if (n2.name.equals(n.name))
074               return n2;
075      }
076      if (n.name == null) {
077         for (Namespace n2 : getNamespaces())
078            if (n2.uri.equals(n.uri))
079               return n2;
080      }
081      return n;
082   }
083
084   /*
085    * Add a namespace to this session.
086    *
087    * @param ns The namespace being added.
088    */
089   private void addNamespace(Namespace ns) {
090      if (ns == defaultNamespace)
091         return;
092
093      for (Namespace n : namespaces)
094         if (n == ns)
095            return;
096
097      if (defaultNamespace != null && (ns.uri.equals(defaultNamespace.uri) || ns.name.equals(defaultNamespace.name)))
098         defaultNamespace = ns;
099      else
100         namespaces = append(namespaces, ns);
101   }
102
103   /**
104    * Returns <jk>true</jk> if we're serializing HTML.
105    *
106    * <p>
107    * The difference in behavior is how empty non-void elements are handled.
108    * The XML serializer will produce a collapsed tag, whereas the HTML serializer will produce a start and end tag.
109    *
110    * @return <jk>true</jk> if we're generating HTML.
111    */
112   protected boolean isHtmlMode() {
113      return false;
114   }
115
116   /**
117    * Converts the specified output target object to an {@link XmlWriter}.
118    *
119    * @param out The output target object.
120    * @return The output target object wrapped in an {@link XmlWriter}.
121    * @throws IOException Thrown by underlying stream.
122    */
123   public final XmlWriter getXmlWriter(SerializerPipe out) throws IOException {
124      Object output = out.getRawOutput();
125      if (output instanceof XmlWriter)
126         return (XmlWriter)output;
127      XmlWriter w = new XmlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), getUriResolver(), isEnableNamespaces(), defaultNamespace);
128      out.setWriter(w);
129      return w;
130   }
131
132   @Override /* Serializer */
133   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
134      if (isEnableNamespaces() && isAutoDetectNamespaces())
135         findNsfMappings(o);
136      serializeAnything(getXmlWriter(out), o, getExpectedRootType(o), null, null, null, isEnableNamespaces() && isAddNamespaceUrisToRoot(), XmlFormat.DEFAULT, false, false, null);
137   }
138
139   /**
140    * Recursively searches for the XML namespaces on the specified POJO and adds them to the serializer context object.
141    *
142    * @param o The POJO to check.
143    * @throws SerializeException Thrown if bean recursion occurred.
144    */
145   protected final void findNsfMappings(Object o) throws SerializeException {
146      ClassMeta<?> aType = null;                // The actual type
147
148      try {
149         aType = push(null, o, null);
150      } catch (BeanRecursionException e) {
151         throw new SerializeException(e);
152      }
153
154      if (aType != null) {
155         Namespace ns = getXmlClassMeta(aType).getNamespace();
156         if (ns != null) {
157            if (ns.uri != null)
158               addNamespace(ns);
159            else
160               ns = null;
161         }
162      }
163
164      // Handle recursion
165      if (aType != null && ! aType.isPrimitive()) {
166
167         BeanMap<?> bm = null;
168         if (aType.isBeanMap()) {
169            bm = (BeanMap<?>)o;
170         } else if (aType.isBean()) {
171            bm = toBeanMap(o);
172         } else if (aType.isDelegate()) {
173            ClassMeta<?> innerType = ((Delegate<?>)o).getClassMeta();
174            Namespace ns = getXmlClassMeta(innerType).getNamespace();
175            if (ns != null) {
176               if (ns.uri != null)
177                  addNamespace(ns);
178               else
179                  ns = null;
180            }
181
182            if (innerType.isBean()) {
183               for (BeanPropertyMeta bpm : innerType.getBeanMeta().getPropertyMetas()) {
184                  if (bpm.canRead()) {
185                     ns = getXmlBeanPropertyMeta(bpm).getNamespace();
186                     if (ns != null && ns.uri != null)
187                        addNamespace(ns);
188                  }
189               }
190
191            } else if (innerType.isMap()) {
192               for (Object o2 : ((Map<?,?>)o).values())
193                  findNsfMappings(o2);
194            } else if (innerType.isCollection()) {
195               for (Object o2 : ((Collection<?>)o))
196                  findNsfMappings(o2);
197            }
198
199         } else if (aType.isMap()) {
200            for (Object o2 : ((Map<?,?>)o).values())
201               findNsfMappings(o2);
202         } else if (aType.isCollection()) {
203            for (Object o2 : ((Collection<?>)o))
204               findNsfMappings(o2);
205         } else if (aType.isArray() && ! aType.getElementType().isPrimitive()) {
206            for (Object o2 : ((Object[])o))
207               findNsfMappings(o2);
208         }
209         if (bm != null) {
210            for (BeanPropertyValue p : bm.getValues(isKeepNullProperties())) {
211
212               Namespace ns = getXmlBeanPropertyMeta(p.getMeta()).getNamespace();
213               if (ns != null && ns.uri != null)
214                  addNamespace(ns);
215
216               try {
217                  findNsfMappings(p.getValue());
218               } catch (Throwable x) {
219                  // Ignore
220               }
221            }
222         }
223      }
224
225      pop();
226   }
227
228   /**
229    * Workhorse method.
230    *
231    * @param out The writer to send the output to.
232    * @param o The object to serialize.
233    * @param eType The expected type if this is a bean property value being serialized.
234    * @param keyName The property name or map key name.
235    * @param elementName The root element name.
236    * @param elementNamespace The namespace of the element.
237    * @param addNamespaceUris Flag indicating that namespace URIs need to be added.
238    * @param format The format to serialize the output to.
239    * @param isMixedOrText We're serializing mixed content, so don't use whitespace.
240    * @param preserveWhitespace
241    *    <jk>true</jk> if we're serializing {@link XmlFormat#MIXED_PWS} or {@link XmlFormat#TEXT_PWS}.
242    * @param pMeta The bean property metadata if this is a bean property being serialized.
243    * @return The same writer passed in so that calls to the writer can be chained.
244    * @throws IOException Thrown by underlying stream.
245    * @throws SerializeException General serialization error occurred.
246    */
247   protected ContentResult serializeAnything(
248         XmlWriter out,
249         Object o,
250         ClassMeta<?> eType,
251         String keyName,
252         String elementName,
253         Namespace elementNamespace,
254         boolean addNamespaceUris,
255         XmlFormat format,
256         boolean isMixedOrText,
257         boolean preserveWhitespace,
258         BeanPropertyMeta pMeta) throws IOException, SerializeException {
259
260      JsonType type = null;              // The type string (e.g. <type> or <x x='type'>
261      int i = isMixedOrText ? 0 : indent;       // Current indentation
262      ClassMeta<?> aType = null;     // The actual type
263      ClassMeta<?> wType = null;     // The wrapped type (delegate)
264      ClassMeta<?> sType = object(); // The serialized type
265
266      aType = push2(keyName, o, eType);
267
268      if (eType == null)
269         eType = object();
270
271      // Handle recursion
272      if (aType == null) {
273         o = null;
274         aType = object();
275      }
276
277      // Handle Optional<X>
278      if (isOptional(aType)) {
279         o = getOptionalValue(o);
280         eType = getOptionalType(eType);
281         aType = getClassMetaForObject(o, object());
282      }
283
284      if (o != null) {
285
286         if (aType.isDelegate()) {
287            wType = aType;
288            eType = aType = ((Delegate<?>)o).getClassMeta();
289         }
290
291         sType = aType;
292
293         // Swap if necessary
294         PojoSwap swap = aType.getSwap(this);
295         if (swap != null) {
296            o = swap(swap, o);
297            sType = swap.getSwapClassMeta(this);
298
299            // If the getSwapClass() method returns Object, we need to figure out
300            // the actual type now.
301            if (sType.isObject())
302               sType = getClassMetaForObject(o);
303         }
304      } else {
305         sType = eType.getSerializedClassMeta(this);
306      }
307
308      // Does the actual type match the expected type?
309      boolean isExpectedType = true;
310      if (o == null || ! eType.same(aType)) {
311         if (eType.isNumber())
312            isExpectedType = aType.isNumber();
313         else if (eType.isMap())
314            isExpectedType = aType.isMap();
315         else if (eType.isCollectionOrArray())
316            isExpectedType = aType.isCollectionOrArray();
317         else
318            isExpectedType = false;
319      }
320
321      String resolvedDictionaryName = isExpectedType ? null : aType.getDictionaryName();
322
323      // Note that the dictionary name may be specified on the actual type or the serialized type.
324      // HTML templates will have them defined on the serialized type.
325      String dictionaryName = aType.getDictionaryName();
326      if (dictionaryName == null)
327         dictionaryName = sType.getDictionaryName();
328
329      // char '\0' is interpreted as null.
330      if (o != null && sType.isChar() && ((Character)o).charValue() == 0)
331         o = null;
332
333      boolean isCollapsed = false;     // If 'true', this is a collection and we're not rendering the outer element.
334      boolean isRaw = (sType.isReader() || sType.isInputStream()) && o != null;
335
336      // Get the JSON type string.
337      if (o == null) {
338         type = NULL;
339      } else if (sType.isCharSequence() || sType.isChar()) {
340         type = STRING;
341      } else if (sType.isNumber()) {
342         type = NUMBER;
343      } else if (sType.isBoolean()) {
344         type = BOOLEAN;
345      } else if (sType.isMapOrBean()) {
346         isCollapsed = getXmlClassMeta(sType).getFormat() == COLLAPSED;
347         type = OBJECT;
348      } else if (sType.isCollectionOrArray()) {
349         isCollapsed = (format == COLLAPSED && ! addNamespaceUris);
350         type = ARRAY;
351      } else {
352         type = STRING;
353      }
354
355      if (format.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT) && type.isOneOf(NULL,STRING,NUMBER,BOOLEAN))
356         isCollapsed = true;
357
358      // Is there a name associated with this bean?
359
360      String name = keyName;
361      if (elementName == null && dictionaryName != null) {
362         elementName = dictionaryName;
363         isExpectedType = o != null;  // preserve type='null' when it's null.
364      }
365
366      if (elementName == null) {
367         elementName = name;
368         name = null;
369      }
370
371      if (StringUtils.isEquals(name, elementName))
372         name = null;
373
374      if (isEnableNamespaces()) {
375         if (elementNamespace == null)
376            elementNamespace = getXmlClassMeta(sType).getNamespace();
377         if (elementNamespace == null)
378            elementNamespace = getXmlClassMeta(aType).getNamespace();
379         if (elementNamespace != null && elementNamespace.uri == null)
380            elementNamespace = null;
381         if (elementNamespace == null)
382            elementNamespace = defaultNamespace;
383      } else {
384         elementNamespace = null;
385      }
386
387      // Do we need a carriage return after the start tag?
388      boolean cr = o != null && (sType.isMapOrBean() || sType.isCollectionOrArray()) && ! isMixedOrText;
389
390      String en = elementName;
391      if (en == null && ! isRaw) {
392         en = type.toString();
393         type = null;
394      }
395
396      boolean encodeEn = elementName != null;
397      String ns = (elementNamespace == null ? null : elementNamespace.name);
398      String dns = null, elementNs = null;
399      if (isEnableNamespaces()) {
400         dns = elementName == null && defaultNamespace != null ? defaultNamespace.name : null;
401         elementNs = elementName == null ? dns : ns;
402         if (elementName == null)
403            elementNamespace = null;
404      }
405
406      // Render the start tag.
407      if (! isCollapsed) {
408         if (en != null) {
409            out.oTag(i, elementNs, en, encodeEn);
410            if (addNamespaceUris) {
411               out.attr((String)null, "xmlns", defaultNamespace.getUri());
412
413               for (Namespace n : namespaces)
414                  out.attr("xmlns", n.getName(), n.getUri());
415            }
416            if (! isExpectedType) {
417               if (resolvedDictionaryName != null)
418                  out.attr(dns, getBeanTypePropertyName(eType), resolvedDictionaryName);
419               else if (type != null && type != STRING)
420                  out.attr(dns, getBeanTypePropertyName(eType), type);
421            }
422            if (name != null)
423               out.attr(getNamePropertyName(), name);
424         } else {
425            out.i(i);
426         }
427         if (o == null) {
428            if ((sType.isBoolean() || sType.isNumber()) && ! sType.isNullable())
429               o = sType.getPrimitiveDefault();
430         }
431
432         if (o != null && ! (sType.isMapOrBean() || en == null))
433            out.append('>');
434
435         if (cr && ! (sType.isMapOrBean()))
436            out.nl(i+1);
437      }
438
439      ContentResult rc = CR_ELEMENTS;
440
441      // Render the tag contents.
442      if (o != null) {
443         if (sType.isUri() || (pMeta != null && pMeta.isUri())) {
444            out.textUri(o);
445         } else if (sType.isCharSequence() || sType.isChar()) {
446            if (isXmlText(format, sType))
447               out.append(o);
448            else
449               out.text(o, preserveWhitespace);
450         } else if (sType.isNumber() || sType.isBoolean()) {
451            out.append(o);
452         } else if (sType.isMap() || (wType != null && wType.isMap())) {
453            if (o instanceof BeanMap)
454               rc = serializeBeanMap(out, (BeanMap)o, elementNamespace, isCollapsed, isMixedOrText);
455            else
456               rc = serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), isMixedOrText);
457         } else if (sType.isBean()) {
458            rc = serializeBeanMap(out, toBeanMap(o), elementNamespace, isCollapsed, isMixedOrText);
459         } else if (sType.isCollection() || (wType != null && wType.isCollection())) {
460            if (isCollapsed)
461               this.indent--;
462            serializeCollection(out, o, sType, eType, pMeta, isMixedOrText);
463            if (isCollapsed)
464               this.indent++;
465         } else if (sType.isArray()) {
466            if (isCollapsed)
467               this.indent--;
468            serializeCollection(out, o, sType, eType, pMeta, isMixedOrText);
469            if (isCollapsed)
470               this.indent++;
471         } else if (sType.isReader() || sType.isInputStream()) {
472            IOUtils.pipe(o, out);
473         } else {
474            if (isXmlText(format, sType))
475               out.append(toString(o));
476            else
477               out.text(toString(o));
478         }
479      }
480
481      pop();
482
483      // Render the end tag.
484      if (! isCollapsed) {
485         if (en != null) {
486            if (rc == CR_EMPTY) {
487               if (isHtmlMode())
488                  out.append('>').eTag(elementNs, en, encodeEn);
489               else
490                  out.append('/').append('>');
491            } else if (rc == CR_VOID || o == null) {
492               out.append('/').append('>');
493            }
494            else
495               out.ie(cr && rc != CR_MIXED ? i : 0).eTag(elementNs, en, encodeEn);
496         }
497         if (! isMixedOrText)
498            out.nl(i);
499      }
500
501      return rc;
502   }
503
504   private boolean isXmlText(XmlFormat format, ClassMeta<?> sType) {
505      if (format == XMLTEXT)
506         return true;
507      XmlClassMeta xcm = getXmlClassMeta(sType);
508      if (xcm == null)
509         return false;
510      return xcm.getFormat() == XMLTEXT;
511   }
512
513   private ContentResult serializeMap(XmlWriter out, Map m, ClassMeta<?> sType,
514         ClassMeta<?> eKeyType, ClassMeta<?> eValueType, boolean isMixed) throws IOException, SerializeException {
515
516      m = sort(m);
517
518      ClassMeta<?> keyType = eKeyType == null ? sType.getKeyType() : eKeyType;
519      ClassMeta<?> valueType = eValueType == null ? sType.getValueType() : eValueType;
520
521      boolean hasChildren = false;
522      for (Iterator i = m.entrySet().iterator(); i.hasNext();) {
523         Map.Entry e = (Map.Entry)i.next();
524
525         Object k = e.getKey();
526         if (k == null) {
527            k = "\u0000";
528         } else {
529            k = generalize(k, keyType);
530            if (isTrimStrings() && k instanceof String)
531               k = k.toString().trim();
532         }
533
534         Object value = e.getValue();
535
536         if (! hasChildren) {
537            hasChildren = true;
538            out.append('>').nlIf(! isMixed, indent);
539         }
540         serializeAnything(out, value, valueType, toString(k), null, null, false, XmlFormat.DEFAULT, isMixed, false, null);
541      }
542      return hasChildren ? CR_ELEMENTS : CR_EMPTY;
543   }
544
545   private ContentResult serializeBeanMap(XmlWriter out, BeanMap<?> m,
546         Namespace elementNs, boolean isCollapsed, boolean isMixedOrText) throws IOException, SerializeException {
547      boolean hasChildren = false;
548      BeanMeta<?> bm = m.getMeta();
549
550      List<BeanPropertyValue> lp = m.getValues(isKeepNullProperties());
551
552      XmlBeanMeta xbm = getXmlBeanMeta(bm);
553
554      Set<String>
555         attrs = xbm.getAttrPropertyNames(),
556         elements = xbm.getElementPropertyNames(),
557         collapsedElements = xbm.getCollapsedPropertyNames();
558      String
559         attrsProperty = xbm.getAttrsPropertyName(),
560         contentProperty = xbm.getContentPropertyName();
561
562      XmlFormat cf = null;
563
564      Object content = null;
565      ClassMeta<?> contentType = null;
566      for (BeanPropertyValue p : lp) {
567         String n = p.getName();
568         if (attrs.contains(n) || attrs.contains("*") || n.equals(attrsProperty)) {
569            BeanPropertyMeta pMeta = p.getMeta();
570            if (pMeta.canRead()) {
571               ClassMeta<?> cMeta = p.getClassMeta();
572
573               String key = p.getName();
574               Object value = p.getValue();
575               Throwable t = p.getThrown();
576               if (t != null)
577                  onBeanGetterException(pMeta, t);
578
579               if (canIgnoreValue(cMeta, key, value))
580                  continue;
581
582               XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta);
583               Namespace ns = (isEnableNamespaces() && bpXml.getNamespace() != elementNs ? bpXml.getNamespace() : null);
584
585               if (pMeta.isUri()  ) {
586                  out.attrUri(ns, key, value);
587               } else if (n.equals(attrsProperty)) {
588                  if (value instanceof BeanMap) {
589                     BeanMap<?> bm2 = (BeanMap)value;
590                     for (BeanPropertyValue p2 : bm2.getValues(false)) {
591                        String key2 = p2.getName();
592                        Object value2 = p2.getValue();
593                        Throwable t2 = p2.getThrown();
594                        if (t2 != null)
595                           onBeanGetterException(pMeta, t);
596                        out.attr(ns, key2, value2);
597                     }
598                  } else /* Map */ {
599                     Map m2 = (Map)value;
600                     if (m2 != null)
601                        for (Map.Entry e : (Set<Map.Entry>)(m2.entrySet()))
602                           out.attr(ns, toString(e.getKey()), e.getValue());
603                  }
604               } else {
605                  out.attr(ns, key, value);
606               }
607            }
608         }
609      }
610
611      boolean
612         hasContent = false,
613         preserveWhitespace = false,
614         isVoidElement = xbm.getContentFormat() == VOID;
615
616      for (BeanPropertyValue p : lp) {
617         BeanPropertyMeta pMeta = p.getMeta();
618         if (pMeta.canRead()) {
619            ClassMeta<?> cMeta = p.getClassMeta();
620
621            String n = p.getName();
622            if (n.equals(contentProperty)) {
623               content = p.getValue();
624               contentType = p.getClassMeta();
625               hasContent = true;
626               cf = xbm.getContentFormat();
627               if (cf.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT))
628                  isMixedOrText = true;
629               if (cf.isOneOf(MIXED_PWS, TEXT_PWS))
630                  preserveWhitespace = true;
631               if (contentType.isCollection() && ((Collection)content).isEmpty())
632                  hasContent = false;
633               else if (contentType.isArray() && Array.getLength(content) == 0)
634                  hasContent = false;
635            } else if (elements.contains(n) || collapsedElements.contains(n) || elements.contains("*") || collapsedElements.contains("*") ) {
636               String key = p.getName();
637               Object value = p.getValue();
638               Throwable t = p.getThrown();
639               if (t != null)
640                  onBeanGetterException(pMeta, t);
641
642               if (canIgnoreValue(cMeta, key, value))
643                  continue;
644
645               if (! hasChildren) {
646                  hasChildren = true;
647                  out.appendIf(! isCollapsed, '>').nlIf(! isMixedOrText, indent);
648               }
649
650               XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta);
651               serializeAnything(out, value, cMeta, key, null, bpXml.getNamespace(), false, bpXml.getXmlFormat(), isMixedOrText, false, pMeta);
652            }
653         }
654      }
655      if (contentProperty == null && ! hasContent)
656         return (hasChildren ? CR_ELEMENTS : isVoidElement ? CR_VOID : CR_EMPTY);
657
658      // Serialize XML content.
659      if (content != null) {
660         out.append('>').nlIf(! isMixedOrText, indent);
661         if (contentType == null) {
662         } else if (contentType.isCollection()) {
663            Collection c = (Collection)content;
664            for (Iterator i = c.iterator(); i.hasNext();) {
665               Object value = i.next();
666               serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
667            }
668         } else if (contentType.isArray()) {
669            Collection c = toList(Object[].class, content);
670            for (Iterator i = c.iterator(); i.hasNext();) {
671               Object value = i.next();
672               serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
673            }
674         } else {
675            serializeAnything(out, content, contentType, null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
676         }
677      } else {
678         out.attr("nil", "true").append('>').nlIf(! isMixedOrText, indent);
679      }
680      return isMixedOrText ? CR_MIXED : CR_ELEMENTS;
681   }
682
683   private XmlWriter serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType,
684         ClassMeta<?> eType, BeanPropertyMeta ppMeta, boolean isMixed) throws IOException, SerializeException {
685
686      ClassMeta<?> eeType = eType.getElementType();
687
688      Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in));
689
690      c = sort(c);
691
692      String type2 = null;
693
694      String eName = type2;
695      Namespace eNs = null;
696
697      if (ppMeta != null) {
698         XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(ppMeta);
699         eName = bpXml.getChildName();
700         eNs = bpXml.getNamespace();
701      }
702
703      for (Iterator i = c.iterator(); i.hasNext();) {
704         Object value = i.next();
705         serializeAnything(out, value, eeType, null, eName, eNs, false, XmlFormat.DEFAULT, isMixed, false, null);
706      }
707      return out;
708   }
709
710   static enum JsonType {
711      STRING("string"),BOOLEAN("boolean"),NUMBER("number"),ARRAY("array"),OBJECT("object"),NULL("null");
712
713      private final String value;
714      private JsonType(String value) {
715         this.value = value;
716      }
717
718      @Override
719      public String toString() {
720         return value;
721      }
722
723      boolean isOneOf(JsonType...types) {
724         for (JsonType type : types)
725            if (type == this)
726               return true;
727         return false;
728      }
729   }
730
731   /**
732    * Identifies what the contents were of a serialized bean.
733    */
734   @SuppressWarnings("javadoc")
735   public static enum ContentResult {
736      CR_VOID,      // No content...append "/>" to the start tag.
737      CR_EMPTY,     // No content...append "/>" to the start tag if XML, "/></end>" if HTML.
738      CR_MIXED,     // Mixed content...don't add whitespace.
739      CR_ELEMENTS   // Elements...use normal whitespace rules.
740   }
741
742   //-----------------------------------------------------------------------------------------------------------------
743   // Properties
744   //-----------------------------------------------------------------------------------------------------------------
745
746   /**
747    * Configuration property:  Add <js>"_type"</js> properties when needed.
748    *
749    * @see XmlSerializer#XML_addBeanTypes
750    * @return
751    *    <jk>true</jk> if<js>"_type"</js> properties will be added to beans if their type cannot be inferred
752    *    through reflection.
753    */
754   @Override
755   protected boolean isAddBeanTypes() {
756      return ctx.isAddBeanTypes();
757   }
758
759   /**
760    * Configuration property:  Add namespace URLs to the root element.
761    *
762    * @see XmlSerializer#XML_addNamespaceUrisToRoot
763    * @return
764    *    <jk>true</jk> if {@code xmlns:x} attributes are added to the root element for the default and all mapped namespaces.
765    */
766   protected final boolean isAddNamespaceUrisToRoot() {
767      return ctx.isAddNamespaceUrlsToRoot();
768   }
769
770   /**
771    * Configuration property:  Auto-detect namespace usage.
772    *
773    * @see XmlSerializer#XML_autoDetectNamespaces
774    * @return
775    *    <jk>true</jk> if namespace usage is detected before serialization.
776    */
777   protected final boolean isAutoDetectNamespaces() {
778      return ctx.isAutoDetectNamespaces();
779   }
780
781   /**
782    * Configuration property:  Default namespace.
783    *
784    * @see XmlSerializer#XML_defaultNamespace
785    * @return
786    *    The default namespace URI for this document.
787    */
788   protected final Namespace getDefaultNamespace() {
789      return defaultNamespace;
790   }
791
792   /**
793    * Configuration property:  Enable support for XML namespaces.
794    *
795    * @see XmlSerializer#XML_enableNamespaces
796    * @return
797    *    <jk>false</jk> if XML output will not contain any namespaces regardless of any other settings.
798    */
799   protected final boolean isEnableNamespaces() {
800      return ctx.isEnableNamespaces();
801   }
802
803   /**
804    * Configuration property:  Default namespaces.
805    *
806    * @see XmlSerializer#XML_namespaces
807    * @return
808    *    The default list of namespaces associated with this serializer.
809    */
810   protected final Namespace[] getNamespaces() {
811      return namespaces;
812   }
813
814   /**
815    * Configuration property:  XMLSchema namespace.
816    *
817    * @see XmlSerializer#XML_xsNamespace
818    * @return
819    *    The namespace for the <c>XMLSchema</c> namespace, used by the schema generated by the
820    *    {@link org.apache.juneau.xmlschema.XmlSchemaSerializer} class.
821    */
822   @Deprecated
823   protected final Namespace getXsNamespace() {
824      return ctx.getXsNamespace();
825   }
826
827   //-----------------------------------------------------------------------------------------------------------------
828   // Extended metadata
829   //-----------------------------------------------------------------------------------------------------------------
830
831   /**
832    * Returns the language-specific metadata on the specified class.
833    *
834    * @param cm The class to return the metadata on.
835    * @return The metadata.
836    */
837   public XmlClassMeta getXmlClassMeta(ClassMeta<?> cm) {
838      return ctx.getXmlClassMeta(cm);
839   }
840
841   /**
842    * Returns the language-specific metadata on the specified bean.
843    *
844    * @param bm The bean to return the metadata on.
845    * @return The metadata.
846    */
847   public XmlBeanMeta getXmlBeanMeta(BeanMeta<?> bm) {
848      return ctx.getXmlBeanMeta(bm);
849   }
850
851   /**
852    * Returns the language-specific metadata on the specified bean property.
853    *
854    * @param bpm The bean property to return the metadata on.
855    * @return The metadata.
856    */
857   public XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) {
858      return bpm == null ? XmlBeanPropertyMeta.DEFAULT : ctx.getXmlBeanPropertyMeta(bpm);
859   }
860
861   //-----------------------------------------------------------------------------------------------------------------
862   // Other methods
863   //-----------------------------------------------------------------------------------------------------------------
864
865   @Override /* Session */
866   public OMap toMap() {
867      return super.toMap()
868         .a("XmlSerializerSession", new DefaultFilteringOMap()
869         );
870   }
871}