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