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