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