001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.xml;
018
019import static org.apache.juneau.common.utils.IOUtils.*;
020import static org.apache.juneau.internal.ArrayUtils.*;
021import static org.apache.juneau.xml.XmlSerializerSession.ContentResult.*;
022import static org.apache.juneau.xml.XmlSerializerSession.JsonType.*;
023import static org.apache.juneau.xml.annotation.XmlFormat.*;
024
025import java.io.*;
026import java.lang.reflect.*;
027import java.nio.charset.*;
028import java.util.*;
029import java.util.function.*;
030
031import org.apache.juneau.*;
032import org.apache.juneau.common.utils.*;
033import org.apache.juneau.httppart.*;
034import org.apache.juneau.internal.*;
035import org.apache.juneau.serializer.*;
036import org.apache.juneau.svl.*;
037import org.apache.juneau.swap.*;
038import org.apache.juneau.xml.annotation.*;
039
040/**
041 * Session object that lives for the duration of a single use of {@link XmlSerializer}.
042 *
043 * <h5 class='section'>Notes:</h5><ul>
044 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
045 * </ul>
046 *
047 * <h5 class='section'>See Also:</h5><ul>
048 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/XmlBasics">XML Basics</a>
049
050 * </ul>
051 */
052@SuppressWarnings({"unchecked","rawtypes"})
053public class XmlSerializerSession extends WriterSerializerSession {
054
055   //-----------------------------------------------------------------------------------------------------------------
056   // Static
057   //-----------------------------------------------------------------------------------------------------------------
058
059   /**
060    * Creates a new builder for this object.
061    *
062    * @param ctx The context creating this session.
063    * @return A new builder.
064    */
065   public static Builder create(XmlSerializer ctx) {
066      return new Builder(ctx);
067   }
068
069   //-----------------------------------------------------------------------------------------------------------------
070   // Builder
071   //-----------------------------------------------------------------------------------------------------------------
072
073   /**
074    * Builder class.
075    */
076   public static class Builder extends WriterSerializerSession.Builder {
077
078      XmlSerializer ctx;
079
080      /**
081       * Constructor
082       *
083       * @param ctx The context creating this session.
084       */
085      protected Builder(XmlSerializer ctx) {
086         super(ctx);
087         this.ctx = ctx;
088      }
089
090      @Override
091      public XmlSerializerSession build() {
092         return new XmlSerializerSession(this);
093      }
094      @Override /* Overridden from Builder */
095      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
096         super.apply(type, apply);
097         return this;
098      }
099
100      @Override /* Overridden from Builder */
101      public Builder debug(Boolean value) {
102         super.debug(value);
103         return this;
104      }
105
106      @Override /* Overridden from Builder */
107      public Builder properties(Map<String,Object> value) {
108         super.properties(value);
109         return this;
110      }
111
112      @Override /* Overridden from Builder */
113      public Builder property(String key, Object value) {
114         super.property(key, value);
115         return this;
116      }
117
118      @Override /* Overridden from Builder */
119      public Builder unmodifiable() {
120         super.unmodifiable();
121         return this;
122      }
123
124      @Override /* Overridden from Builder */
125      public Builder locale(Locale value) {
126         super.locale(value);
127         return this;
128      }
129
130      @Override /* Overridden from Builder */
131      public Builder localeDefault(Locale value) {
132         super.localeDefault(value);
133         return this;
134      }
135
136      @Override /* Overridden from Builder */
137      public Builder mediaType(MediaType value) {
138         super.mediaType(value);
139         return this;
140      }
141
142      @Override /* Overridden from Builder */
143      public Builder mediaTypeDefault(MediaType value) {
144         super.mediaTypeDefault(value);
145         return this;
146      }
147
148      @Override /* Overridden from Builder */
149      public Builder timeZone(TimeZone value) {
150         super.timeZone(value);
151         return this;
152      }
153
154      @Override /* Overridden from Builder */
155      public Builder timeZoneDefault(TimeZone value) {
156         super.timeZoneDefault(value);
157         return this;
158      }
159
160      @Override /* Overridden from Builder */
161      public Builder javaMethod(Method value) {
162         super.javaMethod(value);
163         return this;
164      }
165
166      @Override /* Overridden from Builder */
167      public Builder resolver(VarResolverSession value) {
168         super.resolver(value);
169         return this;
170      }
171
172      @Override /* Overridden from Builder */
173      public Builder schema(HttpPartSchema value) {
174         super.schema(value);
175         return this;
176      }
177
178      @Override /* Overridden from Builder */
179      public Builder schemaDefault(HttpPartSchema value) {
180         super.schemaDefault(value);
181         return this;
182      }
183
184      @Override /* Overridden from Builder */
185      public Builder uriContext(UriContext value) {
186         super.uriContext(value);
187         return this;
188      }
189
190      @Override /* Overridden from Builder */
191      public Builder fileCharset(Charset value) {
192         super.fileCharset(value);
193         return this;
194      }
195
196      @Override /* Overridden from Builder */
197      public Builder streamCharset(Charset value) {
198         super.streamCharset(value);
199         return this;
200      }
201
202      @Override /* Overridden from Builder */
203      public Builder useWhitespace(Boolean value) {
204         super.useWhitespace(value);
205         return this;
206      }
207   }
208
209   //-----------------------------------------------------------------------------------------------------------------
210   // Instance
211   //-----------------------------------------------------------------------------------------------------------------
212
213   private final XmlSerializer ctx;
214   private Namespace
215      defaultNamespace;
216   private Namespace[] namespaces = {};
217   private final String textNodeDelimiter;
218
219   /**
220    * Constructor.
221    *
222    * @param builder The builder for this object.
223    */
224   protected XmlSerializerSession(Builder builder) {
225      super(builder);
226      ctx = builder.ctx;
227      namespaces = ctx.getNamespaces();
228      defaultNamespace = findDefaultNamespace(ctx.getDefaultNamespace());
229      textNodeDelimiter = ctx.textNodeDelimiter;
230   }
231
232   private Namespace findDefaultNamespace(Namespace n) {
233      if (n == null)
234         return null;
235      if (n.name != null && n.uri != null)
236         return n;
237      if (n.uri == null) {
238         for (Namespace n2 : getNamespaces())
239            if (n2.name.equals(n.name))
240               return n2;
241      }
242      if (n.name == null) {
243         for (Namespace n2 : getNamespaces())
244            if (n2.uri.equals(n.uri))
245               return n2;
246      }
247      return n;
248   }
249
250   /*
251    * Add a namespace to this session.
252    *
253    * @param ns The namespace being added.
254    */
255   private void addNamespace(Namespace ns) {
256      if (ns == defaultNamespace)
257         return;
258
259      for (Namespace n : namespaces)
260         if (n == ns)
261            return;
262
263      if (defaultNamespace != null && (ns.uri.equals(defaultNamespace.uri) || ns.name.equals(defaultNamespace.name)))
264         defaultNamespace = ns;
265      else
266         namespaces = append(namespaces, ns);
267   }
268
269   /**
270    * Returns <jk>true</jk> if we're serializing HTML.
271    *
272    * <p>
273    * The difference in behavior is how empty non-void elements are handled.
274    * The XML serializer will produce a collapsed tag, whereas the HTML serializer will produce a start and end tag.
275    *
276    * @return <jk>true</jk> if we're generating HTML.
277    */
278   protected boolean isHtmlMode() {
279      return false;
280   }
281
282   /**
283    * Converts the specified output target object to an {@link XmlWriter}.
284    *
285    * @param out The output target object.
286    * @return The output target object wrapped in an {@link XmlWriter}.
287    * @throws IOException Thrown by underlying stream.
288    */
289   public final XmlWriter getXmlWriter(SerializerPipe out) throws IOException {
290      Object output = out.getRawOutput();
291      if (output instanceof XmlWriter)
292         return (XmlWriter)output;
293      XmlWriter w = new XmlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), getUriResolver(), isEnableNamespaces(), defaultNamespace);
294      out.setWriter(w);
295      return w;
296   }
297
298   @Override /* Serializer */
299   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
300      if (isEnableNamespaces() && isAutoDetectNamespaces())
301         findNsfMappings(o);
302      serializeAnything(getXmlWriter(out), o, getExpectedRootType(o), null, null, null, isEnableNamespaces() && isAddNamespaceUrisToRoot(), XmlFormat.DEFAULT, false, false, null);
303   }
304
305   /**
306    * Recursively searches for the XML namespaces on the specified POJO and adds them to the serializer context object.
307    *
308    * @param o The POJO to check.
309    * @throws SerializeException Thrown if bean recursion occurred.
310    */
311   protected final void findNsfMappings(Object o) throws SerializeException {
312      ClassMeta<?> aType = null;                // The actual type
313
314      try {
315         aType = push(null, o, null);
316      } catch (BeanRecursionException e) {
317         throw new SerializeException(e);
318      }
319
320      if (aType != null) {
321         Namespace ns = getXmlClassMeta(aType).getNamespace();
322         if (ns != null) {
323            if (ns.uri != null)
324               addNamespace(ns);
325            else
326               ns = null;
327         }
328      }
329
330      // Handle recursion
331      if (aType != null && ! aType.isPrimitive()) {
332
333         BeanMap<?> bm = null;
334         if (aType.isBeanMap()) {
335            bm = (BeanMap<?>)o;
336         } else if (aType.isBean()) {
337            bm = toBeanMap(o);
338         } else if (aType.isDelegate()) {
339            ClassMeta<?> innerType = ((Delegate<?>)o).getClassMeta();
340            Value<Namespace >ns = Value.of(getXmlClassMeta(innerType).getNamespace());
341            if (ns.isPresent()) {
342               if (ns.get().uri != null)
343                  addNamespace(ns.get());
344               else
345                  ns.getAndUnset();
346            }
347
348            if (innerType.isBean()) {
349               innerType.getBeanMeta().forEachProperty(BeanPropertyMeta::canRead, x -> {
350                  ns.set(getXmlBeanPropertyMeta(x).getNamespace());
351                  if (ns.isPresent() && ns.get().uri != null)
352                     addNamespace(ns.get());
353               });
354            } else if (innerType.isMap()) {
355               ((Map<?,?>)o).forEach((k,v) -> findNsfMappings(v));
356            } else if (innerType.isCollection()) {
357               ((Collection<?>)o).forEach(this::findNsfMappings);
358            }
359
360         } else if (aType.isMap()) {
361            ((Map<?,?>)o).forEach((k,v) -> findNsfMappings(v));
362         } else if (aType.isCollection()) {
363            ((Collection<?>)o).forEach(this::findNsfMappings);
364         } else if (aType.isArray() && ! aType.getElementType().isPrimitive()) {
365            for (Object o2 : ((Object[])o))
366               findNsfMappings(o2);
367         }
368         if (bm != null) {
369            Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null;
370            bm.forEachValue(checkNull, (pMeta,key,value,thrown) -> {
371               Namespace ns = getXmlBeanPropertyMeta(pMeta).getNamespace();
372               if (ns != null && ns.uri != null)
373                  addNamespace(ns);
374
375               try {
376                  findNsfMappings(value);
377               } catch (Throwable x) {
378                  // Ignore
379               }
380            });
381         }
382      }
383
384      pop();
385   }
386
387   /**
388    * Workhorse method.
389    *
390    * @param out The writer to send the output to.
391    * @param o The object to serialize.
392    * @param eType The expected type if this is a bean property value being serialized.
393    * @param keyName The property name or map key name.
394    * @param elementName The root element name.
395    * @param elementNamespace The namespace of the element.
396    * @param addNamespaceUris Flag indicating that namespace URIs need to be added.
397    * @param format The format to serialize the output to.
398    * @param isMixedOrText We're serializing mixed content, so don't use whitespace.
399    * @param preserveWhitespace
400    *    <jk>true</jk> if we're serializing {@link XmlFormat#MIXED_PWS} or {@link XmlFormat#TEXT_PWS}.
401    * @param pMeta The bean property metadata if this is a bean property being serialized.
402    * @return The same writer passed in so that calls to the writer can be chained.
403    * @throws SerializeException General serialization error occurred.
404    */
405   protected ContentResult serializeAnything(
406         XmlWriter out,
407         Object o,
408         ClassMeta<?> eType,
409         String keyName,
410         String elementName,
411         Namespace elementNamespace,
412         boolean addNamespaceUris,
413         XmlFormat format,
414         boolean isMixedOrText,
415         boolean preserveWhitespace,
416         BeanPropertyMeta pMeta) throws SerializeException {
417
418      JsonType type = null;              // The type string (e.g. <type> or <x x='type'>
419      int i = isMixedOrText ? 0 : indent;       // Current indentation
420      ClassMeta<?> aType = null;     // The actual type
421      ClassMeta<?> wType = null;     // The wrapped type (delegate)
422      ClassMeta<?> sType = object(); // The serialized type
423
424      aType = push2(keyName, o, eType);
425
426      if (eType == null)
427         eType = object();
428
429      // Handle recursion
430      if (aType == null) {
431         o = null;
432         aType = object();
433      }
434
435      // Handle Optional<X>
436      if (isOptional(aType)) {
437         o = getOptionalValue(o);
438         eType = getOptionalType(eType);
439         aType = getClassMetaForObject(o, object());
440      }
441
442      if (o != null) {
443
444         if (aType.isDelegate()) {
445            wType = aType;
446            eType = aType = ((Delegate<?>)o).getClassMeta();
447         }
448
449         sType = aType;
450
451         // Swap if necessary
452         ObjectSwap swap = aType.getSwap(this);
453         if (swap != null) {
454            o = swap(swap, o);
455            sType = swap.getSwapClassMeta(this);
456
457            // If the getSwapClass() method returns Object, we need to figure out
458            // the actual type now.
459            if (sType.isObject())
460               sType = getClassMetaForObject(o);
461         }
462      } else {
463         sType = eType.getSerializedClassMeta(this);
464      }
465
466      // Does the actual type match the expected type?
467      boolean isExpectedType = true;
468      if (o == null || ! eType.same(aType)) {
469         if (eType.isNumber())
470            isExpectedType = aType.isNumber();
471         else if (eType.isMap())
472            isExpectedType = aType.isMap();
473         else if (eType.isCollectionOrArray())
474            isExpectedType = aType.isCollectionOrArray();
475         else
476            isExpectedType = false;
477      }
478
479      String resolvedDictionaryName = isExpectedType ? null : aType.getDictionaryName();
480
481      // Note that the dictionary name may be specified on the actual type or the serialized type.
482      // HTML templates will have them defined on the serialized type.
483      String dictionaryName = aType.getDictionaryName();
484      if (dictionaryName == null)
485         dictionaryName = sType.getDictionaryName();
486
487      // char '\0' is interpreted as null.
488      if (o != null && sType.isChar() && ((Character)o).charValue() == 0)
489         o = null;
490
491      boolean isCollapsed = false;     // If 'true', this is a collection and we're not rendering the outer element.
492      boolean isRaw = (sType.isReader() || sType.isInputStream()) && o != null;
493
494      // Get the JSON type string.
495      if (o == null) {
496         type = NULL;
497      } else if (sType.isCharSequence() || sType.isChar()) {
498         type = STRING;
499      } else if (sType.isNumber()) {
500         type = NUMBER;
501      } else if (sType.isBoolean()) {
502         type = BOOLEAN;
503      } else if (sType.isMapOrBean()) {
504         isCollapsed = getXmlClassMeta(sType).getFormat() == COLLAPSED;
505         type = OBJECT;
506      } else if (sType.isCollectionOrArray()) {
507         isCollapsed = (format == COLLAPSED && ! addNamespaceUris);
508         type = ARRAY;
509      } else {
510         type = STRING;
511      }
512
513      if (format.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT) && type.isOneOf(NULL,STRING,NUMBER,BOOLEAN))
514         isCollapsed = true;
515
516      // Is there a name associated with this bean?
517
518      String name = keyName;
519      if (elementName == null && dictionaryName != null) {
520         elementName = dictionaryName;
521         isExpectedType = o != null;  // preserve type='null' when it's null.
522      }
523
524      if (elementName == null) {
525         elementName = name;
526         name = null;
527      }
528
529      if (Utils.eq(name, elementName))
530         name = null;
531
532      if (isEnableNamespaces()) {
533         if (elementNamespace == null)
534            elementNamespace = getXmlClassMeta(sType).getNamespace();
535         if (elementNamespace == null)
536            elementNamespace = getXmlClassMeta(aType).getNamespace();
537         if (elementNamespace != null && elementNamespace.uri == null)
538            elementNamespace = null;
539         if (elementNamespace == null)
540            elementNamespace = defaultNamespace;
541      } else {
542         elementNamespace = null;
543      }
544
545      // Do we need a carriage return after the start tag?
546      boolean cr = o != null && (sType.isMapOrBean() || sType.isCollectionOrArray()) && ! isMixedOrText;
547
548      String en = elementName;
549      if (en == null && ! isRaw) {
550         if (isAddJsonTags()) {
551            en = type.toString();
552            type = null;
553         }
554      }
555
556      boolean encodeEn = elementName != null;
557      String ns = (elementNamespace == null ? null : elementNamespace.name);
558      String dns = null, elementNs = null;
559      if (isEnableNamespaces()) {
560         dns = elementName == null && defaultNamespace != null ? defaultNamespace.name : null;
561         elementNs = elementName == null ? dns : ns;
562         if (elementName == null)
563            elementNamespace = null;
564      }
565
566      // Render the start tag.
567      if (! isCollapsed) {
568         if (en != null) {
569            out.oTag(i, elementNs, en, encodeEn);
570            if (addNamespaceUris) {
571               out.attr((String)null, "xmlns", defaultNamespace.getUri());
572
573               for (Namespace n : namespaces)
574                  out.attr("xmlns", n.getName(), n.getUri());
575            }
576            if (! isExpectedType) {
577               if (resolvedDictionaryName != null)
578                  out.attr(dns, getBeanTypePropertyName(eType), resolvedDictionaryName);
579               else if (type != null && type != STRING)
580                  out.attr(dns, getBeanTypePropertyName(eType), type);
581            }
582            if (name != null)
583               out.attr(getNamePropertyName(), name);
584         } else {
585            out.i(i);
586         }
587         if (o == null) {
588            if ((sType.isBoolean() || sType.isNumber()) && ! sType.isNullable())
589               o = sType.getPrimitiveDefault();
590         }
591
592         if (o != null && ! (sType.isMapOrBean() || en == null))
593            out.w('>');
594
595         if (cr && ! (sType.isMapOrBean()))
596            out.nl(i+1);
597      }
598
599      ContentResult rc = CR_ELEMENTS;
600
601      // Render the tag contents.
602      if (o != null) {
603         if (sType.isUri() || (pMeta != null && pMeta.isUri())) {
604            out.textUri(o);
605         } else if (sType.isCharSequence() || sType.isChar()) {
606            if (isXmlText(format, sType))
607               out.append(o);
608            else
609               out.text(o, preserveWhitespace);
610         } else if (sType.isNumber() || sType.isBoolean()) {
611            out.append(o);
612         } else if (sType.isMap() || (wType != null && wType.isMap())) {
613            if (o instanceof BeanMap)
614               rc = serializeBeanMap(out, (BeanMap)o, elementNamespace, isCollapsed, isMixedOrText);
615            else
616               rc = serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), isMixedOrText);
617         } else if (sType.isBean()) {
618            rc = serializeBeanMap(out, toBeanMap(o), elementNamespace, isCollapsed, isMixedOrText);
619         } else if (sType.isCollection() || (wType != null && wType.isCollection())) {
620            if (isCollapsed)
621               this.indent--;
622            serializeCollection(out, o, sType, eType, pMeta, isMixedOrText);
623            if (isCollapsed)
624               this.indent++;
625         } else if (sType.isArray()) {
626            if (isCollapsed)
627               this.indent--;
628            serializeCollection(out, o, sType, eType, pMeta, isMixedOrText);
629            if (isCollapsed)
630               this.indent++;
631         } else if (sType.isReader()) {
632            pipe((Reader)o, out, SerializerSession::handleThrown);
633         } else if (sType.isInputStream()) {
634            pipe((InputStream)o, out, SerializerSession::handleThrown);
635         } else {
636            if (isXmlText(format, sType))
637               out.append(toString(o));
638            else
639               out.text(toString(o));
640         }
641      }
642
643      pop();
644
645      // Render the end tag.
646      if (! isCollapsed) {
647         if (en != null) {
648            if (rc == CR_EMPTY) {
649               if (isHtmlMode())
650                  out.w('>').eTag(elementNs, en, encodeEn);
651               else
652                  out.w('/').w('>');
653            } else if (rc == CR_VOID || o == null) {
654               out.w('/').w('>');
655            }
656            else
657               out.ie(cr && rc != CR_MIXED ? i : 0).eTag(elementNs, en, encodeEn);
658         }
659         if (! isMixedOrText)
660            out.nl(i);
661      }
662
663      return rc;
664   }
665
666   private boolean isXmlText(XmlFormat format, ClassMeta<?> sType) {
667      if (format == XMLTEXT)
668         return true;
669      XmlClassMeta xcm = getXmlClassMeta(sType);
670      if (xcm == null)
671         return false;
672      return xcm.getFormat() == XMLTEXT;
673   }
674
675   private ContentResult serializeMap(XmlWriter out, Map m, ClassMeta<?> sType,
676         ClassMeta<?> eKeyType, ClassMeta<?> eValueType, boolean isMixed) throws SerializeException {
677
678      ClassMeta<?> keyType = eKeyType == null ? sType.getKeyType() : eKeyType;
679      ClassMeta<?> valueType = eValueType == null ? sType.getValueType() : eValueType;
680
681      Flag hasChildren = Flag.create();
682      forEachEntry(m, e -> {
683
684         Object k = e.getKey();
685         if (k == null) {
686            k = "\u0000";
687         } else {
688            k = generalize(k, keyType);
689            if (isTrimStrings() && k instanceof String)
690               k = k.toString().trim();
691         }
692
693         Object value = e.getValue();
694
695         hasChildren.ifNotSet(()->out.w('>').nlIf(! isMixed, indent)).set();
696         serializeAnything(out, value, valueType, toString(k), null, null, false, XmlFormat.DEFAULT, isMixed, false, null);
697      });
698
699      return hasChildren.isSet() ? CR_ELEMENTS : CR_EMPTY;
700   }
701
702   private ContentResult serializeBeanMap(XmlWriter out, BeanMap<?> m,
703         Namespace elementNs, boolean isCollapsed, boolean isMixedOrText) throws SerializeException {
704      boolean hasChildren = false;
705      BeanMeta<?> bm = m.getMeta();
706
707      List<BeanPropertyValue> lp = new ArrayList<>();
708
709      Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null;
710      m.forEachValue(checkNull, (pMeta,key,value,thrown) -> {
711         lp.add(new BeanPropertyValue(pMeta, key, value, thrown));
712      });
713
714      XmlBeanMeta xbm = getXmlBeanMeta(bm);
715
716      Set<String>
717         attrs = xbm.getAttrPropertyNames(),
718         elements = xbm.getElementPropertyNames(),
719         collapsedElements = xbm.getCollapsedPropertyNames();
720      String
721         attrsProperty = xbm.getAttrsPropertyName(),
722         contentProperty = xbm.getContentPropertyName();
723
724      XmlFormat cf = null;
725
726      Object content = null;
727      ClassMeta<?> contentType = null;
728      for (BeanPropertyValue p : lp) {
729         String n = p.getName();
730         if (attrs.contains(n) || attrs.contains("*") || n.equals(attrsProperty)) {
731            BeanPropertyMeta pMeta = p.getMeta();
732            if (pMeta.canRead()) {
733               ClassMeta<?> cMeta = p.getClassMeta();
734
735               String key = p.getName();
736               Object value = p.getValue();
737               Throwable t = p.getThrown();
738               if (t != null)
739                  onBeanGetterException(pMeta, t);
740
741               if (canIgnoreValue(cMeta, key, value))
742                  continue;
743
744               XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta);
745               Namespace ns = (isEnableNamespaces() && bpXml.getNamespace() != elementNs ? bpXml.getNamespace() : null);
746
747               if (pMeta.isUri()  ) {
748                  out.attrUri(ns, key, value);
749               } else if (n.equals(attrsProperty)) {
750                  if (value instanceof BeanMap) {
751                     BeanMap<?> bm2 = (BeanMap)value;
752                     bm2.forEachValue(x -> true, (pMeta2,key2,value2,thrown2) -> {
753                        if (thrown2 != null)
754                           onBeanGetterException(pMeta, thrown2);
755                        out.attr(ns, key2, value2);
756                     });
757                  } else /* Map */ {
758                     Map m2 = (Map)value;
759                     if (m2 != null)
760                        m2.forEach((k,v) -> out.attr(ns, Utils.s(k), v));
761                  }
762               } else {
763                  out.attr(ns, key, value);
764               }
765            }
766         }
767      }
768
769      boolean
770         hasContent = false,
771         preserveWhitespace = false,
772         isVoidElement = xbm.getContentFormat() == VOID;
773
774      for (BeanPropertyValue p : lp) {
775         BeanPropertyMeta pMeta = p.getMeta();
776         if (pMeta.canRead()) {
777            ClassMeta<?> cMeta = p.getClassMeta();
778
779            String n = p.getName();
780            if (n.equals(contentProperty)) {
781               content = p.getValue();
782               contentType = p.getClassMeta();
783               hasContent = true;
784               cf = xbm.getContentFormat();
785               if (cf.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT))
786                  isMixedOrText = true;
787               if (cf.isOneOf(MIXED_PWS, TEXT_PWS))
788                  preserveWhitespace = true;
789               if (contentType.isCollection() && ((Collection)content).isEmpty())
790                  hasContent = false;
791               else if (contentType.isArray() && Array.getLength(content) == 0)
792                  hasContent = false;
793            } else if (elements.contains(n) || collapsedElements.contains(n) || elements.contains("*") || collapsedElements.contains("*") ) {
794               String key = p.getName();
795               Object value = p.getValue();
796               Throwable t = p.getThrown();
797               if (t != null)
798                  onBeanGetterException(pMeta, t);
799
800               if (canIgnoreValue(cMeta, key, value))
801                  continue;
802
803               if (! hasChildren) {
804                  hasChildren = true;
805                  out.appendIf(! isCollapsed, '>').nlIf(! isMixedOrText, indent);
806               }
807
808               XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta);
809               serializeAnything(out, value, cMeta, key, null, bpXml.getNamespace(), false, bpXml.getXmlFormat(), isMixedOrText, false, pMeta);
810            }
811         }
812      }
813      if (contentProperty == null && ! hasContent)
814         return (hasChildren ? CR_ELEMENTS : isVoidElement ? CR_VOID : CR_EMPTY);
815
816      // Serialize XML content.
817      if (content != null) {
818         out.w('>').nlIf(! isMixedOrText, indent);
819         if (contentType == null) {
820         } else if (contentType.isCollection()) {
821            Collection c = (Collection)content;
822            boolean previousWasTextNode = false;
823            for (Object value : c) {
824               boolean currentIsTextNode = isTextNode(value);
825               // Insert delimiter between consecutive text nodes
826               if (previousWasTextNode && currentIsTextNode && textNodeDelimiter != null && !textNodeDelimiter.isEmpty()) {
827                  out.append(textNodeDelimiter);
828               }
829               serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
830               previousWasTextNode = currentIsTextNode;
831            }
832         } else if (contentType.isArray()) {
833            Collection c = toList(Object[].class, content);
834            boolean previousWasTextNode = false;
835            for (Object value : c) {
836               boolean currentIsTextNode = isTextNode(value);
837               // Insert delimiter between consecutive text nodes
838               if (previousWasTextNode && currentIsTextNode && textNodeDelimiter != null && !textNodeDelimiter.isEmpty()) {
839                  out.append(textNodeDelimiter);
840               }
841               serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
842               previousWasTextNode = currentIsTextNode;
843            }
844      } else {
845         serializeAnything(out, content, contentType, null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
846      }
847   } else {
848      if (isAddJsonTags())
849         out.attr("nil", "true");
850      out.w('>').nlIf(! isMixedOrText, indent);
851   }
852   return isMixedOrText ? CR_MIXED : CR_ELEMENTS;
853   }
854
855   private XmlWriter serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType,
856         ClassMeta<?> eType, BeanPropertyMeta ppMeta, boolean isMixed) throws SerializeException {
857
858      ClassMeta<?> eeType = eType.getElementType();
859
860      Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in));
861
862      String type2 = null;
863
864      Value<String> eName = Value.of(type2);
865      Value<Namespace> eNs = Value.empty();
866
867      if (ppMeta != null) {
868         XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(ppMeta);
869         eName.set(bpXml.getChildName());
870         eNs.set(bpXml.getNamespace());
871      }
872
873      // Track if previous element was a text node for delimiter insertion
874      Value<Boolean> previousWasTextNode = Value.of(false);
875
876      forEachEntry(c, x -> {
877         boolean currentIsTextNode = isTextNode(x);
878
879         // Insert delimiter between consecutive text nodes
880         if (previousWasTextNode.get() && currentIsTextNode && textNodeDelimiter != null && !textNodeDelimiter.isEmpty()) {
881            out.append(textNodeDelimiter);
882         }
883
884         serializeAnything(out, x, eeType, null, eName.get(), eNs.get(), false, XmlFormat.DEFAULT, isMixed, false, null);
885         previousWasTextNode.set(currentIsTextNode);
886      });
887
888      return out;
889   }
890   
891   /**
892    * Checks if an object is a text node (String or primitive type).
893    */
894   private boolean isTextNode(Object o) {
895      if (o == null)
896         return false;
897      Class<?> c = o.getClass();
898      // Text nodes are strings and primitives (not beans, collections, arrays, or other complex types)
899      return CharSequence.class.isAssignableFrom(c) || Number.class.isAssignableFrom(c) || Boolean.class.isAssignableFrom(c) || c.isPrimitive();
900   }
901
902   enum JsonType {
903      STRING("string"),BOOLEAN("boolean"),NUMBER("number"),ARRAY("array"),OBJECT("object"),NULL("null");
904
905      private final String value;
906      JsonType(String value) {
907         this.value = value;
908      }
909
910      @Override
911      public String toString() {
912         return value;
913      }
914
915      boolean isOneOf(JsonType...types) {
916         for (JsonType type : types)
917            if (type == this)
918               return true;
919         return false;
920      }
921   }
922
923   /**
924    * Identifies what the contents were of a serialized bean.
925    */
926   @SuppressWarnings("javadoc")
927   public enum ContentResult {
928      CR_VOID,      // No content...append "/>" to the start tag.
929      CR_EMPTY,     // No content...append "/>" to the start tag if XML, "/></end>" if HTML.
930      CR_MIXED,     // Mixed content...don't add whitespace.
931      CR_ELEMENTS   // Elements...use normal whitespace rules.
932   }
933
934   //-----------------------------------------------------------------------------------------------------------------
935   // Properties
936   //-----------------------------------------------------------------------------------------------------------------
937
938   /**
939    * Add <js>"_type"</js> properties when needed.
940    *
941    * @see XmlSerializer.Builder#addBeanTypesXml()
942    * @return
943    *    <jk>true</jk> if<js>"_type"</js> properties will be added to beans if their type cannot be inferred
944    *    through reflection.
945    */
946   @Override
947   protected boolean isAddBeanTypes() {
948      return ctx.isAddBeanTypes();
949   }
950
951   /**
952    * Add namespace URLs to the root element.
953    *
954    * @see XmlSerializer.Builder#addNamespaceUrisToRoot()
955    * @return
956    *    <jk>true</jk> if {@code xmlns:x} attributes are added to the root element for the default and all mapped namespaces.
957    */
958   protected final boolean isAddNamespaceUrisToRoot() {
959      return ctx.isAddNamespaceUrlsToRoot();
960   }
961
962   /**
963    * Auto-detect namespace usage.
964    *
965    * @see XmlSerializer.Builder#disableAutoDetectNamespaces()
966    * @return
967    *    <jk>true</jk> if namespace usage is detected before serialization.
968    */
969   protected final boolean isAutoDetectNamespaces() {
970      return ctx.isAutoDetectNamespaces();
971   }
972
973   /**
974    * Default namespace.
975    *
976    * @see XmlSerializer.Builder#defaultNamespace(Namespace)
977    * @return
978    *    The default namespace URI for this document.
979    */
980   protected final Namespace getDefaultNamespace() {
981      return defaultNamespace;
982   }
983
984   /**
985    * Enable support for XML namespaces.
986    *
987    * @see XmlSerializer.Builder#enableNamespaces()
988    * @return
989    *    <jk>false</jk> if XML output will not contain any namespaces regardless of any other settings.
990    */
991   protected final boolean isEnableNamespaces() {
992      return ctx.isEnableNamespaces();
993   }
994
995   /**
996    * Default namespaces.
997    *
998    * @see XmlSerializer.Builder#namespaces(Namespace...)
999    * @return
1000    *    The default list of namespaces associated with this serializer.
1001    */
1002   protected final Namespace[] getNamespaces() {
1003      return namespaces;
1004   }
1005
1006   //-----------------------------------------------------------------------------------------------------------------
1007   // Extended metadata
1008   //-----------------------------------------------------------------------------------------------------------------
1009
1010   /**
1011    * Returns the language-specific metadata on the specified class.
1012    *
1013    * @param cm The class to return the metadata on.
1014    * @return The metadata.
1015    */
1016   public XmlClassMeta getXmlClassMeta(ClassMeta<?> cm) {
1017      return ctx.getXmlClassMeta(cm);
1018   }
1019
1020   /**
1021    * Returns the language-specific metadata on the specified bean.
1022    *
1023    * @param bm The bean to return the metadata on.
1024    * @return The metadata.
1025    */
1026   public XmlBeanMeta getXmlBeanMeta(BeanMeta<?> bm) {
1027      return ctx.getXmlBeanMeta(bm);
1028   }
1029
1030   /**
1031    * Returns the language-specific metadata on the specified bean property.
1032    *
1033    * @param bpm The bean property to return the metadata on.
1034    * @return The metadata.
1035    */
1036   public XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) {
1037      return bpm == null ? XmlBeanPropertyMeta.DEFAULT : ctx.getXmlBeanPropertyMeta(bpm);
1038   }
1039
1040   //-----------------------------------------------------------------------------------------------------------------
1041   // Properties
1042   //-----------------------------------------------------------------------------------------------------------------
1043
1044   /**
1045    * Add JSON type tags.
1046    *
1047    * @see XmlSerializer.Builder#disableJsonTags()
1048    * @return
1049    *    <jk>true</jk> if plain strings will be wrapped in <js>&lt;string&gt;</js> tags when serialized as root elements.
1050    */
1051   protected final boolean isAddJsonTags() {
1052      return ctx.addJsonTags;
1053   }
1054}