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.html;
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.StringUtils.*;
023import static org.apache.juneau.commons.utils.ThrowableUtils.*;
024import static org.apache.juneau.commons.utils.Utils.*;
025import static org.apache.juneau.xml.XmlSerializerSession.ContentResult.*;
026
027import java.io.*;
028import java.lang.reflect.*;
029import java.nio.charset.*;
030import java.util.*;
031import java.util.function.*;
032import java.util.regex.*;
033
034import org.apache.juneau.*;
035import org.apache.juneau.commons.lang.*;
036import org.apache.juneau.html.annotation.*;
037import org.apache.juneau.httppart.*;
038import org.apache.juneau.serializer.*;
039import org.apache.juneau.svl.*;
040import org.apache.juneau.swap.*;
041import org.apache.juneau.xml.*;
042import org.apache.juneau.xml.annotation.*;
043
044/**
045 * Session object that lives for the duration of a single use of {@link HtmlSerializer}.
046 *
047 * <h5 class='section'>Notes:</h5><ul>
048 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
049 * </ul>
050 *
051 * <h5 class='section'>See Also:</h5><ul>
052 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HtmlBasics">HTML Basics</a>
053
054 * </ul>
055 */
056@SuppressWarnings("resource")
057public class HtmlSerializerSession extends XmlSerializerSession {
058   /**
059    * Builder class.
060    */
061   public static class Builder extends XmlSerializerSession.Builder {
062
063      private HtmlSerializer ctx;
064
065      /**
066       * Constructor
067       *
068       * @param ctx The context creating this session.
069       *    <br>Cannot be <jk>null</jk>.
070       */
071      protected Builder(HtmlSerializer ctx) {
072         super(assertArgNotNull("ctx", ctx));
073         this.ctx = ctx;
074      }
075
076      @Override /* Overridden from Builder */
077      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
078         super.apply(type, apply);
079         return this;
080      }
081
082      @Override
083      public HtmlSerializerSession build() {
084         return new HtmlSerializerSession(this);
085      }
086
087      @Override /* Overridden from Builder */
088      public Builder debug(Boolean value) {
089         super.debug(value);
090         return this;
091      }
092
093      @Override /* Overridden from Builder */
094      public Builder fileCharset(Charset value) {
095         super.fileCharset(value);
096         return this;
097      }
098
099      @Override /* Overridden from Builder */
100      public Builder javaMethod(Method value) {
101         super.javaMethod(value);
102         return this;
103      }
104
105      @Override /* Overridden from Builder */
106      public Builder locale(Locale value) {
107         super.locale(value);
108         return this;
109      }
110
111      @Override /* Overridden from Builder */
112      public Builder mediaType(MediaType value) {
113         super.mediaType(value);
114         return this;
115      }
116
117      @Override /* Overridden from Builder */
118      public Builder mediaTypeDefault(MediaType value) {
119         super.mediaTypeDefault(value);
120         return this;
121      }
122
123      @Override /* Overridden from Builder */
124      public Builder properties(Map<String,Object> value) {
125         super.properties(value);
126         return this;
127      }
128
129      @Override /* Overridden from Builder */
130      public Builder property(String key, Object value) {
131         super.property(key, value);
132         return this;
133      }
134
135      @Override /* Overridden from Builder */
136      public Builder resolver(VarResolverSession value) {
137         super.resolver(value);
138         return this;
139      }
140
141      @Override /* Overridden from Builder */
142      public Builder schema(HttpPartSchema value) {
143         super.schema(value);
144         return this;
145      }
146
147      @Override /* Overridden from Builder */
148      public Builder schemaDefault(HttpPartSchema value) {
149         super.schemaDefault(value);
150         return this;
151      }
152
153      @Override /* Overridden from Builder */
154      public Builder streamCharset(Charset value) {
155         super.streamCharset(value);
156         return this;
157      }
158
159      @Override /* Overridden from Builder */
160      public Builder timeZone(TimeZone value) {
161         super.timeZone(value);
162         return this;
163      }
164
165      @Override /* Overridden from Builder */
166      public Builder timeZoneDefault(TimeZone value) {
167         super.timeZoneDefault(value);
168         return this;
169      }
170
171      @Override /* Overridden from Builder */
172      public Builder unmodifiable() {
173         super.unmodifiable();
174         return this;
175      }
176
177      @Override /* Overridden from Builder */
178      public Builder uriContext(UriContext value) {
179         super.uriContext(value);
180         return this;
181      }
182
183      @Override /* Overridden from Builder */
184      public Builder useWhitespace(Boolean value) {
185         super.useWhitespace(value);
186         return this;
187      }
188   }
189
190   /**
191    * Creates a new builder for this object.
192    *
193    * @param ctx The context creating this session.
194    *    <br>Cannot be <jk>null</jk>.
195    * @return A new builder.
196    */
197   public static Builder create(HtmlSerializer ctx) {
198      return new Builder(assertArgNotNull("ctx", ctx));
199   }
200
201   private final HtmlSerializer ctx;
202   private final Pattern urlPattern = Pattern.compile("http[s]?\\:\\/\\/.*");
203   private final Pattern labelPattern;
204
205   /**
206    * Constructor.
207    *
208    * @param builder The builder for this object.
209    */
210   protected HtmlSerializerSession(Builder builder) {
211      super(builder);
212      ctx = builder.ctx;
213      labelPattern = Pattern.compile("[\\?\\&]" + Pattern.quote(ctx.getLabelParameter()) + "=([^\\&]*)");
214   }
215
216   /**
217    * Returns the anchor text to use for the specified URL object.
218    *
219    * @param pMeta
220    *    The property metadata of the bean property of the object.
221    *    Can be <jk>null</jk> if the object isn't from a bean property.
222    * @param o The URL object.
223    * @return The anchor text to use for the specified URL object.
224    */
225   public String getAnchorText(BeanPropertyMeta pMeta, Object o) {
226      var s = o.toString();
227      if (isDetectLabelParameters()) {
228         Matcher m = labelPattern.matcher(s);
229         if (m.find())
230            return urlDecode(m.group(1));
231      }
232      return switch (getUriAnchorText()) {
233         case LAST_TOKEN -> {
234            s = resolveUri(s);
235            if (s.indexOf('/') != -1)
236               s = s.substring(s.lastIndexOf('/') + 1);
237            if (s.indexOf('?') != -1)
238               s = s.substring(0, s.indexOf('?'));
239            if (s.indexOf('#') != -1)
240               s = s.substring(0, s.indexOf('#'));
241            if (s.isEmpty())
242               s = "/";
243            yield urlDecode(s);
244         }
245         case URI_ANCHOR -> {
246            if (s.indexOf('#') != -1)
247               s = s.substring(s.lastIndexOf('#') + 1);
248            yield urlDecode(s);
249         }
250         case PROPERTY_NAME -> pMeta == null ? s : pMeta.getName();
251         case URI -> resolveUri(s);
252         case CONTEXT_RELATIVE -> relativizeUri("context:/", s);
253         case SERVLET_RELATIVE -> relativizeUri("servlet:/", s);
254         case PATH_RELATIVE -> relativizeUri("request:/", s);
255         default /* TO_STRING */ -> s;
256      };
257   }
258
259   @Override /* Overridden from XmlSerializer */
260   public boolean isHtmlMode() { return true; }
261
262   /**
263    * Returns <jk>true</jk> if the specified object is a URL.
264    *
265    * @param cm The ClassMeta of the object being serialized.
266    * @param pMeta
267    *    The property metadata of the bean property of the object.
268    *    Can be <jk>null</jk> if the object isn't from a bean property.
269    * @param o The object.
270    * @return <jk>true</jk> if the specified object is a URL.
271    */
272   public boolean isUri(ClassMeta<?> cm, BeanPropertyMeta pMeta, Object o) {
273      if (cm.isUri() || (nn(pMeta) && pMeta.isUri()))
274         return true;
275      if (isDetectLinksInStrings() && o instanceof CharSequence && urlPattern.matcher(o.toString()).matches())
276         return true;
277      return false;
278   }
279
280   /**
281    * Main serialization routine.
282    *
283    * @param session The serialization context object.
284    * @param o The object being serialized.
285    * @param w The writer to serialize to.
286    * @return The same writer passed in.
287    * @throws IOException If a problem occurred trying to send output to the writer.
288    */
289   private XmlWriter doSerialize(Object o, XmlWriter w) throws IOException, SerializeException {
290      serializeAnything(w, o, getExpectedRootType(o), null, null, getInitialDepth() - 1, true, false);
291      return w;
292   }
293
294   private String getAnchorText(BeanPropertyMeta pMeta) {
295      return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getAnchorText();
296   }
297
298   private String getLink(BeanPropertyMeta pMeta) {
299      return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getLink();
300   }
301
302   private HtmlRender<?> getRender(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) {
303      if (pMeta == null)
304         return null;
305      HtmlRender<?> render = getHtmlBeanPropertyMeta(pMeta).getRender();
306      if (nn(render))
307         return render;
308      var cMeta = session.getClassMetaForObject(value);
309      render = cMeta == null ? null : getHtmlClassMeta(cMeta).getRender();
310      return render;
311   }
312
313   @SuppressWarnings({ "rawtypes", "unchecked" })
314   private String getStyle(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) {
315      // Check for annotation style first
316      if (nn(pMeta)) {
317         String annotationStyle = getHtmlBeanPropertyMeta(pMeta).getStyle();
318         if (nn(annotationStyle))
319            return annotationStyle;
320      }
321      // Fall back to render
322      HtmlRender render = getRender(session, pMeta, value);
323      return render == null ? null : render.getStyle(session, value);
324   }
325
326   /*
327    * Returns the table column headers for the specified collection of objects.
328    * Returns null if collection should not be serialized as a 2-dimensional table.
329    * Returns an empty array if it should be treated as a table but without headers.
330    * 2-dimensional tables are used for collections of objects that all have the same set of property names.
331    */
332   @SuppressWarnings({ "rawtypes", "unchecked" })
333   private Object[] getTableHeaders(Collection c, HtmlBeanPropertyMeta bpHtml) throws SerializeException {
334
335      if (c.isEmpty())
336         return null;
337
338      c = sort(c);
339
340      var o1 = (Object)null;
341      for (var o : c)
342         if (nn(o)) {
343            o1 = o;
344            break;
345         }
346      if (o1 == null)
347         return null;
348
349      var cm1 = getClassMetaForObject(o1);
350
351      ObjectSwap swap = cm1.getSwap(this);
352      o1 = swap(swap, o1);
353      if (nn(swap))
354         cm1 = swap.getSwapClassMeta(this);
355
356      if (cm1 == null || ! cm1.isMapOrBean() || getAnnotationProvider().has(HtmlLink.class, cm1))
357         return null;
358
359      HtmlClassMeta cHtml = getHtmlClassMeta(cm1);
360
361      if (cHtml.isNoTables() || bpHtml.isNoTables() || cHtml.isXml() || bpHtml.isXml() || canIgnoreValue(cm1, null, o1))
362         return null;
363
364      if (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())
365         return new Object[0];
366
367      // If it's a non-bean map, only use table if all entries are also maps.
368      if (cm1.isMap() && ! cm1.isBeanMap()) {
369
370         Set<Object> set = set();
371         for (var o : c) {
372            o = swap(swap, o);
373            if (! canIgnoreValue(cm1, null, o)) {
374               if (! cm1.isInstance(o))
375                  return null;
376               forEachEntry((Map)o, x -> set.add(x.getKey()));
377            }
378         }
379         return set.toArray(new Object[set.size()]);
380      }
381
382      // Must be a bean or BeanMap.
383      for (var o : c) {
384         o = swap(swap, o);
385         if (! canIgnoreValue(cm1, null, o)) {
386            if (! cm1.isInstance(o))
387               return null;
388         }
389      }
390
391      BeanMap<?> bm = toBeanMap(o1);
392      return bm.keySet().toArray(new String[bm.size()]);
393   }
394
395   private void serializeBeanMap(XmlWriter out, BeanMap<?> m, ClassMeta<?> eType, BeanPropertyMeta ppMeta) throws SerializeException {
396
397      HtmlClassMeta cHtml = getHtmlClassMeta(m.getClassMeta());
398      HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta);
399
400      int i = indent;
401
402      out.oTag(i, "table");
403
404      String typeName = m.getMeta().getDictionaryName();
405      if (nn(typeName) && eType != m.getClassMeta())
406         out.attr(getBeanTypePropertyName(m.getClassMeta()), typeName);
407
408      out.w('>').nl(i);
409      if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) {
410         out.sTag(i + 1, "tr").nl(i + 1);
411         out.sTag(i + 2, "th").append("key").eTag("th").nl(i + 2);
412         out.sTag(i + 2, "th").append("value").eTag("th").nl(i + 2);
413         out.ie(i + 1).eTag("tr").nl(i + 1);
414      }
415
416      Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
417
418      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
419         var cMeta = pMeta.getClassMeta();
420
421         if (nn(thrown))
422            onBeanGetterException(pMeta, thrown);
423
424         if (canIgnoreValue(cMeta, key, value))
425            return;
426
427         var link = (String)null;
428         var anchorText = (String)null;
429         if (! cMeta.isCollectionOrArray()) {
430            link = m.resolveVars(getLink(pMeta));
431            anchorText = m.resolveVars(getAnchorText(pMeta));
432         }
433
434         if (nn(anchorText))
435            value = anchorText;
436
437         out.sTag(i + 1, "tr").nl(i + 1);
438         out.sTag(i + 2, "td").text(key).eTag("td").nl(i + 2);
439         out.oTag(i + 2, "td");
440         String style = getStyle(this, pMeta, value);
441         if (nn(style))
442            out.attr("style", style);
443         out.cTag();
444
445         try {
446            if (nn(link))
447               out.oTag(i + 3, "a").attrUri("href", link).cTag();
448            ContentResult cr = serializeAnything(out, value, cMeta, key, pMeta, 2, false, true);
449            if (cr == CR_ELEMENTS)
450               out.i(i + 2);
451            if (nn(link))
452               out.eTag("a");
453         } catch (SerializeException | Error e) {
454            throw e;
455         } catch (Throwable e) {
456            onBeanGetterException(pMeta, e);
457         }
458         out.eTag("td").nl(i + 2);
459         out.ie(i + 1).eTag("tr").nl(i + 1);
460      });
461
462      out.ie(i).eTag("table").nl(i);
463   }
464
465   @SuppressWarnings({ "rawtypes", "unchecked" })
466   private void serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, ClassMeta<?> eType, String name, BeanPropertyMeta ppMeta) throws SerializeException {
467
468      HtmlClassMeta cHtml = getHtmlClassMeta(sType);
469      HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta);
470
471      Collection c = (sType.isCollection() ? (Collection)in : toList(sType.inner(), in));
472
473      boolean isCdc = cHtml.isHtmlCdc() || bpHtml.isHtmlCdc();
474      boolean isSdc = cHtml.isHtmlSdc() || bpHtml.isHtmlSdc();
475      boolean isDc = isCdc || isSdc;
476
477      int i = indent;
478      if (c.isEmpty()) {
479         out.appendln(i, "<ul></ul>");
480         return;
481      }
482
483      var type2 = (String)null;
484      if (sType != eType)
485         type2 = sType.getBeanDictionaryName();
486      if (type2 == null)
487         type2 = "array";
488
489      c = sort(c);
490
491      String btpn = getBeanTypePropertyName(eType);
492
493      // Look at the objects to see how we're going to handle them.  Check the first object to see how we're going to
494      // handle this.
495      // If it's a map or bean, then we'll create a table.
496      // Otherwise, we'll create a list.
497      Object[] th = getTableHeaders(c, bpHtml);
498
499      if (nn(th)) {
500
501         out.oTag(i, "table").attr(btpn, type2).w('>').nl(i + 1);
502         if (th.length > 0) {
503            out.sTag(i + 1, "tr").nl(i + 2);
504            for (var key : th) {
505               out.sTag(i + 2, "th");
506               out.text(convertToType(key, String.class));
507               out.eTag("th").nl(i + 2);
508            }
509            out.ie(i + 1).eTag("tr").nl(i + 1);
510         } else {
511            th = null;
512         }
513
514         for (var o : c) {
515            var cm = getClassMetaForObject(o);
516
517            if (nn(cm) && nn(cm.getSwap(this))) {
518               ObjectSwap swap = cm.getSwap(this);
519               o = swap(swap, o);
520               cm = swap.getSwapClassMeta(this);
521            }
522
523            out.oTag(i + 1, "tr");
524            String typeName = (cm == null ? null : cm.getBeanDictionaryName());
525            String typeProperty = getBeanTypePropertyName(cm);
526
527            if (nn(typeName) && eType.getElementType() != cm)
528               out.attr(typeProperty, typeName);
529            out.cTag().nl(i + 2);
530
531            if (cm == null) {
532               out.i(i + 2);
533               serializeAnything(out, o, null, null, null, 1, false, false);
534               out.nl(0);
535
536            } else if (cm.isMap() && ! (cm.isBeanMap())) {
537               Map m2 = sort((Map)o);
538
539               if (th == null)
540                  th = m2.keySet().toArray(new Object[m2.size()]);
541
542               for (var k : th) {
543                  out.sTag(i + 2, "td");
544                  ContentResult cr = serializeAnything(out, m2.get(k), eType.getElementType(), toString(k), null, 2, false, true);
545                  if (cr == CR_ELEMENTS)
546                     out.i(i + 2);
547                  out.eTag("td").nl(i + 2);
548               }
549            } else {
550               BeanMap m2 = toBeanMap(o);
551
552               if (th == null)
553                  th = m2.keySet().toArray(new Object[m2.size()]);
554
555               for (var k : th) {
556                  BeanMapEntry p = m2.getProperty(toString(k));
557                  BeanPropertyMeta pMeta = p.getMeta();
558                  if (pMeta.canRead()) {
559                     Object value = p.getValue();
560
561                     var link = (String)null;
562                     var anchorText = (String)null;
563                     if (! pMeta.getClassMeta().isCollectionOrArray()) {
564                        link = m2.resolveVars(getLink(pMeta));
565                        anchorText = m2.resolveVars(getAnchorText(pMeta));
566                     }
567
568                     if (nn(anchorText))
569                        value = anchorText;
570
571                     String style = getStyle(this, pMeta, value);
572                     out.oTag(i + 2, "td");
573                     if (nn(style))
574                        out.attr("style", style);
575                     out.cTag();
576                     if (nn(link))
577                        out.oTag("a").attrUri("href", link).cTag();
578                     ContentResult cr = serializeAnything(out, value, pMeta.getClassMeta(), p.getKey().toString(), pMeta, 2, false, true);
579                     if (cr == CR_ELEMENTS)
580                        out.i(i + 2);
581                     if (nn(link))
582                        out.eTag("a");
583                     out.eTag("td").nl(i + 2);
584                  }
585               }
586            }
587            out.ie(i + 1).eTag("tr").nl(i + 1);
588         }
589         out.ie(i).eTag("table").nl(i);
590
591      } else {
592         out.oTag(i, isDc ? "p" : "ul");
593         if (! type2.equals("array"))
594            out.attr(btpn, type2);
595         out.w('>').nl(i + 1);
596         var isFirst = true;
597         for (var o : c) {
598            if (isDc && ! isFirst)
599               out.append(isCdc ? ", " : " ");
600            if (! isDc)
601               out.oTag(i + 1, "li");
602            String style = getStyle(this, ppMeta, o);
603            String link = getLink(ppMeta);
604            if (nn(style) && ! isDc)
605               out.attr("style", style);
606            if (! isDc)
607               out.cTag();
608            if (nn(link))
609               out.oTag(i + 2, "a").attrUri("href", link.replace("{#}", s(o))).cTag();
610            ContentResult cr = serializeAnything(out, o, eType.getElementType(), name, null, 1, false, true);
611            if (nn(link))
612               out.eTag("a");
613            if (cr == CR_ELEMENTS)
614               out.ie(i + 1);
615            if (! isDc)
616               out.eTag("li").nl(i + 1);
617            isFirst = false;
618         }
619         out.ie(i).eTag(isDc ? "p" : "ul").nl(i);
620      }
621   }
622
623   @SuppressWarnings({ "rawtypes", "unchecked" })
624   private void serializeMap(XmlWriter out, Map m, ClassMeta<?> sType, ClassMeta<?> eKeyType, ClassMeta<?> eValueType, String typeName, BeanPropertyMeta ppMeta) throws SerializeException {
625
626      var keyType = eKeyType == null ? string() : eKeyType;
627      var valueType = eValueType == null ? object() : eValueType;
628      var aType = getClassMetaForObject(m);       // The actual type
629      HtmlClassMeta cHtml = getHtmlClassMeta(aType);
630      HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta);
631
632      int i = indent;
633
634      out.oTag(i, "table");
635
636      if (nn(typeName) && nn(ppMeta) && ppMeta.getClassMeta() != aType)
637         out.attr(getBeanTypePropertyName(sType), typeName);
638
639      out.append(">").nl(i + 1);
640      if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) {
641         out.sTag(i + 1, "tr").nl(i + 2);
642         out.sTag(i + 2, "th").append("key").eTag("th").nl(i + 3);
643         out.sTag(i + 2, "th").append("value").eTag("th").nl(i + 3);
644         out.ie(i + 1).eTag("tr").nl(i + 2);
645      }
646
647      forEachEntry(m, x -> serializeMapEntry(out, x, keyType, valueType, i, ppMeta));
648
649      out.ie(i).eTag("table").nl(i);
650   }
651
652   @SuppressWarnings("rawtypes")
653   private void serializeMapEntry(XmlWriter out, Map.Entry e, ClassMeta<?> keyType, ClassMeta<?> valueType, int i, BeanPropertyMeta ppMeta) throws SerializeException {
654      Object key = generalize(e.getKey(), keyType);
655      var value = (Object)null;
656      try {
657         value = e.getValue();
658      } catch (StackOverflowError t) {
659         throw t;
660      } catch (Throwable t) {
661         onError(t, "Could not call getValue() on property ''{0}'', {1}", e.getKey(), lm(t));
662      }
663
664      String link = getLink(ppMeta);
665      String style = getStyle(this, ppMeta, value);
666
667      out.sTag(i + 1, "tr").nl(i + 2);
668      out.oTag(i + 2, "td");
669      if (nn(style))
670         out.attr("style", style);
671      out.cTag();
672      if (nn(link))
673         out.oTag(i + 3, "a").attrUri("href", link.replace("{#}", s(value))).cTag();
674      ContentResult cr = serializeAnything(out, key, keyType, null, null, 2, false, false);
675      if (nn(link))
676         out.eTag("a");
677      if (cr == CR_ELEMENTS)
678         out.i(i + 2);
679      out.eTag("td").nl(i + 2);
680      out.sTag(i + 2, "td");
681      cr = serializeAnything(out, value, valueType, (key == null ? "_x0000_" : toString(key)), null, 2, false, true);
682      if (cr == CR_ELEMENTS)
683         out.ie(i + 2);
684      out.eTag("td").nl(i + 2);
685      out.ie(i + 1).eTag("tr").nl(i + 1);
686
687   }
688
689   @Override /* Overridden from Serializer */
690   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
691      doSerialize(o, getHtmlWriter(out));
692   }
693
694   /**
695    * Returns the language-specific metadata on the specified bean property.
696    *
697    * @param bpm The bean property to return the metadata on.
698    * @return The metadata.
699    */
700   protected HtmlBeanPropertyMeta getHtmlBeanPropertyMeta(BeanPropertyMeta bpm) {
701      return ctx.getHtmlBeanPropertyMeta(bpm);
702   }
703
704   /**
705    * Returns the language-specific metadata on the specified class.
706    *
707    * @param cm The class to return the metadata on.
708    * @return The metadata.
709    */
710   protected HtmlClassMeta getHtmlClassMeta(ClassMeta<?> cm) {
711      return ctx.getHtmlClassMeta(cm);
712   }
713
714   /**
715    * Converts the specified output target object to an {@link HtmlWriter}.
716    *
717    * @param out The output target object.
718    * @return The output target object wrapped in an {@link HtmlWriter}.
719    * @throws IOException Thrown by underlying stream.
720    */
721   protected final HtmlWriter getHtmlWriter(SerializerPipe out) throws IOException {
722      Object output = out.getRawOutput();
723      if (output instanceof HtmlWriter output2)
724         return output2;
725      var w = new HtmlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getQuoteChar(), getUriResolver());
726      out.setWriter(w);
727      return w;
728   }
729
730   /**
731    * Link label parameter name.
732    *
733    * @see HtmlSerializer.Builder#labelParameter(String)
734    * @return
735    *    The parameter name to look for when resolving link labels.
736    */
737   protected final String getLabelParameter() { return ctx.getLabelParameter(); }
738
739   /**
740    * Anchor text source.
741    *
742    * @see HtmlSerializer.Builder#uriAnchorText(AnchorText)
743    * @return
744    *    When creating anchor tags (e.g. <code><xt>&lt;a</xt> <xa>href</xa>=<xs>'...'</xs>
745    *    <xt>&gt;</xt>text<xt>&lt;/a&gt;</xt></code>) in HTML, this setting defines what to set the inner text to.
746    */
747   protected final AnchorText getUriAnchorText() { return ctx.getUriAnchorText(); }
748
749   /**
750    * Add <js>"_type"</js> properties when needed.
751    *
752    * @see HtmlSerializer.Builder#addBeanTypesHtml()
753    * @return
754    *    <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
755    *    through reflection.
756    */
757   @Override
758   protected final boolean isAddBeanTypes() { return ctx.isAddBeanTypes(); }
759
760   /**
761    * Add key/value headers on bean/map tables.
762    *
763    * @see HtmlSerializer.Builder#addKeyValueTableHeaders()
764    * @return
765    *    <jk>true</jk> if <bc>key</bc> and <bc>value</bc> column headers are added to tables.
766    */
767   protected final boolean isAddKeyValueTableHeaders() { return ctx.isAddKeyValueTableHeaders(); }
768
769   /**
770    * Look for link labels in URIs.
771    *
772    * @see HtmlSerializer.Builder#disableDetectLabelParameters()
773    * @return
774    *    <jk>true</jk> if we should ook for URL label parameters (e.g. <js>"?label=foobar"</js>).
775    */
776   protected final boolean isDetectLabelParameters() { return ctx.isDetectLabelParameters(); }
777
778   /**
779    * Look for URLs in {@link String Strings}.
780    *
781    * @see HtmlSerializer.Builder#disableDetectLinksInStrings()
782    * @return
783    *    <jk>true</jk> if we should automatically convert strings to URLs if they look like a URL.
784    */
785   protected final boolean isDetectLinksInStrings() { return ctx.isDetectLinksInStrings(); }
786
787   /**
788    * Serialize the specified object to the specified writer.
789    *
790    * @param out The writer.
791    * @param o The object to serialize.
792    * @param eType The expected type of the object if this is a bean property.
793    * @param name
794    *    The attribute name of this object if this object was a field in a JSON object (i.e. key of a
795    *    {@link java.util.Map.Entry} or property name of a bean).
796    * @param pMeta The bean property being serialized, or <jk>null</jk> if we're not serializing a bean property.
797    * @param xIndent The current indentation value.
798    * @param isRoot <jk>true</jk> if this is the root element of the document.
799    * @param nlIfElement <jk>true</jk> if we should add a newline to the output before serializing only if the object is an element and not text.
800    * @return The type of content encountered.  Either simple (no whitespace) or normal (elements with whitespace).
801    * @throws SerializeException Generic serialization error occurred.
802    */
803   @SuppressWarnings({ "rawtypes", "unchecked", "null" })
804   protected ContentResult serializeAnything(XmlWriter out, Object o, ClassMeta<?> eType, String name, BeanPropertyMeta pMeta, int xIndent, boolean isRoot, boolean nlIfElement)
805      throws SerializeException {
806
807      ClassMeta<?> aType = null;       // The actual type
808      ClassMeta<?> wType = null;     // The wrapped type (delegate)
809      ClassMeta<?> sType = object();   // The serialized type
810
811      var addJsonTags = isAddJsonTags();
812
813      if (eType == null)
814         eType = object();
815
816      aType = push2(name, o, eType);
817
818      // Handle recursion
819      if (aType == null) {
820         o = null;
821         aType = object();
822      }
823
824      // Handle Optional<X>
825      if (isOptional(aType)) {
826         o = getOptionalValue(o);
827         eType = getOptionalType(eType);
828         aType = getClassMetaForObject(o, object());
829      }
830
831      indent += xIndent;
832
833      ContentResult cr = CR_ELEMENTS;
834
835      // Determine the type.
836      if (o == null || (aType.isChar() && ((Character)o).charValue() == 0)) {
837         if (addJsonTags)
838            out.tag("null");
839         cr = ContentResult.CR_MIXED;
840
841      } else {
842
843         if (aType.isDelegate()) {
844            wType = aType;
845            aType = ((Delegate)o).getClassMeta();
846         }
847
848         sType = aType;
849
850         var typeName = (String)null;
851         if (isAddBeanTypes() && ! eType.equals(aType))
852            typeName = aType.getBeanDictionaryName();
853
854         // Swap if necessary
855         ObjectSwap swap = aType.getSwap(this);
856         if (nn(swap)) {
857            o = swap(swap, o);
858            sType = swap.getSwapClassMeta(this);
859
860            // If the getSwapClass() method returns Object, we need to figure out
861            // the actual type now.
862            if (sType.isObject())
863               sType = getClassMetaForObject(o);
864         }
865
866         // Handle the case where we're serializing a raw stream.
867         if (sType.isReader() || sType.isInputStream()) {
868            pop();
869            indent -= xIndent;
870            if (sType.isReader())
871               pipe((Reader)o, out, SerializerSession::handleThrown);
872            else
873               pipe((InputStream)o, out, SerializerSession::handleThrown);
874            return ContentResult.CR_MIXED;
875         }
876
877         HtmlClassMeta cHtml = getHtmlClassMeta(sType);
878         HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(pMeta);
879
880         HtmlRender render = firstNonNull(bpHtml.getRender(), cHtml.getRender());
881
882         if (nn(render)) {
883            Object o2 = render.getContent(this, o);
884            if (o2 != o) {
885               indent -= xIndent;
886               pop();
887               out.nl(indent);
888               return serializeAnything(out, o2, null, typeName, null, xIndent, false, false);
889            }
890         }
891
892         if (cHtml.isXml() || bpHtml.isXml()) {
893            pop();
894            indent++;
895            if (nlIfElement)
896               out.nl(0);
897            super.serializeAnything(out, o, null, null, null, null, false, XmlFormat.MIXED, false, false, null);
898            indent -= xIndent + 1;
899            return cr;
900
901         } else if (cHtml.isPlainText() || bpHtml.isPlainText()) {
902            out.w(o == null ? "null" : o.toString());
903            cr = CR_MIXED;
904
905         } else if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) {
906            if (addJsonTags)
907               out.tag("null");
908            cr = CR_MIXED;
909
910         } else if (sType.isNumber()) {
911            if (eType.isNumber() && ! (isRoot && addJsonTags))
912               out.append(o);
913            else
914               out.sTag("number").append(o).eTag("number");
915            cr = CR_MIXED;
916
917         } else if (sType.isBoolean()) {
918            if (eType.isBoolean() && ! (isRoot && addJsonTags))
919               out.append(o);
920            else
921               out.sTag("boolean").append(o).eTag("boolean");
922            cr = CR_MIXED;
923
924         } else if (sType.isMap() || (nn(wType) && wType.isMap())) {
925            out.nlIf(! isRoot, xIndent + 1);
926            if (o instanceof BeanMap)
927               serializeBeanMap(out, (BeanMap)o, eType, pMeta);
928            else
929               serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), typeName, pMeta);
930
931         } else if (sType.isBean()) {
932            BeanMap m = toBeanMap(o);
933            if (getAnnotationProvider().has(HtmlLink.class, aType)) {
934               var uriProperty = Value.<String>empty();
935               var nameProperty = Value.<String>empty();
936               aType.forEachAnnotation(HtmlLink.class, x -> ne(x.uriProperty()), x -> uriProperty.set(x.uriProperty()));
937               aType.forEachAnnotation(HtmlLink.class, x -> ne(x.nameProperty()), x -> nameProperty.set(x.nameProperty()));
938               Object urlProp = m.get(uriProperty.orElse(""));
939               Object nameProp = m.get(nameProperty.orElse(""));
940
941               out.oTag("a").attrUri("href", urlProp).w('>').text(nameProp).eTag("a");
942               cr = CR_MIXED;
943            } else {
944               out.nlIf(! isRoot, xIndent + 2);
945               serializeBeanMap(out, m, eType, pMeta);
946            }
947
948         } else if (sType.isCollection() || sType.isArray() || (nn(wType) && wType.isCollection())) {
949            out.nlIf(! isRoot, xIndent + 1);
950            serializeCollection(out, o, sType, eType, name, pMeta);
951
952         } else if (isUri(sType, pMeta, o)) {
953            String label = getAnchorText(pMeta, o);
954            out.oTag("a").attrUri("href", o).w('>');
955            out.text(label);
956            out.eTag("a");
957            cr = CR_MIXED;
958
959         } else {
960            if (isRoot && addJsonTags)
961               out.sTag("string").text(toString(o)).eTag("string");
962            else
963               out.text(toString(o));
964            cr = CR_MIXED;
965         }
966      }
967      pop();
968      indent -= xIndent;
969      return cr;
970   }
971
972   @SuppressWarnings({ "rawtypes" })
973   @Override /* Overridden from XmlSerializerSession */
974   protected ContentResult serializeAnything(XmlWriter out, Object o, ClassMeta<?> eType, String keyName, String elementName, Namespace elementNamespace, boolean addNamespaceUris, XmlFormat format,
975      boolean isMixed, boolean preserveWhitespace, BeanPropertyMeta pMeta) throws SerializeException {
976
977      // If this is a bean, then we want to serialize it as HTML unless it's @Html(format=XML).
978      var type = push2(elementName, o, eType);
979      pop();
980
981      if (type == null)
982         type = object();
983      else if (type.isDelegate())
984         type = ((Delegate)o).getClassMeta();
985      ObjectSwap swap = type.getSwap(this);
986      if (nn(swap)) {
987         o = swap(swap, o);
988         type = swap.getSwapClassMeta(this);
989         if (type.isObject())
990            type = getClassMetaForObject(o);
991      }
992
993      HtmlClassMeta cHtml = getHtmlClassMeta(type);
994
995      if (type.isMapOrBean() && ! cHtml.isXml())
996         return serializeAnything(out, o, eType, elementName, pMeta, 0, false, false);
997
998      return super.serializeAnything(out, o, eType, keyName, elementName, elementNamespace, addNamespaceUris, format, isMixed, preserveWhitespace, pMeta);
999   }
1000}