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><a</xt> <xa>href</xa>=<xs>'...'</xs> 890 * <xt>></xt>text<xt></a></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}