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, false);
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 = getHtmlClassMeta(type);
202
203      if (type.isMapOrBean() && ! cHtml.isXml())
204         return serializeAnything(out, o, eType, elementName, pMeta, 0, false, 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    * @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.
221    * @return The type of content encountered.  Either simple (no whitespace) or normal (elements with whitespace).
222    * @throws IOException Thrown by underlying stream.
223    * @throws SerializeException Generic serialization error occurred.
224    */
225   @SuppressWarnings({ "rawtypes", "unchecked" })
226   protected ContentResult serializeAnything(XmlWriter out, Object o,
227         ClassMeta<?> eType, String name, BeanPropertyMeta pMeta, int xIndent, boolean isRoot, boolean nlIfElement) throws IOException, SerializeException {
228
229      ClassMeta<?> aType = null;       // The actual type
230      ClassMeta<?> wType = null;     // The wrapped type (delegate)
231      ClassMeta<?> sType = object();   // The serialized type
232
233      if (eType == null)
234         eType = object();
235
236      aType = push2(name, o, eType);
237
238      // Handle recursion
239      if (aType == null) {
240         o = null;
241         aType = object();
242      }
243
244      // Handle Optional<X>
245      if (isOptional(aType)) {
246         o = getOptionalValue(o);
247         eType = getOptionalType(eType);
248         aType = getClassMetaForObject(o, object());
249      }
250
251      indent += xIndent;
252
253      ContentResult cr = CR_ELEMENTS;
254
255      // Determine the type.
256      if (o == null || (aType.isChar() && ((Character)o).charValue() == 0)) {
257         out.tag("null");
258         cr = ContentResult.CR_MIXED;
259
260      } else {
261
262         if (aType.isDelegate()) {
263            wType = aType;
264            aType = ((Delegate)o).getClassMeta();
265         }
266
267         sType = aType;
268
269         String typeName = null;
270         if (isAddBeanTypes() && ! eType.equals(aType))
271            typeName = aType.getDictionaryName();
272
273         // Swap if necessary
274         PojoSwap swap = aType.getPojoSwap(this);
275         if (swap != null) {
276            o = swap(swap, o);
277            sType = swap.getSwapClassMeta(this);
278
279            // If the getSwapClass() method returns Object, we need to figure out
280            // the actual type now.
281            if (sType.isObject())
282               sType = getClassMetaForObject(o);
283         }
284
285         // Handle the case where we're serializing a raw stream.
286         if (sType.isReader() || sType.isInputStream()) {
287            pop();
288            indent -= xIndent;
289            IOUtils.pipe(o, out);
290            return ContentResult.CR_MIXED;
291         }
292
293         HtmlClassMeta cHtml = getHtmlClassMeta(sType);
294         HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(pMeta);
295
296         HtmlRender render = firstNonNull(bpHtml.getRender(), cHtml.getRender());
297
298         if (render != null) {
299            Object o2 = render.getContent(this, o);
300            if (o2 != o) {
301               indent -= xIndent;
302               pop();
303               out.nl(indent);
304               return serializeAnything(out, o2, null, typeName, null, xIndent, false, false);
305            }
306         }
307
308         if (cHtml.isXml() || bpHtml.isXml()) {
309            pop();
310            indent++;
311            if (nlIfElement)
312               out.nl(0);
313            super.serializeAnything(out, o, null, null, null, false, XmlFormat.MIXED, false, false, null);
314            indent -= xIndent+1;
315            return cr;
316
317         } else if (cHtml.isPlainText() || bpHtml.isPlainText()) {
318            out.write(o == null ? "null" : o.toString());
319            cr = CR_MIXED;
320
321         } else if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) {
322            out.tag("null");
323            cr = CR_MIXED;
324
325         } else if (sType.isNumber()) {
326            if (eType.isNumber() && ! isRoot)
327               out.append(o);
328            else
329               out.sTag("number").append(o).eTag("number");
330            cr = CR_MIXED;
331
332         } else if (sType.isBoolean()) {
333            if (eType.isBoolean() && ! isRoot)
334               out.append(o);
335            else
336               out.sTag("boolean").append(o).eTag("boolean");
337            cr = CR_MIXED;
338
339         } else if (sType.isMap() || (wType != null && wType.isMap())) {
340            out.nlIf(! isRoot, xIndent+1);
341            if (o instanceof BeanMap)
342               serializeBeanMap(out, (BeanMap)o, eType, pMeta);
343            else
344               serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), typeName, pMeta);
345
346         } else if (sType.isBean()) {
347            BeanMap m = toBeanMap(o);
348            if (aType.hasAnnotation(HtmlLink.class)) {
349               HtmlLink h = aType.getAnnotation(HtmlLink.class);
350               Object urlProp = m.get(h.uriProperty());
351               Object nameProp = m.get(h.nameProperty());
352               out.oTag("a").attrUri("href", urlProp).append('>').text(nameProp).eTag("a");
353               cr = CR_MIXED;
354            } else {
355               out.nlIf(! isRoot, xIndent+2);
356               serializeBeanMap(out, m, eType, pMeta);
357            }
358
359         } else if (sType.isCollection() || sType.isArray() || (wType != null && wType.isCollection())) {
360            out.nlIf(! isRoot, xIndent+1);
361            serializeCollection(out, o, sType, eType, name, pMeta);
362
363         } else if (isUri(sType, pMeta, o)) {
364            String label = getAnchorText(pMeta, o);
365            out.oTag("a").attrUri("href", o).append('>');
366            out.text(label);
367            out.eTag("a");
368            cr = CR_MIXED;
369
370         } else {
371            if (isRoot)
372               out.sTag("string").text(toString(o)).eTag("string");
373            else
374               out.text(toString(o));
375            cr = CR_MIXED;
376         }
377      }
378      pop();
379      indent -= xIndent;
380      return cr;
381   }
382
383   @SuppressWarnings({ "rawtypes" })
384   private void serializeMap(XmlWriter out, Map m, ClassMeta<?> sType,
385         ClassMeta<?> eKeyType, ClassMeta<?> eValueType, String typeName, BeanPropertyMeta ppMeta) throws IOException, SerializeException {
386
387      ClassMeta<?> keyType = eKeyType == null ? string() : eKeyType;
388      ClassMeta<?> valueType = eValueType == null ? object() : eValueType;
389      ClassMeta<?> aType = getClassMetaForObject(m);       // The actual type
390      HtmlClassMeta cHtml = getHtmlClassMeta(aType);
391      HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta);
392
393      int i = indent;
394
395      out.oTag(i, "table");
396
397      if (typeName != null && ppMeta != null && ppMeta.getClassMeta() != aType)
398         out.attr(getBeanTypePropertyName(sType), typeName);
399
400      out.append(">").nl(i+1);
401      if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) {
402         out.sTag(i+1, "tr").nl(i+2);
403         out.sTag(i+2, "th").append("key").eTag("th").nl(i+3);
404         out.sTag(i+2, "th").append("value").eTag("th").nl(i+3);
405         out.ie(i+1).eTag("tr").nl(i+2);
406      }
407      for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) {
408
409         Object key = generalize(e.getKey(), keyType);
410         Object value = null;
411         try {
412            value = e.getValue();
413         } catch (StackOverflowError t) {
414            throw t;
415         } catch (Throwable t) {
416            onError(t, "Could not call getValue() on property ''{0}'', {1}", e.getKey(), t.getLocalizedMessage());
417         }
418
419         String link = getLink(ppMeta);
420         String style = getStyle(this, ppMeta, value);
421
422         out.sTag(i+1, "tr").nl(i+2);
423         out.oTag(i+2, "td");
424         if (style != null)
425            out.attr("style", style);
426         out.cTag();
427         if (link != null)
428            out.oTag(i+3, "a").attrUri("href", link.replace("{#}", stringify(value))).cTag();
429         ContentResult cr = serializeAnything(out, key, keyType, null, null, 2, false, false);
430         if (link != null)
431            out.eTag("a");
432         if (cr == CR_ELEMENTS)
433            out.i(i+2);
434         out.eTag("td").nl(i+2);
435         out.sTag(i+2, "td");
436         cr = serializeAnything(out, value, valueType, (key == null ? "_x0000_" : toString(key)), null, 2, false, false);
437         if (cr == CR_ELEMENTS)
438            out.ie(i+2);
439         out.eTag("td").nl(i+2);
440         out.ie(i+1).eTag("tr").nl(i+1);
441      }
442      out.ie(i).eTag("table").nl(i);
443   }
444
445   private void serializeBeanMap(XmlWriter out, BeanMap<?> m, ClassMeta<?> eType, BeanPropertyMeta ppMeta) throws IOException, SerializeException {
446
447      HtmlClassMeta cHtml = getHtmlClassMeta(m.getClassMeta());
448      HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta);
449
450      int i = indent;
451
452      out.oTag(i, "table");
453
454      String typeName = m.getMeta().getDictionaryName();
455      if (typeName != null && eType != m.getClassMeta())
456         out.attr(getBeanTypePropertyName(m.getClassMeta()), typeName);
457
458      out.append('>').nl(i);
459      if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) {
460         out.sTag(i+1, "tr").nl(i+1);
461         out.sTag(i+2, "th").append("key").eTag("th").nl(i+2);
462         out.sTag(i+2, "th").append("value").eTag("th").nl(i+2);
463         out.ie(i+1).eTag("tr").nl(i+1);
464      }
465
466      for (BeanPropertyValue p : m.getValues(isTrimNullProperties())) {
467         BeanPropertyMeta pMeta = p.getMeta();
468         ClassMeta<?> cMeta = p.getClassMeta();
469
470         String key = p.getName();
471         Object value = p.getValue();
472         Throwable t = p.getThrown();
473         if (t != null)
474            onBeanGetterException(pMeta, t);
475
476         if (canIgnoreValue(cMeta, key, value))
477            continue;
478
479         String link = null, anchorText = null;
480         if (! cMeta.isCollectionOrArray()) {
481            link = m.resolveVars(getLink(pMeta));
482            anchorText = m.resolveVars(getAnchorText(pMeta));
483         }
484
485         if (anchorText != null)
486            value = anchorText;
487
488         out.sTag(i+1, "tr").nl(i+1);
489         out.sTag(i+2, "td").text(key).eTag("td").nl(i+2);
490         out.oTag(i+2, "td");
491         String style = getStyle(this, pMeta, value);
492         if (style != null)
493            out.attr("style", style);
494         out.cTag();
495
496         try {
497            if (link != null)
498               out.oTag(i+3, "a").attrUri("href", link).cTag();
499            ContentResult cr = serializeAnything(out, value, cMeta, key, pMeta, 2, false, true);
500            if (cr == CR_ELEMENTS)
501               out.i(i+2);
502            if (link != null)
503               out.eTag("a");
504         } catch (SerializeException e) {
505            throw e;
506         } catch (Error e) {
507            throw e;
508         } catch (Throwable e) {
509            e.printStackTrace();
510            onBeanGetterException(pMeta, e);
511         }
512         out.eTag("td").nl(i+2);
513         out.ie(i+1).eTag("tr").nl(i+1);
514      }
515      out.ie(i).eTag("table").nl(i);
516   }
517
518   @SuppressWarnings({ "rawtypes", "unchecked" })
519   private void serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, ClassMeta<?> eType, String name, BeanPropertyMeta ppMeta) throws IOException, SerializeException {
520
521      HtmlClassMeta cHtml = getHtmlClassMeta(sType);
522      HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta);
523
524      Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in));
525
526      boolean isCdc = cHtml.isHtmlCdc() || bpHtml.isHtmlCdc();
527      boolean isSdc = cHtml.isHtmlSdc() || bpHtml.isHtmlSdc();
528      boolean isDc = isCdc || isSdc;
529
530      int i = indent;
531      if (c.isEmpty()) {
532         out.appendln(i, "<ul></ul>");
533         return;
534      }
535
536      String type2 = null;
537      if (sType != eType)
538         type2 = sType.getDictionaryName();
539      if (type2 == null)
540         type2 = "array";
541
542      c = sort(c);
543
544      String btpn = getBeanTypePropertyName(eType);
545
546      // Look at the objects to see how we're going to handle them.  Check the first object to see how we're going to
547      // handle this.
548      // If it's a map or bean, then we'll create a table.
549      // Otherwise, we'll create a list.
550      Object[] th = getTableHeaders(c, bpHtml);
551
552      if (th != null) {
553
554         out.oTag(i, "table").attr(btpn, type2).append('>').nl(i+1);
555         out.sTag(i+1, "tr").nl(i+2);
556         for (Object key : th) {
557            out.sTag(i+2, "th");
558            out.text(convertToType(key, String.class));
559            out.eTag("th").nl(i+2);
560         }
561         out.ie(i+1).eTag("tr").nl(i+1);
562
563         for (Object o : c) {
564            ClassMeta<?> cm = getClassMetaForObject(o);
565
566            if (cm != null && cm.getPojoSwap(this) != null) {
567               PojoSwap swap = cm.getPojoSwap(this);
568               o = swap(swap, o);
569               cm = swap.getSwapClassMeta(this);
570            }
571
572            out.oTag(i+1, "tr");
573            String typeName = (cm == null ? null : cm.getDictionaryName());
574            String typeProperty = getBeanTypePropertyName(cm);
575
576            if (typeName != null && eType.getElementType() != cm)
577               out.attr(typeProperty, typeName);
578            out.cTag().nl(i+2);
579
580            if (cm == null) {
581               out.i(i+2);
582               serializeAnything(out, o, null, null, null, 1, false, false);
583               out.nl(0);
584
585            } else if (cm.isMap() && ! (cm.isBeanMap())) {
586               Map m2 = sort((Map)o);
587
588               for (Object k : th) {
589                  out.sTag(i+2, "td");
590                  ContentResult cr = serializeAnything(out, m2.get(k), eType.getElementType(), toString(k), null, 2, false, true);
591                  if (cr == CR_ELEMENTS)
592                     out.i(i+2);
593                  out.eTag("td").nl(i+2);
594               }
595            } else {
596               BeanMap m2 = null;
597               if (o instanceof BeanMap)
598                  m2 = (BeanMap)o;
599               else
600                  m2 = toBeanMap(o);
601
602               for (Object k : th) {
603                  BeanMapEntry p = m2.getProperty(toString(k));
604                  BeanPropertyMeta pMeta = p.getMeta();
605                  if (pMeta.canRead()) {
606                     Object value = p.getValue();
607
608                     String link = null, anchorText = null;
609                     if (! pMeta.getClassMeta().isCollectionOrArray()) {
610                        link = m2.resolveVars(getLink(pMeta));
611                        anchorText = m2.resolveVars(getAnchorText(pMeta));
612                     }
613
614                     if (anchorText != null)
615                        value = anchorText;
616
617                     String style = getStyle(this, pMeta, value);
618                     out.oTag(i+2, "td");
619                     if (style != null)
620                        out.attr("style", style);
621                     out.cTag();
622                     if (link != null)
623                        out.oTag("a").attrUri("href", link).cTag();
624                     ContentResult cr = serializeAnything(out, value, pMeta.getClassMeta(), p.getKey().toString(), pMeta, 2, false, true);
625                     if (cr == CR_ELEMENTS)
626                        out.i(i+2);
627                     if (link != null)
628                        out.eTag("a");
629                     out.eTag("td").nl(i+2);
630                  }
631               }
632            }
633            out.ie(i+1).eTag("tr").nl(i+1);
634         }
635         out.ie(i).eTag("table").nl(i);
636
637      } else {
638         out.oTag(i, isDc ? "p" : "ul");
639         if (! type2.equals("array"))
640            out.attr(btpn, type2);
641         out.append('>').nl(i+1);
642         boolean isFirst = true;
643         for (Object o : c) {
644            if (isDc && ! isFirst)
645               out.append(isCdc ? ", " : " ");
646            if (! isDc)
647               out.oTag(i+1, "li");
648            String style = getStyle(this, ppMeta, o);
649            String link = getLink(ppMeta);
650            if (style != null && ! isDc)
651               out.attr("style", style);
652            if (! isDc)
653               out.cTag();
654            if (link != null)
655               out.oTag(i+2, "a").attrUri("href", link.replace("{#}", stringify(o))).cTag();
656            ContentResult cr = serializeAnything(out, o, eType.getElementType(), name, null, 1, false, true);
657            if (link != null)
658               out.eTag("a");
659            if (cr == CR_ELEMENTS)
660               out.ie(i+1);
661            if (! isDc)
662               out.eTag("li").nl(i+1);
663            isFirst = false;
664         }
665         out.ie(i).eTag(isDc ? "p" : "ul").nl(i);
666      }
667   }
668
669   private HtmlRender<?> getRender(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) {
670      if (pMeta == null)
671         return null;
672      HtmlRender<?> render = getHtmlBeanPropertyMeta(pMeta).getRender();
673      if (render != null)
674         return render;
675      ClassMeta<?> cMeta = session.getClassMetaForObject(value);
676      render = cMeta == null ? null : getHtmlClassMeta(cMeta).getRender();
677      return render;
678   }
679
680   @SuppressWarnings({"rawtypes","unchecked"})
681   private String getStyle(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) {
682      HtmlRender render = getRender(session, pMeta, value);
683      return render == null ? null : render.getStyle(session, value);
684   }
685
686   private String getLink(BeanPropertyMeta pMeta) {
687      return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getLink();
688   }
689
690   private String getAnchorText(BeanPropertyMeta pMeta) {
691      return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getAnchorText();
692   }
693
694   /*
695    * Returns the table column headers for the specified collection of objects.
696    * Returns null if collection should not be serialized as a 2-dimensional table.
697    * 2-dimensional tables are used for collections of objects that all have the same set of property names.
698    */
699   @SuppressWarnings({ "rawtypes", "unchecked" })
700   private Object[] getTableHeaders(Collection c, HtmlBeanPropertyMeta bpHtml) throws SerializeException  {
701      if (c.size() == 0)
702         return null;
703      c = sort(c);
704      Object[] th;
705      Set<ClassMeta> prevC = new HashSet<>();
706      Object o1 = null;
707      for (Object o : c)
708         if (o != null) {
709            o1 = o;
710            break;
711         }
712      if (o1 == null)
713         return null;
714      ClassMeta<?> cm = getClassMetaForObject(o1);
715
716      PojoSwap swap = cm.getPojoSwap(this);
717      if (swap != null) {
718         o1 = swap(swap, o1);
719         cm = swap.getSwapClassMeta(this);
720      }
721
722      if (cm == null || ! cm.isMapOrBean())
723         return null;
724      if (cm.hasAnnotation(HtmlLink.class))
725         return null;
726
727      HtmlClassMeta cHtml = getHtmlClassMeta(cm);
728
729      if (cHtml.isNoTables() || bpHtml.isNoTables())
730         return null;
731      if (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())
732         return new Object[0];
733      if (canIgnoreValue(cm, null, o1))
734         return null;
735      if (cm.isMap() && ! cm.isBeanMap()) {
736         Set<Object> set = new LinkedHashSet<>();
737         for (Object o : c) {
738            if (! canIgnoreValue(cm, null, o)) {
739               if (! cm.isInstance(o))
740                  return null;
741               Map m = sort((Map)o);
742               for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) {
743                  if (e.getValue() != null)
744                     set.add(e.getKey() == null ? null : e.getKey());
745               }
746            }
747         }
748         th = set.toArray(new Object[set.size()]);
749      } else {
750         Map<String,Boolean> m = new LinkedHashMap<>();
751         for (Object o : c) {
752            if (! canIgnoreValue(cm, null, o)) {
753               if (! cm.isInstance(o))
754                  return null;
755               BeanMap<?> bm = (o instanceof BeanMap ? (BeanMap)o : toBeanMap(o));
756               for (Map.Entry<String,Object> e : bm.entrySet()) {
757                  String key = e.getKey();
758                  if (e.getValue() != null)
759                     m.put(key, true);
760                  else if (! m.containsKey(key))
761                     m.put(key, false);
762               }
763            }
764         }
765         for (Iterator<Boolean> i = m.values().iterator(); i.hasNext();)
766            if (! i.next())
767               i.remove();
768         th = m.keySet().toArray(new Object[m.size()]);
769      }
770      prevC.add(cm);
771      boolean isSortable = true;
772      for (Object o : th)
773         isSortable &= (o instanceof Comparable);
774      Set<Object> s = (isSortable ? new TreeSet<>() : new LinkedHashSet<>());
775      s.addAll(Arrays.asList(th));
776
777      for (Object o : c) {
778         if (o == null)
779            continue;
780         cm = getClassMetaForObject(o);
781
782         PojoSwap ps = cm == null ? null : cm.getPojoSwap(this);
783         if (ps != null) {
784            o = swap(ps, o);
785            cm = ps.getSwapClassMeta(this);
786         }
787         if (prevC.contains(cm))
788            continue;
789         if (cm == null || ! (cm.isMap() || cm.isBean()))
790            return null;
791         if (cm.hasAnnotation(HtmlLink.class))
792            return null;
793         if (canIgnoreValue(cm, null, o))
794            return null;
795         if (cm.isMap() && ! cm.isBeanMap()) {
796            Map m = (Map)o;
797            if (th.length != m.keySet().size())
798               return null;
799            for (Object k : m.keySet())
800               if (! s.contains(k.toString()))
801                  return null;
802         } else {
803            BeanMap<?> bm = (o instanceof BeanMap ? (BeanMap)o : toBeanMap(o));
804            int l = 0;
805            for (String k : bm.keySet()) {
806               if (! s.contains(k))
807                  return null;
808               l++;
809            }
810            if (s.size() != l)
811               return null;
812         }
813      }
814      return th;
815   }
816
817   //-----------------------------------------------------------------------------------------------------------------
818   // Properties
819   //-----------------------------------------------------------------------------------------------------------------
820
821   /**
822    * Configuration property:  Add <js>"_type"</js> properties when needed.
823    *
824    * @see HtmlSerializer#HTML_addBeanTypes
825    * @return
826    *    <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
827    *    through reflection.
828    */
829   @Override
830   protected final boolean isAddBeanTypes() {
831      return ctx.isAddBeanTypes();
832   }
833
834   /**
835    * Configuration property:  Add key/value headers on bean/map tables.
836    *
837    * @see HtmlSerializer#HTML_addKeyValueTableHeaders
838    * @return
839    *    <jk>true</jk> if <bc>key</bc> and <bc>value</bc> column headers are added to tables.
840    */
841   protected final boolean isAddKeyValueTableHeaders() {
842      return ctx.isAddKeyValueTableHeaders();
843   }
844
845   /**
846    * Configuration property:  Look for link labels in URIs.
847    *
848    * @see HtmlSerializer#HTML_detectLabelParameters
849    * @return
850    *    <jk>true</jk> if we should look for URL label parameters (e.g. <js>"?label=foobar"</js>).
851    */
852   protected final boolean isDetectLabelParameters() {
853      return ctx.isDetectLabelParameters();
854   }
855
856   /**
857    * Configuration property:  Look for URLs in {@link String Strings}.
858    *
859    * @see HtmlSerializer#HTML_detectLinksInStrings
860    * @return
861    *    <jk>true</jk> if we should automatically convert strings to URLs if they look like a URL.
862    */
863   protected final boolean isDetectLinksInStrings() {
864      return ctx.isDetectLinksInStrings();
865   }
866
867   /**
868    * Configuration property:  Link label parameter name.
869    *
870    * @see HtmlSerializer#HTML_labelParameter
871    * @return
872    *    The parameter name to look for when resolving link labels via {@link HtmlSerializer#HTML_detectLabelParameters}.
873    */
874   protected final String getLabelParameter() {
875      return ctx.getLabelParameter();
876   }
877
878   /**
879    * Configuration property:  Anchor text source.
880    *
881    * @see HtmlSerializer#HTML_uriAnchorText
882    * @return
883    *    When creating anchor tags (e.g. <code><xt>&lt;a</xt> <xa>href</xa>=<xs>'...'</xs>
884    *    <xt>&gt;</xt>text<xt>&lt;/a&gt;</xt></code>) in HTML, this setting defines what to set the inner text to.
885    */
886   protected final AnchorText getUriAnchorText() {
887      return ctx.getUriAnchorText();
888   }
889
890   //-----------------------------------------------------------------------------------------------------------------
891   // Extended metadata
892   //-----------------------------------------------------------------------------------------------------------------
893
894   /**
895    * Returns the language-specific metadata on the specified class.
896    *
897    * @param cm The class to return the metadata on.
898    * @return The metadata.
899    */
900   protected HtmlClassMeta getHtmlClassMeta(ClassMeta<?> cm) {
901      return ctx.getHtmlClassMeta(cm);
902   }
903
904   /**
905    * Returns the language-specific metadata on the specified bean property.
906    *
907    * @param bpm The bean property to return the metadata on.
908    * @return The metadata.
909    */
910   protected HtmlBeanPropertyMeta getHtmlBeanPropertyMeta(BeanPropertyMeta bpm) {
911      return ctx.getHtmlBeanPropertyMeta(bpm);
912   }
913
914   //-----------------------------------------------------------------------------------------------------------------
915   // Other methods
916   //-----------------------------------------------------------------------------------------------------------------
917
918   @Override /* Session */
919   public ObjectMap toMap() {
920      return super.toMap()
921         .append("HtmlSerializerSession", new DefaultFilteringObjectMap()
922      );
923   }
924}