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