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