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