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