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