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