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.common.internal.IOUtils.*;
016import static org.apache.juneau.common.internal.StringUtils.*;
017import static org.apache.juneau.internal.ArrayUtils.*;
018import static org.apache.juneau.xml.XmlSerializerSession.ContentResult.*;
019import static org.apache.juneau.xml.XmlSerializerSession.JsonType.*;
020import static org.apache.juneau.xml.annotation.XmlFormat.*;
021
022import java.io.*;
023import java.lang.reflect.*;
024import java.nio.charset.*;
025import java.util.*;
026import java.util.function.*;
027
028import org.apache.juneau.*;
029import org.apache.juneau.httppart.*;
030import org.apache.juneau.internal.*;
031import org.apache.juneau.serializer.*;
032import org.apache.juneau.svl.*;
033import org.apache.juneau.swap.*;
034import org.apache.juneau.xml.annotation.*;
035
036/**
037 * Session object that lives for the duration of a single use of {@link XmlSerializer}.
038 *
039 * <h5 class='section'>Notes:</h5><ul>
040 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
041 * </ul>
042 *
043 * <h5 class='section'>See Also:</h5><ul>
044 *    <li class='link'><a class="doclink" href="../../../../index.html#jm.XmlDetails">XML Details</a>
045
046 * </ul>
047 */
048@SuppressWarnings({"unchecked","rawtypes"})
049public class XmlSerializerSession extends WriterSerializerSession {
050
051   //-----------------------------------------------------------------------------------------------------------------
052   // Static
053   //-----------------------------------------------------------------------------------------------------------------
054
055   /**
056    * Creates a new builder for this object.
057    *
058    * @param ctx The context creating this session.
059    * @return A new builder.
060    */
061   public static Builder create(XmlSerializer ctx) {
062      return new Builder(ctx);
063   }
064
065   //-----------------------------------------------------------------------------------------------------------------
066   // Builder
067   //-----------------------------------------------------------------------------------------------------------------
068
069   /**
070    * Builder class.
071    */
072   @FluentSetters
073   public static class Builder extends WriterSerializerSession.Builder {
074
075      XmlSerializer ctx;
076
077      /**
078       * Constructor
079       *
080       * @param ctx The context creating this session.
081       */
082      protected Builder(XmlSerializer ctx) {
083         super(ctx);
084         this.ctx = ctx;
085      }
086
087      @Override
088      public XmlSerializerSession build() {
089         return new XmlSerializerSession(this);
090      }
091
092      // <FluentSetters>
093
094      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
095      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
096         super.apply(type, apply);
097         return this;
098      }
099
100      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
101      public Builder debug(Boolean value) {
102         super.debug(value);
103         return this;
104      }
105
106      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
107      public Builder properties(Map<String,Object> value) {
108         super.properties(value);
109         return this;
110      }
111
112      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
113      public Builder property(String key, Object value) {
114         super.property(key, value);
115         return this;
116      }
117
118      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
119      public Builder unmodifiable() {
120         super.unmodifiable();
121         return this;
122      }
123
124      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
125      public Builder locale(Locale value) {
126         super.locale(value);
127         return this;
128      }
129
130      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
131      public Builder localeDefault(Locale value) {
132         super.localeDefault(value);
133         return this;
134      }
135
136      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
137      public Builder mediaType(MediaType value) {
138         super.mediaType(value);
139         return this;
140      }
141
142      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
143      public Builder mediaTypeDefault(MediaType value) {
144         super.mediaTypeDefault(value);
145         return this;
146      }
147
148      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
149      public Builder timeZone(TimeZone value) {
150         super.timeZone(value);
151         return this;
152      }
153
154      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
155      public Builder timeZoneDefault(TimeZone value) {
156         super.timeZoneDefault(value);
157         return this;
158      }
159
160      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
161      public Builder javaMethod(Method value) {
162         super.javaMethod(value);
163         return this;
164      }
165
166      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
167      public Builder resolver(VarResolverSession value) {
168         super.resolver(value);
169         return this;
170      }
171
172      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
173      public Builder schema(HttpPartSchema value) {
174         super.schema(value);
175         return this;
176      }
177
178      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
179      public Builder schemaDefault(HttpPartSchema value) {
180         super.schemaDefault(value);
181         return this;
182      }
183
184      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
185      public Builder uriContext(UriContext value) {
186         super.uriContext(value);
187         return this;
188      }
189
190      @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */
191      public Builder fileCharset(Charset value) {
192         super.fileCharset(value);
193         return this;
194      }
195
196      @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */
197      public Builder streamCharset(Charset value) {
198         super.streamCharset(value);
199         return this;
200      }
201
202      @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */
203      public Builder useWhitespace(Boolean value) {
204         super.useWhitespace(value);
205         return this;
206      }
207
208      // </FluentSetters>
209   }
210
211   //-----------------------------------------------------------------------------------------------------------------
212   // Instance
213   //-----------------------------------------------------------------------------------------------------------------
214
215   private final XmlSerializer ctx;
216   private Namespace
217      defaultNamespace;
218   private Namespace[] namespaces = new Namespace[0];
219
220   /**
221    * Constructor.
222    *
223    * @param builder The builder for this object.
224    */
225   protected XmlSerializerSession(Builder builder) {
226      super(builder);
227      ctx = builder.ctx;
228      namespaces = ctx.getNamespaces();
229      defaultNamespace = findDefaultNamespace(ctx.getDefaultNamespace());
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(x -> x.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(x -> findNsfMappings(x));
358            }
359
360         } else if (aType.isMap()) {
361            ((Map<?,?>)o).forEach((k,v) -> findNsfMappings(v));
362         } else if (aType.isCollection()) {
363            ((Collection<?>)o).forEach(x -> findNsfMappings(x));
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 (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         en = type.toString();
551         type = null;
552      }
553
554      boolean encodeEn = elementName != null;
555      String ns = (elementNamespace == null ? null : elementNamespace.name);
556      String dns = null, elementNs = null;
557      if (isEnableNamespaces()) {
558         dns = elementName == null && defaultNamespace != null ? defaultNamespace.name : null;
559         elementNs = elementName == null ? dns : ns;
560         if (elementName == null)
561            elementNamespace = null;
562      }
563
564      // Render the start tag.
565      if (! isCollapsed) {
566         if (en != null) {
567            out.oTag(i, elementNs, en, encodeEn);
568            if (addNamespaceUris) {
569               out.attr((String)null, "xmlns", defaultNamespace.getUri());
570
571               for (Namespace n : namespaces)
572                  out.attr("xmlns", n.getName(), n.getUri());
573            }
574            if (! isExpectedType) {
575               if (resolvedDictionaryName != null)
576                  out.attr(dns, getBeanTypePropertyName(eType), resolvedDictionaryName);
577               else if (type != null && type != STRING)
578                  out.attr(dns, getBeanTypePropertyName(eType), type);
579            }
580            if (name != null)
581               out.attr(getNamePropertyName(), name);
582         } else {
583            out.i(i);
584         }
585         if (o == null) {
586            if ((sType.isBoolean() || sType.isNumber()) && ! sType.isNullable())
587               o = sType.getPrimitiveDefault();
588         }
589
590         if (o != null && ! (sType.isMapOrBean() || en == null))
591            out.w('>');
592
593         if (cr && ! (sType.isMapOrBean()))
594            out.nl(i+1);
595      }
596
597      ContentResult rc = CR_ELEMENTS;
598
599      // Render the tag contents.
600      if (o != null) {
601         if (sType.isUri() || (pMeta != null && pMeta.isUri())) {
602            out.textUri(o);
603         } else if (sType.isCharSequence() || sType.isChar()) {
604            if (isXmlText(format, sType))
605               out.append(o);
606            else
607               out.text(o, preserveWhitespace);
608         } else if (sType.isNumber() || sType.isBoolean()) {
609            out.append(o);
610         } else if (sType.isMap() || (wType != null && wType.isMap())) {
611            if (o instanceof BeanMap)
612               rc = serializeBeanMap(out, (BeanMap)o, elementNamespace, isCollapsed, isMixedOrText);
613            else
614               rc = serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), isMixedOrText);
615         } else if (sType.isBean()) {
616            rc = serializeBeanMap(out, toBeanMap(o), elementNamespace, isCollapsed, isMixedOrText);
617         } else if (sType.isCollection() || (wType != null && wType.isCollection())) {
618            if (isCollapsed)
619               this.indent--;
620            serializeCollection(out, o, sType, eType, pMeta, isMixedOrText);
621            if (isCollapsed)
622               this.indent++;
623         } else if (sType.isArray()) {
624            if (isCollapsed)
625               this.indent--;
626            serializeCollection(out, o, sType, eType, pMeta, isMixedOrText);
627            if (isCollapsed)
628               this.indent++;
629         } else if (sType.isReader()) {
630            pipe((Reader)o, out, SerializerSession::handleThrown);
631         } else if (sType.isInputStream()) {
632            pipe((InputStream)o, out, SerializerSession::handleThrown);
633         } else {
634            if (isXmlText(format, sType))
635               out.append(toString(o));
636            else
637               out.text(toString(o));
638         }
639      }
640
641      pop();
642
643      // Render the end tag.
644      if (! isCollapsed) {
645         if (en != null) {
646            if (rc == CR_EMPTY) {
647               if (isHtmlMode())
648                  out.w('>').eTag(elementNs, en, encodeEn);
649               else
650                  out.w('/').w('>');
651            } else if (rc == CR_VOID || o == null) {
652               out.w('/').w('>');
653            }
654            else
655               out.ie(cr && rc != CR_MIXED ? i : 0).eTag(elementNs, en, encodeEn);
656         }
657         if (! isMixedOrText)
658            out.nl(i);
659      }
660
661      return rc;
662   }
663
664   private boolean isXmlText(XmlFormat format, ClassMeta<?> sType) {
665      if (format == XMLTEXT)
666         return true;
667      XmlClassMeta xcm = getXmlClassMeta(sType);
668      if (xcm == null)
669         return false;
670      return xcm.getFormat() == XMLTEXT;
671   }
672
673   private ContentResult serializeMap(XmlWriter out, Map m, ClassMeta<?> sType,
674         ClassMeta<?> eKeyType, ClassMeta<?> eValueType, boolean isMixed) throws SerializeException {
675
676      ClassMeta<?> keyType = eKeyType == null ? sType.getKeyType() : eKeyType;
677      ClassMeta<?> valueType = eValueType == null ? sType.getValueType() : eValueType;
678
679      Flag hasChildren = Flag.create();
680      forEachEntry(m, e -> {
681
682         Object k = e.getKey();
683         if (k == null) {
684            k = "\u0000";
685         } else {
686            k = generalize(k, keyType);
687            if (isTrimStrings() && k instanceof String)
688               k = k.toString().trim();
689         }
690
691         Object value = e.getValue();
692
693         hasChildren.ifNotSet(()->out.w('>').nlIf(! isMixed, indent)).set();
694         serializeAnything(out, value, valueType, toString(k), null, null, false, XmlFormat.DEFAULT, isMixed, false, null);
695      });
696
697      return hasChildren.isSet() ? CR_ELEMENTS : CR_EMPTY;
698   }
699
700   private ContentResult serializeBeanMap(XmlWriter out, BeanMap<?> m,
701         Namespace elementNs, boolean isCollapsed, boolean isMixedOrText) throws SerializeException {
702      boolean hasChildren = false;
703      BeanMeta<?> bm = m.getMeta();
704
705      List<BeanPropertyValue> lp = new ArrayList<>();
706
707      Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null;
708      m.forEachValue(checkNull, (pMeta,key,value,thrown) -> {
709         lp.add(new BeanPropertyValue(pMeta, key, value, thrown));
710      });
711
712      XmlBeanMeta xbm = getXmlBeanMeta(bm);
713
714      Set<String>
715         attrs = xbm.getAttrPropertyNames(),
716         elements = xbm.getElementPropertyNames(),
717         collapsedElements = xbm.getCollapsedPropertyNames();
718      String
719         attrsProperty = xbm.getAttrsPropertyName(),
720         contentProperty = xbm.getContentPropertyName();
721
722      XmlFormat cf = null;
723
724      Object content = null;
725      ClassMeta<?> contentType = null;
726      for (BeanPropertyValue p : lp) {
727         String n = p.getName();
728         if (attrs.contains(n) || attrs.contains("*") || n.equals(attrsProperty)) {
729            BeanPropertyMeta pMeta = p.getMeta();
730            if (pMeta.canRead()) {
731               ClassMeta<?> cMeta = p.getClassMeta();
732
733               String key = p.getName();
734               Object value = p.getValue();
735               Throwable t = p.getThrown();
736               if (t != null)
737                  onBeanGetterException(pMeta, t);
738
739               if (canIgnoreValue(cMeta, key, value))
740                  continue;
741
742               XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta);
743               Namespace ns = (isEnableNamespaces() && bpXml.getNamespace() != elementNs ? bpXml.getNamespace() : null);
744
745               if (pMeta.isUri()  ) {
746                  out.attrUri(ns, key, value);
747               } else if (n.equals(attrsProperty)) {
748                  if (value instanceof BeanMap) {
749                     BeanMap<?> bm2 = (BeanMap)value;
750                     bm2.forEachValue(x -> true, (pMeta2,key2,value2,thrown2) -> {
751                        if (thrown2 != null)
752                           onBeanGetterException(pMeta, thrown2);
753                        out.attr(ns, key2, value2);
754                     });
755                  } else /* Map */ {
756                     Map m2 = (Map)value;
757                     if (m2 != null)
758                        m2.forEach((k,v) -> out.attr(ns, stringify(k), v));
759                  }
760               } else {
761                  out.attr(ns, key, value);
762               }
763            }
764         }
765      }
766
767      boolean
768         hasContent = false,
769         preserveWhitespace = false,
770         isVoidElement = xbm.getContentFormat() == VOID;
771
772      for (BeanPropertyValue p : lp) {
773         BeanPropertyMeta pMeta = p.getMeta();
774         if (pMeta.canRead()) {
775            ClassMeta<?> cMeta = p.getClassMeta();
776
777            String n = p.getName();
778            if (n.equals(contentProperty)) {
779               content = p.getValue();
780               contentType = p.getClassMeta();
781               hasContent = true;
782               cf = xbm.getContentFormat();
783               if (cf.isOneOf(MIXED,MIXED_PWS,TEXT,TEXT_PWS,XMLTEXT))
784                  isMixedOrText = true;
785               if (cf.isOneOf(MIXED_PWS, TEXT_PWS))
786                  preserveWhitespace = true;
787               if (contentType.isCollection() && ((Collection)content).isEmpty())
788                  hasContent = false;
789               else if (contentType.isArray() && Array.getLength(content) == 0)
790                  hasContent = false;
791            } else if (elements.contains(n) || collapsedElements.contains(n) || elements.contains("*") || collapsedElements.contains("*") ) {
792               String key = p.getName();
793               Object value = p.getValue();
794               Throwable t = p.getThrown();
795               if (t != null)
796                  onBeanGetterException(pMeta, t);
797
798               if (canIgnoreValue(cMeta, key, value))
799                  continue;
800
801               if (! hasChildren) {
802                  hasChildren = true;
803                  out.appendIf(! isCollapsed, '>').nlIf(! isMixedOrText, indent);
804               }
805
806               XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(pMeta);
807               serializeAnything(out, value, cMeta, key, null, bpXml.getNamespace(), false, bpXml.getXmlFormat(), isMixedOrText, false, pMeta);
808            }
809         }
810      }
811      if (contentProperty == null && ! hasContent)
812         return (hasChildren ? CR_ELEMENTS : isVoidElement ? CR_VOID : CR_EMPTY);
813
814      // Serialize XML content.
815      if (content != null) {
816         out.w('>').nlIf(! isMixedOrText, indent);
817         if (contentType == null) {
818         } else if (contentType.isCollection()) {
819            Collection c = (Collection)content;
820            for (Iterator i = c.iterator(); i.hasNext();) {
821               Object value = i.next();
822               serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
823            }
824         } else if (contentType.isArray()) {
825            Collection c = toList(Object[].class, content);
826            for (Iterator i = c.iterator(); i.hasNext();) {
827               Object value = i.next();
828               serializeAnything(out, value, contentType.getElementType(), null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
829            }
830         } else {
831            serializeAnything(out, content, contentType, null, null, null, false, cf, isMixedOrText, preserveWhitespace, null);
832         }
833      } else {
834         out.attr("nil", "true").w('>').nlIf(! isMixedOrText, indent);
835      }
836      return isMixedOrText ? CR_MIXED : CR_ELEMENTS;
837   }
838
839   private XmlWriter serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType,
840         ClassMeta<?> eType, BeanPropertyMeta ppMeta, boolean isMixed) throws SerializeException {
841
842      ClassMeta<?> eeType = eType.getElementType();
843
844      Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in));
845
846      String type2 = null;
847
848      Value<String> eName = Value.of(type2);
849      Value<Namespace> eNs = Value.empty();
850
851      if (ppMeta != null) {
852         XmlBeanPropertyMeta bpXml = getXmlBeanPropertyMeta(ppMeta);
853         eName.set(bpXml.getChildName());
854         eNs.set(bpXml.getNamespace());
855      }
856
857      forEachEntry(c, x -> serializeAnything(out, x, eeType, null, eName.get(), eNs.get(), false, XmlFormat.DEFAULT, isMixed, false, null));
858
859      return out;
860   }
861
862   static enum JsonType {
863      STRING("string"),BOOLEAN("boolean"),NUMBER("number"),ARRAY("array"),OBJECT("object"),NULL("null");
864
865      private final String value;
866      private JsonType(String value) {
867         this.value = value;
868      }
869
870      @Override
871      public String toString() {
872         return value;
873      }
874
875      boolean isOneOf(JsonType...types) {
876         for (JsonType type : types)
877            if (type == this)
878               return true;
879         return false;
880      }
881   }
882
883   /**
884    * Identifies what the contents were of a serialized bean.
885    */
886   @SuppressWarnings("javadoc")
887   public static enum ContentResult {
888      CR_VOID,      // No content...append "/>" to the start tag.
889      CR_EMPTY,     // No content...append "/>" to the start tag if XML, "/></end>" if HTML.
890      CR_MIXED,     // Mixed content...don't add whitespace.
891      CR_ELEMENTS   // Elements...use normal whitespace rules.
892   }
893
894   //-----------------------------------------------------------------------------------------------------------------
895   // Properties
896   //-----------------------------------------------------------------------------------------------------------------
897
898   /**
899    * Add <js>"_type"</js> properties when needed.
900    *
901    * @see XmlSerializer.Builder#addBeanTypesXml()
902    * @return
903    *    <jk>true</jk> if<js>"_type"</js> properties will be added to beans if their type cannot be inferred
904    *    through reflection.
905    */
906   @Override
907   protected boolean isAddBeanTypes() {
908      return ctx.isAddBeanTypes();
909   }
910
911   /**
912    * Add namespace URLs to the root element.
913    *
914    * @see XmlSerializer.Builder#addNamespaceUrisToRoot()
915    * @return
916    *    <jk>true</jk> if {@code xmlns:x} attributes are added to the root element for the default and all mapped namespaces.
917    */
918   protected final boolean isAddNamespaceUrisToRoot() {
919      return ctx.isAddNamespaceUrlsToRoot();
920   }
921
922   /**
923    * Auto-detect namespace usage.
924    *
925    * @see XmlSerializer.Builder#disableAutoDetectNamespaces()
926    * @return
927    *    <jk>true</jk> if namespace usage is detected before serialization.
928    */
929   protected final boolean isAutoDetectNamespaces() {
930      return ctx.isAutoDetectNamespaces();
931   }
932
933   /**
934    * Default namespace.
935    *
936    * @see XmlSerializer.Builder#defaultNamespace(Namespace)
937    * @return
938    *    The default namespace URI for this document.
939    */
940   protected final Namespace getDefaultNamespace() {
941      return defaultNamespace;
942   }
943
944   /**
945    * Enable support for XML namespaces.
946    *
947    * @see XmlSerializer.Builder#enableNamespaces()
948    * @return
949    *    <jk>false</jk> if XML output will not contain any namespaces regardless of any other settings.
950    */
951   protected final boolean isEnableNamespaces() {
952      return ctx.isEnableNamespaces();
953   }
954
955   /**
956    * Default namespaces.
957    *
958    * @see XmlSerializer.Builder#namespaces(Namespace...)
959    * @return
960    *    The default list of namespaces associated with this serializer.
961    */
962   protected final Namespace[] getNamespaces() {
963      return namespaces;
964   }
965
966   //-----------------------------------------------------------------------------------------------------------------
967   // Extended metadata
968   //-----------------------------------------------------------------------------------------------------------------
969
970   /**
971    * Returns the language-specific metadata on the specified class.
972    *
973    * @param cm The class to return the metadata on.
974    * @return The metadata.
975    */
976   public XmlClassMeta getXmlClassMeta(ClassMeta<?> cm) {
977      return ctx.getXmlClassMeta(cm);
978   }
979
980   /**
981    * Returns the language-specific metadata on the specified bean.
982    *
983    * @param bm The bean to return the metadata on.
984    * @return The metadata.
985    */
986   public XmlBeanMeta getXmlBeanMeta(BeanMeta<?> bm) {
987      return ctx.getXmlBeanMeta(bm);
988   }
989
990   /**
991    * Returns the language-specific metadata on the specified bean property.
992    *
993    * @param bpm The bean property to return the metadata on.
994    * @return The metadata.
995    */
996   public XmlBeanPropertyMeta getXmlBeanPropertyMeta(BeanPropertyMeta bpm) {
997      return bpm == null ? XmlBeanPropertyMeta.DEFAULT : ctx.getXmlBeanPropertyMeta(bpm);
998   }
999}