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.Shorts.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022 023import java.util.*; 024import java.util.concurrent.atomic.*; 025import java.util.function.*; 026import java.util.regex.*; 027 028import org.apache.juneau.commons.collections.*; 029import org.apache.juneau.commons.svl.*; 030import org.apache.juneau.marshall.*; 031 032/** 033 * Serializes POJOs to HTTP responses as HTML documents. 034 * 035 * <h5 class='topic'>Media types</h5> 036 * <p> 037 * Handles <c>Accept</c> types: <bc>text/html</bc> 038 * <p> 039 * Produces <c>Content-Type</c> types: <bc>text/html</bc> 040 * 041 * <h5 class='topic'>Description</h5> 042 * <p> 043 * Same as {@link HtmlSerializer}, except wraps the response in <code><xt><html></xt></code>, 044 * <code><xt><head></xt></code>, and <code><xt><body></xt></code> tags so that it can be rendered in a browser. 045 * 046 * <p> 047 * Configurable properties are typically specified via <ja>@HtmlDocConfig</ja>. 048 * 049 * <h5 class='section'>Example:</h5> 050 * <p class='bjava'> 051 * <ja>@Rest</ja>( 052 * messages=<js>"nls/AddressBookResource"</js>, 053 * title=<js>"$L{title}"</js>, 054 * description=<js>"$L{description}"</js> 055 * ) 056 * <ja>@HtmlDocConfig</ja>( 057 * navlinks={ 058 * <js>"api: servlet:/api"</js>, 059 * <js>"doc: doc"</js> 060 * } 061 * ) 062 * <jk>public class</jk> AddressBookResource <jk>extends</jk> BasicRestServlet { 063 * </p> 064 * 065 * <p> 066 * The <c>$L{...}</c> variable represent localized strings pulled from the resource bundle identified by the 067 * <c>messages</c> annotation. 068 * <br>These variables are replaced at runtime based on the HTTP request locale. 069 * <br>Several built-in runtime variable types are defined, and the API can be extended to include user-defined variables. 070 * 071 * <h5 class='section'>Notes:</h5><ul> 072 * <li class='note'>This class is thread safe and reusable. 073 * </ul> 074 * 075 * <h5 class='section'>See Also:</h5><ul> 076 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HtmlSupport">HTML</a> 077 078 * </ul> 079 */ 080@SuppressWarnings({ 081 "java:S110", // Inheritance depth acceptable for this class hierarchy 082 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 083 "java:S9149" // Per-format static factories intentionally shadow the parent's. 084}) 085public class HtmlDocSerializer extends HtmlStrippedDocSerializer { 086 087 /** 088 * Builder class. 089 */ 090 @SuppressWarnings({ 091 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 092 }) 093 public abstract static class Builder<SELF extends Builder<SELF>> extends HtmlStrippedDocSerializer.Builder<SELF> { 094 095 private static final Cache<HashKey,HtmlDocSerializer> CACHE = Cache.of(HashKey.class, HtmlDocSerializer.class).build(); 096 097 private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)"); 098 099 private static <T> List<T> copy(List<T> s) { 100 return s == null || s.isEmpty() ? null : cp(s); 101 } 102 103 private static <T> List<T> copy(T[] s) { 104 return s.length == 0 ? null : l(s); 105 } 106 107 List<String> aside; 108 List<String> footer; 109 List<String> head; 110 List<String> header; 111 List<String> nav; 112 List<String> navlinks; 113 List<String> script; 114 List<String> style; 115 List<String> stylesheet; 116 AsideFloat asideFloat; 117 String noResultsMessage; 118 119 boolean nowrap; 120 boolean resolveBodyVars; 121 122 Class<? extends HtmlDocTemplate> template; 123 124 List<Class<? extends HtmlWidget>> widgets; 125 126 /** 127 * Constructor, default settings. 128 */ 129 protected Builder() { 130 produces("text/html"); 131 accept("text/html"); 132 asideFloat = AsideFloat.RIGHT; 133 noResultsMessage = "<p>no results</p>"; 134 template = BasicHtmlDocTemplate.class; 135 } 136 137 /** 138 * Copy constructor. 139 * 140 * @param copyFrom The builder to copy from. 141 */ 142 protected Builder(Builder<?> copyFrom) { 143 super(copyFrom); 144 aside = copy(copyFrom.aside); 145 asideFloat = copyFrom.asideFloat; 146 footer = copy(copyFrom.footer); 147 head = copy(copyFrom.head); 148 header = copy(copyFrom.header); 149 nav = copy(copyFrom.nav); 150 navlinks = copy(copyFrom.navlinks); 151 noResultsMessage = copyFrom.noResultsMessage; 152 nowrap = copyFrom.nowrap; 153 resolveBodyVars = copyFrom.resolveBodyVars; 154 script = copy(copyFrom.script); 155 style = copy(copyFrom.style); 156 stylesheet = copy(copyFrom.stylesheet); 157 template = copyFrom.template; 158 widgets = copy(copyFrom.widgets); 159 } 160 161 /** 162 * Copy constructor. 163 * 164 * @param copyFrom The bean to copy from. 165 */ 166 protected Builder(HtmlDocSerializer copyFrom) { 167 super(copyFrom); 168 aside = copy(copyFrom.aside); 169 asideFloat = copyFrom.asideFloat; 170 footer = copy(copyFrom.footer); 171 head = copy(copyFrom.head); 172 header = copy(copyFrom.header); 173 nav = copy(copyFrom.nav); 174 navlinks = copy(copyFrom.navlinks); 175 noResultsMessage = copyFrom.noResultsMessage; 176 nowrap = copyFrom.nowrap; 177 resolveBodyVars = copyFrom.resolveBodyVars; 178 script = copy(copyFrom.script); 179 style = copy(copyFrom.style); 180 stylesheet = copy(copyFrom.stylesheet); 181 template = copyFrom.template; 182 widgets = copy(copyFrom.widgets); 183 } 184 185 /** 186 * Returns the list of aside section contents. 187 * 188 * <p> 189 * Gives access to the inner list if you need to make more than simple additions via {@link #aside(String...)}. 190 * 191 * @return The list of aside section contents. 192 * @see #aside(String...) 193 */ 194 public List<String> aside() { 195 if (aside == null) 196 aside = list(); 197 return aside; 198 } 199 200 /** 201 * Aside section contents. 202 * 203 * <p> 204 * Allows you to specify the contents of the aside section on the HTML page. 205 * The aside section floats on the right of the page for providing content supporting the serialized content of 206 * the page. 207 * 208 * <p> 209 * By default, the aside section is empty. 210 * 211 * <h5 class='section'>Example:</h5> 212 * <p class='bjava'> 213 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 214 * .<jsm>create</jsm>() 215 * .aside( 216 * <js>"<ul>"</js>, 217 * <js>" <li>Item 1"</js>, 218 * <js>" <li>Item 2"</js>, 219 * <js>" <li>Item 3"</js>, 220 * <js>"</ul>"</js> 221 * ) 222 * .build(); 223 * </p> 224 * 225 * <h5 class='section'>Notes:</h5><ul> 226 * <li class='note'> 227 * Format: HTML 228 * <li class='note'> 229 * Supports <a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerSvlVariables">REST Server SVL Variables</a> 230 * (e.g. <js>"$L{my.localized.variable}"</js>). 231 * <li class='note'> 232 * A value of <js>"NONE"</js> can be used to force no value. 233 * <li class='note'> 234 * The parent value can be included by adding the literal <js>"INHERIT"</js> as a value. 235 * <li class='note'> 236 * Multiple values are combined with newlines into a single string. 237 * <li class='note'> 238 * On methods, this value is inherited from the <ja>@HtmlDocConfig</ja> annotation on the servlet/resource class. 239 * <li class='note'> 240 * On servlet/resource classes, this value is inherited from the <ja>@HtmlDocConfig</ja> annotation on the 241 * parent class. 242 * </ul> 243 * 244 * @param value 245 * The new value for this property. 246 * @return This object. 247 */ 248 public SELF aside(String...value) { 249 aside = merge(aside, value); 250 return self(); 251 } 252 253 /** 254 * Float aside section contents. 255 * 256 * <p> 257 * Allows you to position the aside contents of the page around the main contents. 258 * 259 * <p> 260 * By default, the aside section is floated to the right. 261 * 262 * <h5 class='section'>Example:</h5> 263 * <p class='bjava'> 264 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 265 * .<jsm>create</jsm>() 266 * .aside( 267 * <js>"<ul>"</js>, 268 * <js>" <li>Item 1"</js>, 269 * <js>" <li>Item 2"</js>, 270 * <js>" <li>Item 3"</js>, 271 * <js>"</ul>"</js> 272 * ) 273 * .asideFloat(<jsf>RIGHT</jsf>) 274 * .build(); 275 * </p> 276 * 277 * @param value 278 * The new value for this property. 279 * @return This object. 280 */ 281 public SELF asideFloat(AsideFloat value) { 282 asideFloat = value; 283 return self(); 284 } 285 286 @Override /* Overridden from Context.Builder<?> */ 287 public HtmlDocSerializer build() { 288 return cache(CACHE).build(HtmlDocSerializer.class); 289 } 290 291 @Override /* Overridden from Context.Builder<?> */ 292 public abstract SELF copy(); 293 294 /** 295 * Returns the list of footer section contents. 296 * 297 * <p> 298 * Gives access to the inner list if you need to make more than simple additions via {@link #footer(String...)}. 299 * 300 * @return The list of footer section contents. 301 * @see #footer(String...) 302 */ 303 public List<String> footer() { 304 if (footer == null) 305 footer = list(); 306 return footer; 307 } 308 309 /** 310 * Footer section contents. 311 * 312 * <p> 313 * Allows you to specify the contents of the footer section on the HTML page. 314 * 315 * <p> 316 * By default, the footer section is empty. 317 * 318 * <h5 class='section'>Example:</h5> 319 * <p class='bjava'> 320 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 321 * .<jsm>create</jsm>() 322 * .footer( 323 * <js>"<b>This interface is great!</b>"</js> 324 * ) 325 * .build(); 326 * </p> 327 * 328 * @param value 329 * The new value for this property. 330 * @return This object. 331 */ 332 public SELF footer(String...value) { 333 footer = merge(footer, value); 334 return self(); 335 } 336 337 @Override /* Overridden from Context.Builder<?> */ 338 public HashKey hashKey() { 339 // @formatter:off 340 return HashKey.of( 341 super.hashKey(), 342 aside, 343 footer, 344 head, 345 header, 346 nav, 347 navlinks, 348 script, 349 style, 350 stylesheet, 351 asideFloat, 352 noResultsMessage, 353 nowrap, 354 resolveBodyVars, 355 template, 356 widgets 357 ); 358 // @formatter:on 359 } 360 361 /** 362 * Returns the list of head section contents. 363 * 364 * <p> 365 * Gives access to the inner list if you need to make more than simple additions via {@link #head(String...)}. 366 * 367 * @return The list of head section contents. 368 * @see #head(String...) 369 */ 370 public List<String> head() { 371 if (head == null) 372 head = list(); 373 return head; 374 } 375 376 /** 377 * Additional head section content. 378 * 379 * <p> 380 * Adds the specified HTML content to the head section of the page. 381 * 382 * <h5 class='section'>Example:</h5> 383 * <p class='bjava'> 384 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 385 * .<jsm>create</jsm>() 386 * .head( 387 * <js>"<link rel='icon' href='$U{servlet:/htdocs/mypageicon.ico}'>"</js> 388 * ) 389 * .build(); 390 * </p> 391 * 392 * @param value 393 * The new value for this property. 394 * @return This object. 395 */ 396 public SELF head(String...value) { 397 head = merge(head, value); 398 return self(); 399 } 400 401 /** 402 * Returns the list of header section contents. 403 * 404 * <p> 405 * Gives access to the inner list if you need to make more than simple additions via {@link #header(String...)}. 406 * 407 * @return The list of header section contents. 408 * @see #header(String...) 409 */ 410 public List<String> header() { 411 if (header == null) 412 header = list(); 413 return header; 414 } 415 416 /** 417 * Header section contents. 418 * 419 * <p> 420 * Allows you to override the contents of the header section on the HTML page. 421 * The header section normally contains the title and description at the top of the page. 422 * 423 * <h5 class='section'>Example:</h5> 424 * <p class='bjava'> 425 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 426 * .<jsm>create</jsm>() 427 * .header( 428 * <js>"<h1>My own header</h1>"</js> 429 * ) 430 * .build() 431 * </p> 432 * 433 * @param value 434 * The new value for this property. 435 * @return This object. 436 */ 437 public SELF header(String...value) { 438 header = merge(header, value); 439 return self(); 440 } 441 442 /** 443 * Returns the list of nav section contents. 444 * 445 * <p> 446 * Gives access to the inner list if you need to make more than simple additions via {@link #nav(String...)}. 447 * 448 * @return The list of nav section contents. 449 * @see #nav(String...) 450 */ 451 public List<String> nav() { 452 if (nav == null) 453 nav = list(); 454 return nav; 455 } 456 457 /** 458 * Nav section contents. 459 * 460 * <p> 461 * Allows you to override the contents of the nav section on the HTML page. 462 * The nav section normally contains the page links at the top of the page. 463 * 464 * <h5 class='section'>Example:</h5> 465 * <p class='bjava'> 466 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 467 * .<jsm>create</jsm>() 468 * .nav( 469 * <js>"<p class='special-navigation'>This is my special navigation content</p>"</js> 470 * ) 471 * .build() 472 * </p> 473 * 474 * <p> 475 * When this property is specified, the {@link Builder#navlinks(String...)} property is ignored. 476 * 477 * @param value 478 * The new value for this property. 479 * @return This object. 480 */ 481 public SELF nav(String...value) { 482 nav = merge(nav, value); 483 return self(); 484 } 485 486 /** 487 * Returns the list of navlinks section contents. 488 * 489 * <p> 490 * Gives access to the inner list if you need to make more than simple additions via {@link #navlinks(String...)}. 491 * 492 * @return The list of navlinks section contents. 493 * @see #navlinks(String...) 494 */ 495 public List<String> navlinks() { 496 if (navlinks == null) 497 navlinks = list(); 498 return navlinks; 499 } 500 501 /** 502 * Page navigation links. 503 * 504 * <p> 505 * Adds a list of hyperlinks immediately under the title and description but above the content of the page. 506 * 507 * <p> 508 * This can be used to provide convenient hyperlinks when viewing the REST interface from a browser. 509 * 510 * <p> 511 * The value is an array of strings with two possible values: 512 * <ul> 513 * <li>A key-value pair representing a hyperlink label and href: 514 * <br><js>"google: http://google.com"</js> 515 * <li>Arbitrary HTML. 516 * </ul> 517 * 518 * <p> 519 * Relative URLs are considered relative to the servlet path. 520 * For example, if the servlet path is <js>"http://localhost/myContext/myServlet"</js>, and the 521 * URL is <js>"foo"</js>, the link becomes <js>"http://localhost/myContext/myServlet/foo"</js>. 522 * Absolute (<js>"/myOtherContext/foo"</js>) and fully-qualified (<js>"http://localhost2/foo"</js>) URLs 523 * can also be used in addition to various other protocols specified by {@link UriResolver} such as 524 * <js>"servlet:/..."</js>. 525 * 526 * <h5 class='section'>Example:</h5> 527 * <p class='bjava'> 528 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 529 * .<jsm>create</jsm>() 530 * .navlinks( 531 * <js>"api: servlet:/api"</js>, 532 * <js>"stats: servlet:/stats"</js>, 533 * <js>"doc: doc"</js> 534 * ) 535 * .build(); 536 * </p> 537 * 538 * @param value 539 * The new value for this property. 540 * @return This object. 541 */ 542 public SELF navlinks(String...value) { 543 navlinks = mergeNavLinks(navlinks, value); 544 return self(); 545 } 546 547 /** 548 * No-results message. 549 * 550 * <p> 551 * Allows you to specify the string message used when trying to serialize an empty array or empty list. 552 * 553 * <h5 class='section'>Example:</h5> 554 * <p class='bjava'> 555 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 556 * .<jsm>create</jsm>() 557 * .noResultsMessage(<js>"<b>This interface is great!</b>"</js>) 558 * .build(); 559 * </p> 560 * 561 * <p> 562 * A value of <js>"NONE"</js> can be used to represent no value to differentiate it from an empty string. 563 * 564 * @param value 565 * The new value for this property. 566 * @return This object. 567 */ 568 public SELF noResultsMessage(String value) { 569 noResultsMessage = value; 570 return self(); 571 } 572 573 /** 574 * Prevent word wrap on page. 575 * 576 * <p> 577 * Adds <js>"* {white-space:nowrap}"</js> to the CSS instructions on the page to prevent word wrapping. 578 * 579 * @return This object. 580 */ 581 public SELF nowrap() { 582 return nowrap(true); 583 } 584 585 /** 586 * Same as {@link #nowrap()} but allows you to explicitly specify the boolean value. 587 * 588 * @param value 589 * The new value for this property. 590 * @return This object. 591 * @see #nowrap() 592 */ 593 public SELF nowrap(boolean value) { 594 nowrap = value; 595 return self(); 596 } 597 598 /** 599 * Resolve $ variables in serialized POJO. 600 * 601 * @return This object. 602 */ 603 public SELF resolveBodyVars() { 604 return resolveBodyVars(true); 605 } 606 607 /** 608 * Same as {@link #resolveBodyVars()} but allows you to explicitly specify the boolean value. 609 * 610 * @param value 611 * The new value for this property. 612 * @return This object. 613 * @see #nowrap() 614 */ 615 public SELF resolveBodyVars(boolean value) { 616 resolveBodyVars = value; 617 return self(); 618 } 619 620 /** 621 * Returns the list of page script contents. 622 * 623 * <p> 624 * Gives access to the inner list if you need to make more than simple additions via {@link #script(String...)}. 625 * 626 * @return The list of page script contents. 627 * @see #script(String...) 628 */ 629 public List<String> script() { 630 if (script == null) 631 script = list(); 632 return script; 633 } 634 635 /** 636 * Adds the specified Javascript code to the HTML page. 637 * 638 * <p> 639 * A shortcut on <ja>@Rest</ja> is also provided for this setting: 640 * <p class='bjava'> 641 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 642 * .<jsm>create</jsm>() 643 * .script(<js>"alert('hello!');"</js>) 644 * .build(); 645 * </p> 646 * 647 * @param value 648 * The value to add to this property. 649 * @return This object. 650 */ 651 public SELF script(String...value) { 652 script = merge(script, value); 653 return self(); 654 } 655 656 /** 657 * Returns the list of page style contents. 658 * 659 * <p> 660 * Gives access to the inner list if you need to make more than simple additions via {@link #style(String...)}. 661 * 662 * @return The list of page style contents. 663 * @see #style(String...) 664 */ 665 public List<String> style() { 666 if (style == null) 667 style = list(); 668 return style; 669 } 670 671 /** 672 * Adds the specified CSS instructions to the HTML page. 673 * 674 * <p class='bjava'> 675 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 676 * .<jsm>create</jsm>() 677 * .style( 678 * <js>"h3 { color: red; }"</js>, 679 * <js>"h5 { font-weight: bold; }"</js> 680 * ) 681 * .build(); 682 * </p> 683 * 684 * @param value 685 * The value to add to this property. 686 * @return This object. 687 */ 688 public SELF style(String...value) { 689 style = merge(style, value); 690 return self(); 691 } 692 693 /** 694 * Returns the list of stylesheet URLs. 695 * 696 * <p> 697 * Gives access to the inner list if you need to make more than simple additions via {@link #stylesheet(String...)}. 698 * 699 * @return The list of stylesheet URLs. 700 * @see #stylesheet(String...) 701 */ 702 public List<String> stylesheet() { 703 if (stylesheet == null) 704 stylesheet = list(); 705 return stylesheet; 706 } 707 708 /** 709 * Adds to the list of stylesheet URLs. 710 * 711 * <p> 712 * Note that this stylesheet is controlled by the <code><ja>@Rest</ja>.stylesheet()</code> annotation. 713 * 714 * @param value 715 * The value to add to this property. 716 * @return This object. 717 */ 718 public SELF stylesheet(String...value) { 719 stylesheet = merge(stylesheet, value); 720 return self(); 721 } 722 723 /** 724 * HTML document template. 725 * 726 * <p> 727 * Specifies the template to use for serializing the page. 728 * 729 * <p> 730 * By default, the {@link BasicHtmlDocTemplate} class is used to construct the contents of the HTML page, but 731 * can be overridden with your own custom implementation class. 732 * 733 * <h5 class='section'>Example:</h5> 734 * <p class='bjava'> 735 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 736 * .<jsm>create</jsm>() 737 * .template(MySpecialDocTemplate.<jk>class</jk>) 738 * .build(); 739 * </p> 740 * 741 * @param value 742 * The new value for this property. 743 * @return This object. 744 */ 745 public SELF template(Class<? extends HtmlDocTemplate> value) { 746 template = value; 747 return self(); 748 } 749 750 751 /** 752 * Returns the list of page widgets. 753 * 754 * <p> 755 * Gives access to the inner list if you need to make more than simple additions via {@link #widgets(Class...)}. 756 * 757 * @return The list of page widgets. 758 * @see #widgets(Class...) 759 */ 760 public List<Class<? extends HtmlWidget>> widgets() { 761 if (widgets == null) 762 widgets = list(); 763 return widgets; 764 } 765 766 /** 767 * HTML Widgets. 768 * 769 * <p> 770 * Defines widgets that can be used in conjunction with string variables of the form <js>"$W{name}"</js>to quickly 771 * generate arbitrary replacement text. 772 * 773 * Widgets resolve the following variables: 774 * <ul class='spaced-list'> 775 * <li><js>"$W{name}"</js> - Contents returned by {@link HtmlWidget#getHtml(VarResolverSession)}. 776 * <li><js>"$W{name.script}"</js> - Contents returned by {@link HtmlWidget#getScript(VarResolverSession)}. 777 * <br>The script contents are automatically inserted into the <xt><head/script></xt> section 778 * in the HTML page. 779 * <li><js>"$W{name.style}"</js> - Contents returned by {@link HtmlWidget#getStyle(VarResolverSession)}. 780 * <br>The styles contents are automatically inserted into the <xt><head/style></xt> section 781 * in the HTML page. 782 * </ul> 783 * 784 * <p> 785 * The following examples shows how to associate a widget with a REST method and then have it rendered in the links 786 * and aside section of the page: 787 * 788 * <p class='bjava'> 789 * WriterSerializer <jv>serializer</jv> = HtmlDocSerializer 790 * .<jsm>create</jsm>() 791 * .widgets( 792 * MyWidget.<jk>class</jk> 793 * ) 794 * .navlinks( 795 * <js>"$W{MyWidget}"</js> 796 * ) 797 * .aside( 798 * <js>"Check out this widget: $W{MyWidget}"</js> 799 * ) 800 * .build(); 801 * </p> 802 * 803 * <h5 class='section'>Notes:</h5><ul> 804 * <li class='note'> 805 * Widgets are inherited from super classes, but can be overridden by reusing the widget name. 806 * </ul> 807 * 808 * <h5 class='section'>See Also:</h5><ul> 809 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HtmlWidgets">Widgets</a> 810 * </ul> 811 * 812 * @param values The values to add to this setting. 813 * @return This object. 814 */ 815 @SuppressWarnings({ 816 "unchecked" // Varargs method requires unchecked cast 817 }) 818 public SELF widgets(Class<? extends HtmlWidget>...values) { 819 addAll(widgets(), values); 820 return self(); 821 } 822 823 private static List<String> merge(List<String> old, String[] newValues) { 824 List<String> x = listOfSize(newValues.length); 825 for (var s : newValues) { 826 if (eq(s, "NONE")) { 827 if (nn(old)) 828 old.clear(); 829 } else if (eq(s, "INHERIT")) { 830 if (nn(old)) 831 x.addAll(old); 832 } else { 833 x.add(s); 834 } 835 } 836 return x; 837 } 838 839 @SuppressWarnings({ 840 "java:S3776" // Cognitive complexity acceptable for nav link merging with various input formats 841 }) 842 private static List<String> mergeNavLinks(List<String> old, String[] newValues) { 843 List<String> x = listOfSize(newValues.length); 844 for (var s : newValues) { 845 if (eq(s, "NONE")) { 846 if (nn(old)) 847 old.clear(); 848 } else if (eq(s, "INHERIT")) { 849 if (nn(old)) 850 x.addAll(old); 851 } else if (s.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(s).matches()) { 852 Matcher lm = INDEXED_LINK_PATTERN.matcher(s); 853 lm.matches(); 854 String key = lm.group(1); 855 int index = Math.min(x.size(), Integer.parseInt(lm.group(2))); 856 String remainder = lm.group(3); 857 x.add(index, key.isEmpty() ? remainder : key + ":" + remainder); 858 } else { 859 x.add(s); 860 } 861 } 862 return x; 863 } 864 } 865 866 /** 867 * Concrete default builder leaf for the non-subclassed {@link HtmlDocSerializer#create()} / {@link HtmlDocSerializer#copy()} path. 868 */ 869 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 870 871 DefaultBuilder() {} 872 873 DefaultBuilder(HtmlDocSerializer copyFrom) { 874 super(copyFrom); 875 } 876 877 DefaultBuilder(Builder<?> copyFrom) { 878 super(copyFrom); 879 } 880 881 @Override /* Overridden from Context.Builder<?> */ 882 public DefaultBuilder copy() { 883 return new DefaultBuilder(this); 884 } 885 } 886 887 private static final String[] EMPTY_ARRAY = {}; 888 889 /** Default serializer, all default settings. */ 890 public static final HtmlDocSerializer DEFAULT = new HtmlDocSerializer(create()); 891 892 /** 893 * Creates a new builder for this object. 894 * 895 * @return A new builder. 896 */ 897 @SuppressWarnings({ 898 "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type 899 }) 900 public static Builder<?> create() { 901 return new DefaultBuilder(); 902 } 903 904final String[] style; 905final String[] stylesheet; 906final String[] script; 907final String[] navlinks; 908final String[] head; 909final String[] header; 910final String[] nav; 911final String[] aside; 912final String[] footer; 913final AsideFloat asideFloat; 914 final String noResultsMessage; 915 final boolean nowrap; 916 final boolean resolveBodyVars; 917 final Class<? extends HtmlDocTemplate> template; 918 final List<Class<? extends HtmlWidget>> widgets; 919 920 private final HtmlWidgetMap widgetMap; 921 private final HtmlWidget[] widgetArray; 922 private final HtmlDocTemplate templateBean; 923 924 private final AtomicReference<HtmlSchemaDocSerializer> schemaSerializer = new AtomicReference<>(); 925 926 /** 927 * Constructor. 928 * 929 * @param builder The builder for this object. 930 */ 931 public HtmlDocSerializer(Builder<?> builder) { 932 super(builder); 933 aside = nn(builder.aside) ? toArray(builder.aside) : EMPTY_ARRAY; 934 asideFloat = builder.asideFloat; 935 footer = nn(builder.footer) ? toArray(builder.footer) : EMPTY_ARRAY; 936 head = nn(builder.head) ? toArray(builder.head) : EMPTY_ARRAY; 937 header = nn(builder.header) ? toArray(builder.header) : EMPTY_ARRAY; 938 nav = nn(builder.nav) ? toArray(builder.nav) : EMPTY_ARRAY; 939 navlinks = nn(builder.navlinks) ? toArray(builder.navlinks) : EMPTY_ARRAY; 940 noResultsMessage = builder.noResultsMessage; 941 nowrap = builder.nowrap; 942 resolveBodyVars = builder.resolveBodyVars; 943 script = nn(builder.script) ? toArray(builder.script) : EMPTY_ARRAY; 944 style = nn(builder.style) ? toArray(builder.style) : EMPTY_ARRAY; 945 stylesheet = nn(builder.stylesheet) ? toArray(builder.stylesheet) : EMPTY_ARRAY; 946 template = builder.template; 947 widgets = builder.widgets == null ? Collections.emptyList() : cp(builder.widgets); 948 949 templateBean = newInstance(template); 950 widgetMap = new HtmlWidgetMap(); 951 widgets.stream().map(this::newInstance).forEach(widgetMap::append); 952 widgetArray = array(widgetMap.values(), HtmlWidget.class); 953 } 954 955 @Override /* Overridden from Context */ 956 public Builder<?> copy() { 957 return new DefaultBuilder(this); 958 } 959 960 @Override /* Overridden from Context */ 961 public HtmlDocSerializerSession.Builder<?> createSession() { 962 return HtmlDocSerializerSession.create(this); 963 } 964 965 @Override /* Overridden from XmlSerializer */ 966 public HtmlSerializer getSchemaSerializer() { 967 HtmlSchemaDocSerializer result = schemaSerializer.get(); 968 if (result == null) { 969 result = HtmlSchemaDocSerializer.create().marshallingContext(getMarshallingContext()).build(); 970 if (! schemaSerializer.compareAndSet(null, result)) { 971 result = schemaSerializer.get(); 972 } 973 } 974 return result; 975 } 976 977 @Override /* Overridden from Context */ 978 public HtmlDocSerializerSession getSession() { return createSession().build(); } 979 980 <T> T newInstance(Class<T> c) { 981 try { 982 return c.getDeclaredConstructor().newInstance(); 983 } catch (Exception e) { 984 throw toRex(e); 985 } 986 } 987 988 private static String[] toArray(List<String> x) { 989 return x.toArray(new String[x.size()]); 990 } 991 992 /** 993 * Performs an action on all widgets defined on this serializer. 994 * 995 * @param action The action to perform. 996 * @return This object. 997 */ 998 protected final HtmlDocSerializer forEachWidget(Consumer<HtmlWidget> action) { 999 for (var w : widgetArray) 1000 action.accept(w); 1001 return this; 1002 } 1003 1004 /** 1005 * Aside section contents. 1006 * 1007 * @see Builder#aside(String...) 1008 * @return 1009 * The overridden contents of the aside section on the HTML page. 1010 */ 1011 protected final String[] getAside() { return aside; } 1012 1013 /** 1014 * Float side section contents. 1015 * 1016 * @see Builder#asideFloat(AsideFloat) 1017 * @return 1018 * How to float the aside contents on the page. 1019 */ 1020 protected final AsideFloat getAsideFloat() { return asideFloat; } 1021 1022 /** 1023 * Footer section contents. 1024 * 1025 * @see Builder#footer(String...) 1026 * @return 1027 * The overridden contents of the footer section on the HTML page. 1028 */ 1029 protected final String[] getFooter() { return footer; } 1030 1031 /** 1032 * Additional head section content. 1033 * 1034 * @see Builder#head(String...) 1035 * @return 1036 * HTML content to add to the head section of the HTML page. 1037 */ 1038 protected final String[] getHead() { return head; } 1039 1040 /** 1041 * Header section contents. 1042 * 1043 * @see Builder#header(String...) 1044 * @return 1045 * The overridden contents of the header section on the HTML page. 1046 */ 1047 protected final String[] getHeader() { return header; } 1048 1049 /** 1050 * Nav section contents. 1051 * 1052 * @see Builder#nav(String...) 1053 * @return 1054 * The overridden contents of the nav section on the HTML page. 1055 */ 1056 protected final String[] getNav() { return nav; } 1057 1058 /** 1059 * Page navigation links. 1060 * 1061 * @see Builder#navlinks(String...) 1062 * @return 1063 * Navigation links to add to the HTML page. 1064 */ 1065 protected final String[] getNavlinks() { return navlinks; } 1066 1067 /** 1068 * No-results message. 1069 * 1070 * @see Builder#noResultsMessage(String) 1071 * @return 1072 * The message used when serializing an empty array or empty list. 1073 */ 1074 protected final String getNoResultsMessage() { return noResultsMessage; } 1075 1076 /** 1077 * Javascript code. 1078 * 1079 * @see Builder#script(String...) 1080 * @return 1081 * Arbitrary Javascript to add to the HTML page. 1082 */ 1083 protected final String[] getScript() { return script; } 1084 1085 /** 1086 * CSS style code. 1087 * 1088 * @see Builder#style(String...) 1089 * @return 1090 * The CSS instructions to add to the HTML page. 1091 */ 1092 protected final String[] getStyle() { return style; } 1093 1094 /** 1095 * Stylesheet import URLs. 1096 * 1097 * @see Builder#stylesheet(String...) 1098 * @return 1099 * The link to the stylesheet of the HTML page. 1100 */ 1101 protected final String[] getStylesheet() { return stylesheet; } 1102 1103 /** 1104 * HTML document template. 1105 * 1106 * @see Builder#template(Class) 1107 * @return 1108 * The template to use for serializing the page. 1109 */ 1110 protected final HtmlDocTemplate getTemplate() { return templateBean; } 1111 1112 /** 1113 * HTML widgets. 1114 * 1115 * @see Builder#widgets(Class...) 1116 * @return 1117 * Widgets defined on this serializers. 1118 */ 1119 protected final HtmlWidgetMap getWidgets() { return widgetMap; } 1120 1121 /** 1122 * Prevent word wrap on page. 1123 * 1124 * @see Builder#nowrap() 1125 * @return 1126 * <jk>true</jk> if <js>"* {white-space:nowrap}"</js> shoudl be added to the CSS instructions on the page to prevent word wrapping. 1127 */ 1128 protected final boolean isNowrap() { return nowrap; } 1129 1130 @Override /* Overridden from HtmlSerializer */ 1131 protected FluentMap<String,Object> properties() { 1132 return super.properties() 1133 .a("aside", aside) 1134 .a("asideFloat", asideFloat) 1135 .a("footer", footer) 1136 .a("head", head) 1137 .a("header", header) 1138 .a("nav", nav) 1139 .a("navlinks", navlinks) 1140 .a("noResultsMessage", noResultsMessage) 1141 .a("nowrap", nowrap) 1142 .a("style", style) 1143 .a("stylesheet", stylesheet) 1144 .a("template", template) 1145 .a("widgets", widgets); 1146 } 1147}