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 e) { 679 throw e; 680 } catch (Error e) { 681 throw e; 682 } catch (Throwable e) { 683 onBeanGetterException(pMeta, e); 684 } 685 out.eTag("td").nl(i+2); 686 out.ie(i+1).eTag("tr").nl(i+1); 687 }); 688 689 out.ie(i).eTag("table").nl(i); 690 } 691 692 @SuppressWarnings({ "rawtypes", "unchecked" }) 693 private void serializeCollection(XmlWriter out, Object in, ClassMeta<?> sType, ClassMeta<?> eType, String name, BeanPropertyMeta ppMeta) throws SerializeException { 694 695 HtmlClassMeta cHtml = getHtmlClassMeta(sType); 696 HtmlBeanPropertyMeta bpHtml = getHtmlBeanPropertyMeta(ppMeta); 697 698 Collection c = (sType.isCollection() ? (Collection)in : toList(sType.getInnerClass(), in)); 699 700 boolean isCdc = cHtml.isHtmlCdc() || bpHtml.isHtmlCdc(); 701 boolean isSdc = cHtml.isHtmlSdc() || bpHtml.isHtmlSdc(); 702 boolean isDc = isCdc || isSdc; 703 704 int i = indent; 705 if (c.isEmpty()) { 706 out.appendln(i, "<ul></ul>"); 707 return; 708 } 709 710 String type2 = null; 711 if (sType != eType) 712 type2 = sType.getDictionaryName(); 713 if (type2 == null) 714 type2 = "array"; 715 716 c = sort(c); 717 718 String btpn = getBeanTypePropertyName(eType); 719 720 // Look at the objects to see how we're going to handle them. Check the first object to see how we're going to 721 // handle this. 722 // If it's a map or bean, then we'll create a table. 723 // Otherwise, we'll create a list. 724 Object[] th = getTableHeaders(c, bpHtml); 725 726 if (th != null) { 727 728 out.oTag(i, "table").attr(btpn, type2).w('>').nl(i+1); 729 if (th.length > 0) { 730 out.sTag(i+1, "tr").nl(i+2); 731 for (Object key : th) { 732 out.sTag(i+2, "th"); 733 out.text(convertToType(key, String.class)); 734 out.eTag("th").nl(i+2); 735 } 736 out.ie(i+1).eTag("tr").nl(i+1); 737 } else { 738 th = null; 739 } 740 741 for (Object o : c) { 742 ClassMeta<?> cm = getClassMetaForObject(o); 743 744 if (cm != null && cm.getSwap(this) != null) { 745 ObjectSwap swap = cm.getSwap(this); 746 o = swap(swap, o); 747 cm = swap.getSwapClassMeta(this); 748 } 749 750 out.oTag(i+1, "tr"); 751 String typeName = (cm == null ? null : cm.getDictionaryName()); 752 String typeProperty = getBeanTypePropertyName(cm); 753 754 if (typeName != null && eType.getElementType() != cm) 755 out.attr(typeProperty, typeName); 756 out.cTag().nl(i+2); 757 758 if (cm == null) { 759 out.i(i+2); 760 serializeAnything(out, o, null, null, null, 1, false, false); 761 out.nl(0); 762 763 } else if (cm.isMap() && ! (cm.isBeanMap())) { 764 Map m2 = sort((Map)o); 765 766 if (th == null) 767 th = m2.keySet().toArray(new Object[m2.size()]); 768 769 for (Object k : th) { 770 out.sTag(i+2, "td"); 771 ContentResult cr = serializeAnything(out, m2.get(k), eType.getElementType(), toString(k), null, 2, false, true); 772 if (cr == CR_ELEMENTS) 773 out.i(i+2); 774 out.eTag("td").nl(i+2); 775 } 776 } else { 777 BeanMap m2 = toBeanMap(o); 778 779 if (th == null) 780 th = m2.keySet().toArray(new Object[m2.size()]); 781 782 for (Object k : th) { 783 BeanMapEntry p = m2.getProperty(toString(k)); 784 BeanPropertyMeta pMeta = p.getMeta(); 785 if (pMeta.canRead()) { 786 Object value = p.getValue(); 787 788 String link = null, anchorText = null; 789 if (! pMeta.getClassMeta().isCollectionOrArray()) { 790 link = m2.resolveVars(getLink(pMeta)); 791 anchorText = m2.resolveVars(getAnchorText(pMeta)); 792 } 793 794 if (anchorText != null) 795 value = anchorText; 796 797 String style = getStyle(this, pMeta, value); 798 out.oTag(i+2, "td"); 799 if (style != null) 800 out.attr("style", style); 801 out.cTag(); 802 if (link != null) 803 out.oTag("a").attrUri("href", link).cTag(); 804 ContentResult cr = serializeAnything(out, value, pMeta.getClassMeta(), p.getKey().toString(), pMeta, 2, false, true); 805 if (cr == CR_ELEMENTS) 806 out.i(i+2); 807 if (link != null) 808 out.eTag("a"); 809 out.eTag("td").nl(i+2); 810 } 811 } 812 } 813 out.ie(i+1).eTag("tr").nl(i+1); 814 } 815 out.ie(i).eTag("table").nl(i); 816 817 } else { 818 out.oTag(i, isDc ? "p" : "ul"); 819 if (! type2.equals("array")) 820 out.attr(btpn, type2); 821 out.w('>').nl(i+1); 822 boolean isFirst = true; 823 for (Object o : c) { 824 if (isDc && ! isFirst) 825 out.append(isCdc ? ", " : " "); 826 if (! isDc) 827 out.oTag(i+1, "li"); 828 String style = getStyle(this, ppMeta, o); 829 String link = getLink(ppMeta); 830 if (style != null && ! isDc) 831 out.attr("style", style); 832 if (! isDc) 833 out.cTag(); 834 if (link != null) 835 out.oTag(i+2, "a").attrUri("href", link.replace("{#}", stringify(o))).cTag(); 836 ContentResult cr = serializeAnything(out, o, eType.getElementType(), name, null, 1, false, true); 837 if (link != null) 838 out.eTag("a"); 839 if (cr == CR_ELEMENTS) 840 out.ie(i+1); 841 if (! isDc) 842 out.eTag("li").nl(i+1); 843 isFirst = false; 844 } 845 out.ie(i).eTag(isDc ? "p" : "ul").nl(i); 846 } 847 } 848 849 private HtmlRender<?> getRender(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) { 850 if (pMeta == null) 851 return null; 852 HtmlRender<?> render = getHtmlBeanPropertyMeta(pMeta).getRender(); 853 if (render != null) 854 return render; 855 ClassMeta<?> cMeta = session.getClassMetaForObject(value); 856 render = cMeta == null ? null : getHtmlClassMeta(cMeta).getRender(); 857 return render; 858 } 859 860 @SuppressWarnings({"rawtypes","unchecked"}) 861 private String getStyle(HtmlSerializerSession session, BeanPropertyMeta pMeta, Object value) { 862 HtmlRender render = getRender(session, pMeta, value); 863 return render == null ? null : render.getStyle(session, value); 864 } 865 866 private String getLink(BeanPropertyMeta pMeta) { 867 return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getLink(); 868 } 869 870 private String getAnchorText(BeanPropertyMeta pMeta) { 871 return pMeta == null ? null : getHtmlBeanPropertyMeta(pMeta).getAnchorText(); 872 } 873 874 /* 875 * Returns the table column headers for the specified collection of objects. 876 * Returns null if collection should not be serialized as a 2-dimensional table. 877 * Returns an empty array if it should be treated as a table but without headers. 878 * 2-dimensional tables are used for collections of objects that all have the same set of property names. 879 */ 880 @SuppressWarnings({ "rawtypes", "unchecked" }) 881 private Object[] getTableHeaders(Collection c, HtmlBeanPropertyMeta bpHtml) throws SerializeException { 882 883 if (c.size() == 0) 884 return null; 885 886 c = sort(c); 887 888 Object o1 = null; 889 for (Object o : c) 890 if (o != null) { 891 o1 = o; 892 break; 893 } 894 if (o1 == null) 895 return null; 896 897 ClassMeta<?> cm1 = getClassMetaForObject(o1); 898 899 ObjectSwap swap = cm1.getSwap(this); 900 o1 = swap(swap, o1); 901 if (swap != null) 902 cm1 = swap.getSwapClassMeta(this); 903 904 if (cm1 == null || ! cm1.isMapOrBean() || cm1.hasAnnotation(HtmlLink.class)) 905 return null; 906 907 HtmlClassMeta cHtml = getHtmlClassMeta(cm1); 908 909 if (cHtml.isNoTables() || bpHtml.isNoTables() || cHtml.isXml() || bpHtml.isXml() || canIgnoreValue(cm1, null, o1)) 910 return null; 911 912 if (cHtml.isNoTableHeaders() || bpHtml.isNoTableHeaders()) 913 return new Object[0]; 914 915 // If it's a non-bean map, only use table if all entries are also maps. 916 if (cm1.isMap() && ! cm1.isBeanMap()) { 917 918 Set<Object> set = CollectionUtils.set(); 919 for (Object o : c) { 920 o = swap(swap, o); 921 if (! canIgnoreValue(cm1, null, o)) { 922 if (! cm1.isInstance(o)) 923 return null; 924 forEachEntry((Map)o, x -> set.add(x.getKey())); 925 } 926 } 927 return set.toArray(new Object[set.size()]); 928 } 929 930 // Must be a bean or BeanMap. 931 for (Object o : c) { 932 o = swap(swap, o); 933 if (! canIgnoreValue(cm1, null, o)) { 934 if (! cm1.isInstance(o)) 935 return null; 936 } 937 } 938 939 BeanMap<?> bm = toBeanMap(o1); 940 return bm.keySet().toArray(new String[bm.size()]); 941 } 942 943 //----------------------------------------------------------------------------------------------------------------- 944 // Properties 945 //----------------------------------------------------------------------------------------------------------------- 946 947 /** 948 * Add <js>"_type"</js> properties when needed. 949 * 950 * @see HtmlSerializer.Builder#addBeanTypesHtml() 951 * @return 952 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 953 * through reflection. 954 */ 955 @Override 956 protected final boolean isAddBeanTypes() { 957 return ctx.isAddBeanTypes(); 958 } 959 960 /** 961 * Add key/value headers on bean/map tables. 962 * 963 * @see HtmlSerializer.Builder#addKeyValueTableHeaders() 964 * @return 965 * <jk>true</jk> if <bc>key</bc> and <bc>value</bc> column headers are added to tables. 966 */ 967 protected final boolean isAddKeyValueTableHeaders() { 968 return ctx.isAddKeyValueTableHeaders(); 969 } 970 971 /** 972 * Look for link labels in URIs. 973 * 974 * @see HtmlSerializer.Builder#disableDetectLabelParameters() 975 * @return 976 * <jk>true</jk> if we should ook for URL label parameters (e.g. <js>"?label=foobar"</js>). 977 */ 978 protected final boolean isDetectLabelParameters() { 979 return ctx.isDetectLabelParameters(); 980 } 981 982 /** 983 * Look for URLs in {@link String Strings}. 984 * 985 * @see HtmlSerializer.Builder#disableDetectLinksInStrings() 986 * @return 987 * <jk>true</jk> if we should automatically convert strings to URLs if they look like a URL. 988 */ 989 protected final boolean isDetectLinksInStrings() { 990 return ctx.isDetectLinksInStrings(); 991 } 992 993 /** 994 * Link label parameter name. 995 * 996 * @see HtmlSerializer.Builder#labelParameter(String) 997 * @return 998 * The parameter name to look for when resolving link labels. 999 */ 1000 protected final String getLabelParameter() { 1001 return ctx.getLabelParameter(); 1002 } 1003 1004 /** 1005 * Anchor text source. 1006 * 1007 * @see HtmlSerializer.Builder#uriAnchorText(AnchorText) 1008 * @return 1009 * When creating anchor tags (e.g. <code><xt><a</xt> <xa>href</xa>=<xs>'...'</xs> 1010 * <xt>></xt>text<xt></a></xt></code>) in HTML, this setting defines what to set the inner text to. 1011 */ 1012 protected final AnchorText getUriAnchorText() { 1013 return ctx.getUriAnchorText(); 1014 } 1015 1016 //----------------------------------------------------------------------------------------------------------------- 1017 // Extended metadata 1018 //----------------------------------------------------------------------------------------------------------------- 1019 1020 /** 1021 * Returns the language-specific metadata on the specified class. 1022 * 1023 * @param cm The class to return the metadata on. 1024 * @return The metadata. 1025 */ 1026 protected HtmlClassMeta getHtmlClassMeta(ClassMeta<?> cm) { 1027 return ctx.getHtmlClassMeta(cm); 1028 } 1029 1030 /** 1031 * Returns the language-specific metadata on the specified bean property. 1032 * 1033 * @param bpm The bean property to return the metadata on. 1034 * @return The metadata. 1035 */ 1036 protected HtmlBeanPropertyMeta getHtmlBeanPropertyMeta(BeanPropertyMeta bpm) { 1037 return ctx.getHtmlBeanPropertyMeta(bpm); 1038 } 1039}