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}