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