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