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