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 e) {
679            throw e;
680         } catch (Error e) {
681            throw e;
682         } catch (Throwable e) {
683            onBeanGetterException(pMeta, e);
684         }
685         out.eTag("td").nl(i+2);
686         out.ie(i+1).eTag("tr").nl(i+1);
687      });
688
689      out.ie(i).eTag("table").nl(i);
690   }
691
692   @SuppressWarnings({ "rawtypes", "unchecked" })
693   private void serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, ClassMeta<?> eType, String name, BeanPropertyMeta ppMeta) throws SerializeException {
694
695      HtmlClassMeta cHtml = getHtmlClassMeta(sType);
696      HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta);
697
698      Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in));
699
700      boolean isCdc = cHtml.isHtmlCdc() || bpHtml.isHtmlCdc();
701      boolean isSdc = cHtml.isHtmlSdc() || bpHtml.isHtmlSdc();
702      boolean isDc = isCdc || isSdc;
703
704      int i = indent;
705      if (c.isEmpty()) {
706         out.appendln(i, "<ul></ul>");
707         return;
708      }
709
710      String type2 = null;
711      if (sType != eType)
712         type2 = sType.getDictionaryName();
713      if (type2 == null)
714         type2 = "array";
715
716      c = sort(c);
717
718      String btpn = getBeanTypePropertyName(eType);
719
720      // Look at the objects to see how we're going to handle them.  Check the first object to see how we're going to
721      // handle this.
722      // If it's a map or bean, then we'll create a table.
723      // Otherwise, we'll create a list.
724      Object[] th = getTableHeaders(c, bpHtml);
725
726      if (th != null) {
727
728         out.oTag(i, "table").attr(btpn, type2).w('>').nl(i+1);
729         if (th.length > 0) {
730            out.sTag(i+1, "tr").nl(i+2);
731            for (Object key : th) {
732               out.sTag(i+2, "th");
733               out.text(convertToType(key, String.class));
734               out.eTag("th").nl(i+2);
735            }
736            out.ie(i+1).eTag("tr").nl(i+1);
737         } else {
738            th = null;
739         }
740
741         for (Object o : c) {
742            ClassMeta<?> cm = getClassMetaForObject(o);
743
744            if (cm != null && cm.getSwap(this) != null) {
745               ObjectSwap swap = cm.getSwap(this);
746               o = swap(swap, o);
747               cm = swap.getSwapClassMeta(this);
748            }
749
750            out.oTag(i+1, "tr");
751            String typeName = (cm == null ? null : cm.getDictionaryName());
752            String typeProperty = getBeanTypePropertyName(cm);
753
754            if (typeName != null && eType.getElementType() != cm)
755               out.attr(typeProperty, typeName);
756            out.cTag().nl(i+2);
757
758            if (cm == null) {
759               out.i(i+2);
760               serializeAnything(out, o, null, null, null, 1, false, false);
761               out.nl(0);
762
763            } else if (cm.isMap() && ! (cm.isBeanMap())) {
764               Map m2 = sort((Map)o);
765
766               if (th == null)
767                  th = m2.keySet().toArray(new Object[m2.size()]);
768
769               for (Object k : th) {
770                  out.sTag(i+2, "td");
771                  ContentResult cr = serializeAnything(out, m2.get(k), eType.getElementType(), toString(k), null, 2, false, true);
772                  if (cr == CR_ELEMENTS)
773                     out.i(i+2);
774                  out.eTag("td").nl(i+2);
775               }
776            } else {
777               BeanMap m2 = toBeanMap(o);
778
779               if (th == null)
780                  th = m2.keySet().toArray(new Object[m2.size()]);
781
782               for (Object k : th) {
783                  BeanMapEntry p = m2.getProperty(toString(k));
784                  BeanPropertyMeta pMeta = p.getMeta();
785                  if (pMeta.canRead()) {
786                     Object value = p.getValue();
787
788                     String link = null, anchorText = null;
789                     if (! pMeta.getClassMeta().isCollectionOrArray()) {
790                        link = m2.resolveVars(getLink(pMeta));
791                        anchorText = m2.resolveVars(getAnchorText(pMeta));
792                     }
793
794                     if (anchorText != null)
795                        value = anchorText;
796
797                     String style = getStyle(this, pMeta, value);
798                     out.oTag(i+2, "td");
799                     if (style != null)
800                        out.attr("style", style);
801                     out.cTag();
802                     if (link != null)
803                        out.oTag("a").attrUri("href", link).cTag();
804                     ContentResult cr = serializeAnything(out, value, pMeta.getClassMeta(), p.getKey().toString(), pMeta, 2, false, true);
805                     if (cr == CR_ELEMENTS)
806                        out.i(i+2);
807                     if (link != null)
808                        out.eTag("a");
809                     out.eTag("td").nl(i+2);
810                  }
811               }
812            }
813            out.ie(i+1).eTag("tr").nl(i+1);
814         }
815         out.ie(i).eTag("table").nl(i);
816
817      } else {
818         out.oTag(i, isDc ? "p" : "ul");
819         if (! type2.equals("array"))
820            out.attr(btpn, type2);
821         out.w('>').nl(i+1);
822         boolean isFirst = true;
823         for (Object o : c) {
824            if (isDc && ! isFirst)
825               out.append(isCdc ? ", " : " ");
826            if (! isDc)
827               out.oTag(i+1, "li");
828            String style = getStyle(this, ppMeta, o);
829            String link = getLink(ppMeta);
830            if (style != null && ! isDc)
831               out.attr("style", style);
832            if (! isDc)
833               out.cTag();
834            if (link != null)
835               out.oTag(i+2, "a").attrUri("href", link.replace("{#}", stringify(o))).cTag();
836            ContentResult cr = serializeAnything(out, o, eType.getElementType(), name, null, 1, false, true);
837            if (link != null)
838               out.eTag("a");
839            if (cr == CR_ELEMENTS)
840               out.ie(i+1);
841            if (! isDc)
842               out.eTag("li").nl(i+1);
843            isFirst = false;
844         }
845         out.ie(i).eTag(isDc ? "p" : "ul").nl(i);
846      }
847   }
848
849   private HtmlRender<?> getRender(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) {
850      if (pMeta == null)
851         return null;
852      HtmlRender<?> render = getHtmlBeanPropertyMeta(pMeta).getRender();
853      if (render != null)
854         return render;
855      ClassMeta<?> cMeta = session.getClassMetaForObject(value);
856      render = cMeta == null ? null : getHtmlClassMeta(cMeta).getRender();
857      return render;
858   }
859
860   @SuppressWarnings({"rawtypes","unchecked"})
861   private String getStyle(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) {
862      HtmlRender render = getRender(session, pMeta, value);
863      return render == null ? null : render.getStyle(session, value);
864   }
865
866   private String getLink(BeanPropertyMeta pMeta) {
867      return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getLink();
868   }
869
870   private String getAnchorText(BeanPropertyMeta pMeta) {
871      return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getAnchorText();
872   }
873
874   /*
875    * Returns the table column headers for the specified collection of objects.
876    * Returns null if collection should not be serialized as a 2-dimensional table.
877    * Returns an empty array if it should be treated as a table but without headers.
878    * 2-dimensional tables are used for collections of objects that all have the same set of property names.
879    */
880   @SuppressWarnings({ "rawtypes", "unchecked" })
881   private Object[] getTableHeaders(Collection c, HtmlBeanPropertyMeta bpHtml) throws SerializeException  {
882
883      if (c.size() == 0)
884         return null;
885
886      c = sort(c);
887
888      Object o1 = null;
889      for (Object o : c)
890         if (o != null) {
891            o1 = o;
892            break;
893         }
894      if (o1 == null)
895         return null;
896
897      ClassMeta<?> cm1 = getClassMetaForObject(o1);
898
899      ObjectSwap swap = cm1.getSwap(this);
900      o1 = swap(swap, o1);
901      if (swap != null)
902         cm1 = swap.getSwapClassMeta(this);
903
904      if (cm1 == null || ! cm1.isMapOrBean() || cm1.hasAnnotation(HtmlLink.class))
905         return null;
906
907      HtmlClassMeta cHtml = getHtmlClassMeta(cm1);
908
909      if (cHtml.isNoTables() || bpHtml.isNoTables() || cHtml.isXml() || bpHtml.isXml() || canIgnoreValue(cm1, null, o1))
910         return null;
911
912      if (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())
913         return new Object[0];
914
915      // If it's a non-bean map, only use table if all entries are also maps.
916      if (cm1.isMap() && ! cm1.isBeanMap()) {
917
918         Set<Object> set = CollectionUtils.set();
919         for (Object o : c) {
920            o = swap(swap, o);
921            if (! canIgnoreValue(cm1, null, o)) {
922               if (! cm1.isInstance(o))
923                  return null;
924               forEachEntry((Map)o, x -> set.add(x.getKey()));
925            }
926         }
927         return set.toArray(new Object[set.size()]);
928      }
929
930      // Must be a bean or BeanMap.
931      for (Object o : c) {
932         o = swap(swap, o);
933         if (! canIgnoreValue(cm1, null, o)) {
934            if (! cm1.isInstance(o))
935               return null;
936         }
937      }
938
939      BeanMap<?> bm = toBeanMap(o1);
940      return bm.keySet().toArray(new String[bm.size()]);
941   }
942
943   //-----------------------------------------------------------------------------------------------------------------
944   // Properties
945   //-----------------------------------------------------------------------------------------------------------------
946
947   /**
948    * Add <js>"_type"</js> properties when needed.
949    *
950    * @see HtmlSerializer.Builder#addBeanTypesHtml()
951    * @return
952    *    <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
953    *    through reflection.
954    */
955   @Override
956   protected final boolean isAddBeanTypes() {
957      return ctx.isAddBeanTypes();
958   }
959
960   /**
961    * Add key/value headers on bean/map tables.
962    *
963    * @see HtmlSerializer.Builder#addKeyValueTableHeaders()
964    * @return
965    *    <jk>true</jk> if <bc>key</bc> and <bc>value</bc> column headers are added to tables.
966    */
967   protected final boolean isAddKeyValueTableHeaders() {
968      return ctx.isAddKeyValueTableHeaders();
969   }
970
971   /**
972    * Look for link labels in URIs.
973    *
974    * @see HtmlSerializer.Builder#disableDetectLabelParameters()
975    * @return
976    *    <jk>true</jk> if we should ook for URL label parameters (e.g. <js>"?label=foobar"</js>).
977    */
978   protected final boolean isDetectLabelParameters() {
979      return ctx.isDetectLabelParameters();
980   }
981
982   /**
983    * Look for URLs in {@link String Strings}.
984    *
985    * @see HtmlSerializer.Builder#disableDetectLinksInStrings()
986    * @return
987    *    <jk>true</jk> if we should automatically convert strings to URLs if they look like a URL.
988    */
989   protected final boolean isDetectLinksInStrings() {
990      return ctx.isDetectLinksInStrings();
991   }
992
993   /**
994    * Link label parameter name.
995    *
996    * @see HtmlSerializer.Builder#labelParameter(String)
997    * @return
998    *    The parameter name to look for when resolving link labels.
999    */
1000   protected final String getLabelParameter() {
1001      return ctx.getLabelParameter();
1002   }
1003
1004   /**
1005    * Anchor text source.
1006    *
1007    * @see HtmlSerializer.Builder#uriAnchorText(AnchorText)
1008    * @return
1009    *    When creating anchor tags (e.g. <code><xt>&lt;a</xt> <xa>href</xa>=<xs>'...'</xs>
1010    *    <xt>&gt;</xt>text<xt>&lt;/a&gt;</xt></code>) in HTML, this setting defines what to set the inner text to.
1011    */
1012   protected final AnchorText getUriAnchorText() {
1013      return ctx.getUriAnchorText();
1014   }
1015
1016   //-----------------------------------------------------------------------------------------------------------------
1017   // Extended metadata
1018   //-----------------------------------------------------------------------------------------------------------------
1019
1020   /**
1021    * Returns the language-specific metadata on the specified class.
1022    *
1023    * @param cm The class to return the metadata on.
1024    * @return The metadata.
1025    */
1026   protected HtmlClassMeta getHtmlClassMeta(ClassMeta<?> cm) {
1027      return ctx.getHtmlClassMeta(cm);
1028   }
1029
1030   /**
1031    * Returns the language-specific metadata on the specified bean property.
1032    *
1033    * @param bpm The bean property to return the metadata on.
1034    * @return The metadata.
1035    */
1036   protected HtmlBeanPropertyMeta getHtmlBeanPropertyMeta(BeanPropertyMeta bpm) {
1037      return ctx.getHtmlBeanPropertyMeta(bpm);
1038   }
1039}