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 indent += xIndent; 244 245 ContentResult cr = CR_ELEMENTS; 246 247 // Determine the type. 248 if (o == null || (aType.isChar() && ((Character)o).charValue() == 0)) { 249 out.tag("null"); 250 cr = ContentResult.CR_MIXED; 251 252 } else { 253 254 if (aType.isDelegate()) { 255 wType = aType; 256 aType = ((Delegate)o).getClassMeta(); 257 } 258 259 sType = aType; 260 261 String typeName = null; 262 if (isAddBeanTypes() && ! eType.equals(aType)) 263 typeName = aType.getDictionaryName(); 264 265 // Swap if necessary 266 PojoSwap swap = aType.getPojoSwap(this); 267 if (swap != null) { 268 o = swap(swap, o); 269 sType = swap.getSwapClassMeta(this); 270 271 // If the getSwapClass() method returns Object, we need to figure out 272 // the actual type now. 273 if (sType.isObject()) 274 sType = getClassMetaForObject(o); 275 } 276 277 // Handle the case where we're serializing a raw stream. 278 if (sType.isReader() || sType.isInputStream()) { 279 pop(); 280 indent -= xIndent; 281 IOUtils.pipe(o, out); 282 return ContentResult.CR_MIXED; 283 } 284 285 HtmlClassMeta cHtml = cHtml(sType); 286 HtmlBeanPropertyMeta bpHtml = bpHtml(pMeta); 287 288 HtmlRender render = firstNonNull(bpHtml.getRender(), cHtml.getRender()); 289 290 if (render != null) { 291 Object o2 = render.getContent(this, o); 292 if (o2 != o) { 293 indent -= xIndent; 294 pop(); 295 out.nl(indent); 296 return serializeAnything(out, o2, null, typeName, null, xIndent, false); 297 } 298 } 299 300 if (cHtml.isXml() || bpHtml.isXml()) { 301 pop(); 302 indent++; 303 super.serializeAnything(out, o, null, null, null, false, XmlFormat.MIXED, false, false, null); 304 indent -= xIndent+1; 305 return cr; 306 307 } else if (cHtml.isPlainText() || bpHtml.isPlainText()) { 308 out.write(o == null ? "null" : o.toString()); 309 cr = CR_MIXED; 310 311 } else if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) { 312 out.tag("null"); 313 cr = CR_MIXED; 314 315 } else if (sType.isNumber()) { 316 if (eType.isNumber() && ! isRoot) 317 out.append(o); 318 else 319 out.sTag("number").append(o).eTag("number"); 320 cr = CR_MIXED; 321 322 } else if (sType.isBoolean()) { 323 if (eType.isBoolean() && ! isRoot) 324 out.append(o); 325 else 326 out.sTag("boolean").append(o).eTag("boolean"); 327 cr = CR_MIXED; 328 329 } else if (sType.isMap() || (wType != null && wType.isMap())) { 330 out.nlIf(! isRoot, xIndent+1); 331 if (o instanceof BeanMap) 332 serializeBeanMap(out, (BeanMap)o, eType, pMeta); 333 else 334 serializeMap(out, (Map)o, sType, eType.getKeyType(), eType.getValueType(), typeName, pMeta); 335 336 } else if (sType.isBean()) { 337 BeanMap m = toBeanMap(o); 338 Class<?> c = o.getClass(); 339 if (c.isAnnotationPresent(HtmlLink.class)) { 340 HtmlLink h = o.getClass().getAnnotation(HtmlLink.class); 341 Object urlProp = m.get(h.uriProperty()); 342 Object nameProp = m.get(h.nameProperty()); 343 out.oTag("a").attrUri("href", urlProp).append('>').text(nameProp).eTag("a"); 344 cr = CR_MIXED; 345 } else { 346 out.nlIf(! isRoot, xIndent+2); 347 serializeBeanMap(out, m, eType, pMeta); 348 } 349 350 } else if (sType.isCollection() || sType.isArray() || (wType != null && wType.isCollection())) { 351 out.nlIf(! isRoot, xIndent+1); 352 serializeCollection(out, o, sType, eType, name, pMeta); 353 354 } else if (isUri(sType, pMeta, o)) { 355 String label = getAnchorText(pMeta, o); 356 out.oTag("a").attrUri("href", o).append('>'); 357 out.text(label); 358 out.eTag("a"); 359 cr = CR_MIXED; 360 361 } else { 362 if (isRoot) 363 out.sTag("string").text(toString(o)).eTag("string"); 364 else 365 out.text(toString(o)); 366 cr = CR_MIXED; 367 } 368 } 369 pop(); 370 indent -= xIndent; 371 return cr; 372 } 373 374 @SuppressWarnings({ "rawtypes" }) 375 private void serializeMap(XmlWriter out, Map m, ClassMeta<?> sType, 376 ClassMeta<?> eKeyType, ClassMeta<?> eValueType, String typeName, BeanPropertyMeta ppMeta) throws IOException, SerializeException { 377 378 ClassMeta<?> keyType = eKeyType == null ? string() : eKeyType; 379 ClassMeta<?> valueType = eValueType == null ? object() : eValueType; 380 ClassMeta<?> aType = getClassMetaForObject(m); // The actual type 381 HtmlClassMeta cHtml = cHtml(aType); 382 HtmlBeanPropertyMeta bpHtml = bpHtml(ppMeta); 383 384 int i = indent; 385 386 out.oTag(i, "table"); 387 388 if (typeName != null && ppMeta != null && ppMeta.getClassMeta() != aType) 389 out.attr(getBeanTypePropertyName(sType), typeName); 390 391 out.append(">").nl(i+1); 392 if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) { 393 out.sTag(i+1, "tr").nl(i+2); 394 out.sTag(i+2, "th").append("key").eTag("th").nl(i+3); 395 out.sTag(i+2, "th").append("value").eTag("th").nl(i+3); 396 out.ie(i+1).eTag("tr").nl(i+2); 397 } 398 for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) { 399 400 Object key = generalize(e.getKey(), keyType); 401 Object value = null; 402 try { 403 value = e.getValue(); 404 } catch (StackOverflowError t) { 405 throw t; 406 } catch (Throwable t) { 407 onError(t, "Could not call getValue() on property ''{0}'', {1}", e.getKey(), t.getLocalizedMessage()); 408 } 409 410 String link = getLink(ppMeta); 411 String style = getStyle(this, ppMeta, value); 412 413 out.sTag(i+1, "tr").nl(i+2); 414 out.oTag(i+2, "td"); 415 if (style != null) 416 out.attr("style", style); 417 out.cTag(); 418 if (link != null) 419 out.oTag(i+3, "a").attrUri("href", link.replace("{#}", stringify(value))).cTag(); 420 ContentResult cr = serializeAnything(out, key, keyType, null, null, 2, false); 421 if (link != null) 422 out.eTag("a"); 423 if (cr == CR_ELEMENTS) 424 out.i(i+2); 425 out.eTag("td").nl(i+2); 426 out.sTag(i+2, "td"); 427 cr = serializeAnything(out, value, valueType, (key == null ? "_x0000_" : toString(key)), null, 2, false); 428 if (cr == CR_ELEMENTS) 429 out.ie(i+2); 430 out.eTag("td").nl(i+2); 431 out.ie(i+1).eTag("tr").nl(i+1); 432 } 433 out.ie(i).eTag("table").nl(i); 434 } 435 436 private void serializeBeanMap(XmlWriter out, BeanMap<?> m, ClassMeta<?> eType, BeanPropertyMeta ppMeta) throws IOException, SerializeException { 437 438 HtmlClassMeta cHtml = cHtml(m.getClassMeta()); 439 HtmlBeanPropertyMeta bpHtml = bpHtml(ppMeta); 440 441 int i = indent; 442 443 out.oTag(i, "table"); 444 445 String typeName = m.getMeta().getDictionaryName(); 446 if (typeName != null && eType != m.getClassMeta()) 447 out.attr(getBeanTypePropertyName(m.getClassMeta()), typeName); 448 449 out.append('>').nl(i); 450 if (isAddKeyValueTableHeaders() && ! (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders())) { 451 out.sTag(i+1, "tr").nl(i+1); 452 out.sTag(i+2, "th").append("key").eTag("th").nl(i+2); 453 out.sTag(i+2, "th").append("value").eTag("th").nl(i+2); 454 out.ie(i+1).eTag("tr").nl(i+1); 455 } 456 457 for (BeanPropertyValue p : m.getValues(isTrimNullProperties())) { 458 BeanPropertyMeta pMeta = p.getMeta(); 459 ClassMeta<?> cMeta = p.getClassMeta(); 460 461 String key = p.getName(); 462 Object value = p.getValue(); 463 Throwable t = p.getThrown(); 464 if (t != null) 465 onBeanGetterException(pMeta, t); 466 467 if (canIgnoreValue(cMeta, key, value)) 468 continue; 469 470 String link = null, anchorText = null; 471 if (! cMeta.isCollectionOrArray()) { 472 link = m.resolveVars(getLink(pMeta)); 473 anchorText = m.resolveVars(getAnchorText(pMeta)); 474 } 475 476 if (anchorText != null) 477 value = anchorText; 478 479 out.sTag(i+1, "tr").nl(i+1); 480 out.sTag(i+2, "td").text(key).eTag("td").nl(i+2); 481 out.oTag(i+2, "td"); 482 String style = getStyle(this, pMeta, value); 483 if (style != null) 484 out.attr("style", style); 485 out.cTag(); 486 487 try { 488 if (link != null) 489 out.oTag(i+3, "a").attrUri("href", link).cTag(); 490 ContentResult cr = serializeAnything(out, value, cMeta, key, pMeta, 2, false); 491 if (cr == CR_ELEMENTS) 492 out.i(i+2); 493 if (link != null) 494 out.eTag("a"); 495 } catch (SerializeException e) { 496 throw e; 497 } catch (Error e) { 498 throw e; 499 } catch (Throwable e) { 500 e.printStackTrace(); 501 onBeanGetterException(pMeta, e); 502 } 503 out.eTag("td").nl(i+2); 504 out.ie(i+1).eTag("tr").nl(i+1); 505 } 506 out.ie(i).eTag("table").nl(i); 507 } 508 509 @SuppressWarnings({ "rawtypes", "unchecked" }) 510 private void serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, ClassMeta<?> eType, String name, BeanPropertyMeta ppMeta) throws IOException, SerializeException { 511 512 HtmlClassMeta cHtml = cHtml(sType); 513 HtmlBeanPropertyMeta bpHtml = bpHtml(ppMeta); 514 515 Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in)); 516 517 boolean isCdc = cHtml.isHtmlCdc() || bpHtml.isHtmlCdc(); 518 boolean isSdc = cHtml.isHtmlSdc() || bpHtml.isHtmlSdc(); 519 boolean isDc = isCdc || isSdc; 520 521 int i = indent; 522 if (c.isEmpty()) { 523 out.appendln(i, "<ul></ul>"); 524 return; 525 } 526 527 String type2 = null; 528 if (sType != eType) 529 type2 = sType.getDictionaryName(); 530 if (type2 == null) 531 type2 = "array"; 532 533 c = sort(c); 534 535 String btpn = getBeanTypePropertyName(eType); 536 537 // Look at the objects to see how we're going to handle them. Check the first object to see how we're going to 538 // handle this. 539 // If it's a map or bean, then we'll create a table. 540 // Otherwise, we'll create a list. 541 Object[] th = getTableHeaders(c, bpHtml); 542 543 if (th != null) { 544 545 out.oTag(i, "table").attr(btpn, type2).append('>').nl(i+1); 546 out.sTag(i+1, "tr").nl(i+2); 547 for (Object key : th) { 548 out.sTag(i+2, "th"); 549 out.text(convertToType(key, String.class)); 550 out.eTag("th").nl(i+2); 551 } 552 out.ie(i+1).eTag("tr").nl(i+1); 553 554 for (Object o : c) { 555 ClassMeta<?> cm = getClassMetaForObject(o); 556 557 if (cm != null && cm.getPojoSwap(this) != null) { 558 PojoSwap swap = cm.getPojoSwap(this); 559 o = swap(swap, o); 560 cm = swap.getSwapClassMeta(this); 561 } 562 563 out.oTag(i+1, "tr"); 564 String typeName = (cm == null ? null : cm.getDictionaryName()); 565 String typeProperty = getBeanTypePropertyName(cm); 566 567 if (typeName != null && eType.getElementType() != cm) 568 out.attr(typeProperty, typeName); 569 out.cTag().nl(i+2); 570 571 if (cm == null) { 572 out.i(i+2); 573 serializeAnything(out, o, null, null, null, 1, false); 574 out.nl(0); 575 576 } else if (cm.isMap() && ! (cm.isBeanMap())) { 577 Map m2 = sort((Map)o); 578 579 for (Object k : th) { 580 out.sTag(i+2, "td"); 581 ContentResult cr = serializeAnything(out, m2.get(k), eType.getElementType(), toString(k), null, 2, false); 582 if (cr == CR_ELEMENTS) 583 out.i(i+2); 584 out.eTag("td").nl(i+2); 585 } 586 } else { 587 BeanMap m2 = null; 588 if (o instanceof BeanMap) 589 m2 = (BeanMap)o; 590 else 591 m2 = toBeanMap(o); 592 593 for (Object k : th) { 594 BeanMapEntry p = m2.getProperty(toString(k)); 595 BeanPropertyMeta pMeta = p.getMeta(); 596 if (pMeta.canRead()) { 597 Object value = p.getValue(); 598 599 String link = null, anchorText = null; 600 if (! pMeta.getClassMeta().isCollectionOrArray()) { 601 link = m2.resolveVars(getLink(pMeta)); 602 anchorText = m2.resolveVars(getAnchorText(pMeta)); 603 } 604 605 if (anchorText != null) 606 value = anchorText; 607 608 String style = getStyle(this, pMeta, value); 609 out.oTag(i+2, "td"); 610 if (style != null) 611 out.attr("style", style); 612 out.cTag(); 613 if (link != null) 614 out.oTag("a").attrUri("href", link).cTag(); 615 ContentResult cr = serializeAnything(out, value, pMeta.getClassMeta(), p.getKey().toString(), pMeta, 2, false); 616 if (cr == CR_ELEMENTS) 617 out.i(i+2); 618 if (link != null) 619 out.eTag("a"); 620 out.eTag("td").nl(i+2); 621 } 622 } 623 } 624 out.ie(i+1).eTag("tr").nl(i+1); 625 } 626 out.ie(i).eTag("table").nl(i); 627 628 } else { 629 out.oTag(i, isDc ? "p" : "ul"); 630 if (! type2.equals("array")) 631 out.attr(btpn, type2); 632 out.append('>').nl(i+1); 633 boolean isFirst = true; 634 for (Object o : c) { 635 if (isDc && ! isFirst) 636 out.append(isCdc ? ", " : " "); 637 if (! isDc) 638 out.oTag(i+1, "li"); 639 String style = getStyle(this, ppMeta, o); 640 String link = getLink(ppMeta); 641 if (style != null && ! isDc) 642 out.attr("style", style); 643 if (! isDc) 644 out.cTag(); 645 if (link != null) 646 out.oTag(i+2, "a").attrUri("href", link.replace("{#}", stringify(o))).cTag(); 647 ContentResult cr = serializeAnything(out, o, eType.getElementType(), name, null, 1, false); 648 if (link != null) 649 out.eTag("a"); 650 if (cr == CR_ELEMENTS) 651 out.ie(i+1); 652 if (! isDc) 653 out.eTag("li").nl(i+1); 654 isFirst = false; 655 } 656 out.ie(i).eTag(isDc ? "p" : "ul").nl(i); 657 } 658 } 659 660 private static HtmlRender<?> getRender(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) { 661 if (pMeta == null) 662 return null; 663 HtmlRender<?> render = bpHtml(pMeta).getRender(); 664 if (render != null) 665 return render; 666 ClassMeta<?> cMeta = session.getClassMetaForObject(value); 667 render = cMeta == null ? null : cHtml(cMeta).getRender(); 668 return render; 669 } 670 671 @SuppressWarnings({"rawtypes","unchecked"}) 672 private static String getStyle(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) { 673 HtmlRender render = getRender(session, pMeta, value); 674 return render == null ? null : render.getStyle(session, value); 675 } 676 677 private static String getLink(BeanPropertyMeta pMeta) { 678 return pMeta == null ? null : pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class).getLink(); 679 } 680 681 private static String getAnchorText(BeanPropertyMeta pMeta) { 682 return pMeta == null ? null : pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class).getAnchorText(); 683 } 684 685 private static HtmlClassMeta cHtml(ClassMeta<?> cm) { 686 return cm.getExtendedMeta(HtmlClassMeta.class); 687 } 688 689 private static HtmlBeanPropertyMeta bpHtml(BeanPropertyMeta pMeta) { 690 return pMeta == null ? HtmlBeanPropertyMeta.DEFAULT : pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class); 691 } 692 693 /* 694 * Returns the table column headers for the specified collection of objects. 695 * Returns null if collection should not be serialized as a 2-dimensional table. 696 * 2-dimensional tables are used for collections of objects that all have the same set of property names. 697 */ 698 @SuppressWarnings({ "rawtypes", "unchecked" }) 699 private Object[] getTableHeaders(Collection c, HtmlBeanPropertyMeta bpHtml) throws SerializeException { 700 if (c.size() == 0) 701 return null; 702 c = sort(c); 703 Object[] th; 704 Set<ClassMeta> prevC = new HashSet<>(); 705 Object o1 = null; 706 for (Object o : c) 707 if (o != null) { 708 o1 = o; 709 break; 710 } 711 if (o1 == null) 712 return null; 713 ClassMeta<?> cm = getClassMetaForObject(o1); 714 715 PojoSwap swap = cm.getPojoSwap(this); 716 if (swap != null) { 717 o1 = swap(swap, o1); 718 cm = swap.getSwapClassMeta(this); 719 } 720 721 if (cm == null || ! cm.isMapOrBean()) 722 return null; 723 if (cm.getInnerClass().isAnnotationPresent(HtmlLink.class)) 724 return null; 725 726 HtmlClassMeta cHtml = cHtml(cm); 727 728 if (cHtml.isNoTables() || bpHtml.isNoTables()) 729 return null; 730 if (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders()) 731 return new Object[0]; 732 if (canIgnoreValue(cm, null, o1)) 733 return null; 734 if (cm.isMap() && ! cm.isBeanMap()) { 735 Set<Object> set = new LinkedHashSet<>(); 736 for (Object o : c) { 737 if (! canIgnoreValue(cm, null, o)) { 738 if (! cm.isInstance(o)) 739 return null; 740 Map m = sort((Map)o); 741 for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) { 742 if (e.getValue() != null) 743 set.add(e.getKey() == null ? null : e.getKey()); 744 } 745 } 746 } 747 th = set.toArray(new Object[set.size()]); 748 } else { 749 Map<String,Boolean> m = new LinkedHashMap<>(); 750 for (Object o : c) { 751 if (! canIgnoreValue(cm, null, o)) { 752 if (! cm.isInstance(o)) 753 return null; 754 BeanMap<?> bm = (o instanceof BeanMap ? (BeanMap)o : toBeanMap(o)); 755 for (Map.Entry<String,Object> e : bm.entrySet()) { 756 String key = e.getKey(); 757 if (e.getValue() != null) 758 m.put(key, true); 759 else if (! m.containsKey(key)) 760 m.put(key, false); 761 } 762 } 763 } 764 for (Iterator<Boolean> i = m.values().iterator(); i.hasNext();) 765 if (! i.next()) 766 i.remove(); 767 th = m.keySet().toArray(new Object[m.size()]); 768 } 769 prevC.add(cm); 770 boolean isSortable = true; 771 for (Object o : th) 772 isSortable &= (o instanceof Comparable); 773 Set<Object> s = (isSortable ? new TreeSet<>() : new LinkedHashSet<>()); 774 s.addAll(Arrays.asList(th)); 775 776 for (Object o : c) { 777 if (o == null) 778 continue; 779 cm = getClassMetaForObject(o); 780 781 PojoSwap ps = cm == null ? null : cm.getPojoSwap(this); 782 if (ps != null) { 783 o = swap(ps, o); 784 cm = ps.getSwapClassMeta(this); 785 } 786 if (prevC.contains(cm)) 787 continue; 788 if (cm == null || ! (cm.isMap() || cm.isBean())) 789 return null; 790 if (cm.getInnerClass().isAnnotationPresent(HtmlLink.class)) 791 return null; 792 if (canIgnoreValue(cm, null, o)) 793 return null; 794 if (cm.isMap() && ! cm.isBeanMap()) { 795 Map m = (Map)o; 796 if (th.length != m.keySet().size()) 797 return null; 798 for (Object k : m.keySet()) 799 if (! s.contains(k.toString())) 800 return null; 801 } else { 802 BeanMap<?> bm = (o instanceof BeanMap ? (BeanMap)o : toBeanMap(o)); 803 int l = 0; 804 for (String k : bm.keySet()) { 805 if (! s.contains(k)) 806 return null; 807 l++; 808 } 809 if (s.size() != l) 810 return null; 811 } 812 } 813 return th; 814 } 815 816 //----------------------------------------------------------------------------------------------------------------- 817 // Properties 818 //----------------------------------------------------------------------------------------------------------------- 819 820 /** 821 * Configuration property: Add <js>"_type"</js> properties when needed. 822 * 823 * @see HtmlSerializer#HTML_addBeanTypes 824 * @return 825 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 826 * through reflection. 827 */ 828 @Override 829 protected final boolean isAddBeanTypes() { 830 return ctx.isAddBeanTypes(); 831 } 832 833 /** 834 * Configuration property: Add key/value headers on bean/map tables. 835 * 836 * @see HtmlSerializer#HTML_addKeyValueTableHeaders 837 * @return 838 * <jk>true</jk> if <bc>key</bc> and <bc>value</bc> column headers are added to tables. 839 */ 840 protected final boolean isAddKeyValueTableHeaders() { 841 return ctx.isAddKeyValueTableHeaders(); 842 } 843 844 /** 845 * Configuration property: Look for link labels in URIs. 846 * 847 * @see HtmlSerializer#HTML_detectLabelParameters 848 * @return 849 * <jk>true</jk> if we should look for URL label parameters (e.g. <js>"?label=foobar"</js>). 850 */ 851 protected final boolean isDetectLabelParameters() { 852 return ctx.isDetectLabelParameters(); 853 } 854 855 /** 856 * Configuration property: Look for URLs in {@link String Strings}. 857 * 858 * @see HtmlSerializer#HTML_detectLinksInStrings 859 * @return 860 * <jk>true</jk> if we should automatically convert strings to URLs if they look like a URL. 861 */ 862 protected final boolean isDetectLinksInStrings() { 863 return ctx.isDetectLinksInStrings(); 864 } 865 866 /** 867 * Configuration property: Link label parameter name. 868 * 869 * @see HtmlSerializer#HTML_labelParameter 870 * @return 871 * The parameter name to look for when resolving link labels via {@link HtmlSerializer#HTML_detectLabelParameters}. 872 */ 873 protected final String getLabelParameter() { 874 return ctx.getLabelParameter(); 875 } 876 877 /** 878 * Configuration property: Anchor text source. 879 * 880 * @see HtmlSerializer#HTML_uriAnchorText 881 * @return 882 * When creating anchor tags (e.g. <code><xt><a</xt> <xa>href</xa>=<xs>'...'</xs> 883 * <xt>></xt>text<xt></a></xt></code>) in HTML, this setting defines what to set the inner text to. 884 */ 885 protected final AnchorText getUriAnchorText() { 886 return ctx.getUriAnchorText(); 887 } 888 889 //----------------------------------------------------------------------------------------------------------------- 890 // Other methods 891 //----------------------------------------------------------------------------------------------------------------- 892 893 @Override /* Session */ 894 public ObjectMap toMap() { 895 return super.toMap() 896 .append("HtmlSerializerSession", new DefaultFilteringObjectMap() 897 ); 898 } 899}