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.Shorts.*; 020import static org.apache.juneau.commons.utils.SystemUtils.*; 021 022import java.util.*; 023import java.util.concurrent.*; 024import java.util.concurrent.atomic.*; 025 026import org.apache.juneau.commons.bean.*; 027import org.apache.juneau.commons.collections.*; 028import org.apache.juneau.marshall.*; 029import org.apache.juneau.marshall.serializer.*; 030import org.apache.juneau.marshall.xml.*; 031 032/** 033 * Serializes POJO models to HTML. 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 * The conversion is as follows... 044 * <ul class='spaced-list'> 045 * <li> 046 * {@link Map Maps} (e.g. {@link HashMap}, {@link TreeMap}) and beans are converted to HTML tables with 047 * 'key' and 'value' columns. 048 * <li> 049 * {@link Collection Collections} (e.g. {@link HashSet}, {@link LinkedList}) and Java arrays are converted 050 * to HTML ordered lists. 051 * <li> 052 * {@code Collections} of {@code Maps} and beans are converted to HTML tables with keys as headers. 053 * <li> 054 * Everything else is converted to text. 055 * </ul> 056 * 057 * <p> 058 * This serializer provides several serialization options. Typically, one of the predefined <jsf>DEFAULT</jsf> 059 * serializers will be sufficient. 060 * However, custom serializers can be constructed to fine-tune behavior. 061 * 062 * <p> 063 * The {@link HtmlLink} annotation can be used on beans to add hyperlinks to the output. 064 * 065 * <h5 class='topic'>Behavior-specific subclasses</h5> 066 * <p> 067 * The following direct subclasses are provided for convenience: 068 * <ul class='spaced-list'> 069 * <li> 070 * {@link Sq} - Default serializer, single quotes. 071 * <li> 072 * {@link SqReadable} - Default serializer, single quotes, whitespace added. 073 * </ul> 074 * 075 * <h5 class='section'>Example:</h5> 076 * <p class='bjava'> 077 * <jc>// Use one of the default serializers to serialize a POJO</jc> 078 * String <jv>html</jv> = HtmlSerializer.<jsf>DEFAULT</jsf>.write(<jv>someObject</jv>); 079 * 080 * <jc>// Create a custom serializer that doesn't use whitespace and newlines</jc> 081 * HtmlSerializer <jv>serializer</jv> = HtmlSerializer.<jsm>create</jsm>().ws().build(); 082 * 083 * <jc>// Same as above, except uses cloning</jc> 084 * HtmlSerializer <jv>serializer</jv> = HtmlSerializer.<jsf>DEFAULT</jsf>.copy().ws().build(); 085 * 086 * <jc>// Serialize POJOs to HTML</jc> 087 * 088 * <jc>// Produces: </jc> 089 * <jc>// <ul><li>1<li>2<li>3</ul></jc> 090 * List <jv>list</jv> = JsonList.<jsm>of</jsm>(1, 2, 3); 091 * String <jv>html</jv> = HtmlSerializer.<jsf>DEFAULT</jsf>.write(<jv>list</jv>); 092 * 093 * <jc>// Produces: </jc> 094 * <jc>// <table> </jc> 095 * <jc>// <tr><th>firstName</th><th>lastName</th></tr> </jc> 096 * <jc>// <tr><td>Bob</td><td>Costas</td></tr> </jc> 097 * <jc>// <tr><td>Billy</td><td>TheKid</td></tr> </jc> 098 * <jc>// <tr><td>Barney</td><td>Miller</td></tr> </jc> 099 * <jc>// </table> </jc> 100 * <jv>html</jv> = JsonList.<jsm>of</jsm>( 101 * JsonMap.<jsm>ofText</jsm>(<js>"{firstName:'Bob',lastName:'Costas'}"</js>), 102 * JsonMap.<jsm>ofText</jsm>(<js>"{firstName:'Billy',lastName:'TheKid'}"</js>), 103 * JsonMap.<jsm>ofText</jsm>(<js>"{firstName:'Barney',lastName:'Miller'}"</js>) 104 * ); 105 * String <jv>html</jv> = HtmlSerializer.<jsf>DEFAULT</jsf>.write(<jv>list</jv>); 106 * 107 * <jc>// Produces: </jc> 108 * <jc>// <table> </jc> 109 * <jc>// <tr><th>key</th><th>value</th></tr> </jc> 110 * <jc>// <tr><td>foo</td><td>bar</td></tr> </jc> 111 * <jc>// <tr><td>baz</td><td>123</td></tr> </jc> 112 * <jc>// </table> </jc> 113 * Map <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{foo:'bar',baz:123}"</js>); 114 * String <jv>html</jv> = HtmlSerializer.<jsf>DEFAULT</jsf>.write(<jv>map</jv>); 115 * 116 * <jc>// HTML elements can be nested arbitrarily deep</jc> 117 * <jc>// Produces: </jc> 118 * <jc>// <table> </jc> 119 * <jc>// <tr><th>key</th><th>value</th></tr> </jc> 120 * <jc>// <tr><td>foo</td><td>bar</td></tr> </jc> 121 * <jc>// <tr><td>baz</td><td>123</td></tr> </jc> 122 * <jc>// <tr><td>someNumbers</td><td><ul><li>1<li>2<li>3</ul></td></tr> </jc> 123 * <jc>// <tr><td>someSubMap</td><td> </jc> 124 * <jc>// <table> </jc> 125 * <jc>// <tr><th>key</th><th>value</th></tr> </jc> 126 * <jc>// <tr><td>a</td><td>b</td></tr> </jc> 127 * <jc>// </table> </jc> 128 * <jc>// </td></tr> </jc> 129 * <jc>// </table> </jc> 130 * Map <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{foo:'bar',baz:123}"</js>); 131 * <jv>map</jv>.put(<js>"someNumbers"</js>, JsonList.<jsm>of</jsm>(1, 2, 3)); 132 * <jv>map</jv>.put(<js>"someSubMap"</js>, JsonMap.<jsm>ofText</jsm>(<js>"{a:'b'}"</js>)); 133 * String <jv>html</jv> = HtmlSerializer.<jsf>DEFAULT</jsf>.write(<jv>map</jv>); 134 * </p> 135 * 136 * <h5 class='section'>Notes:</h5><ul> 137 * <li class='note'>This class is thread safe and reusable. 138 * </ul> 139 * 140 * <h5 class='section'>See Also:</h5><ul> 141 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HtmlSupport">HTML</a> 142 143 * </ul> 144 */ 145@SuppressWarnings({ 146 "java:S110", // Inheritance depth acceptable for this class hierarchy 147 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 148 "java:S9149" // Per-format static factories intentionally shadow the parent's. 149}) 150public class HtmlSerializer extends XmlSerializer implements HtmlMetaProvider { 151 152 /** 153 * Builder class. 154 */ 155 @SuppressWarnings({ 156 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 157 }) 158 public abstract static class Builder<SELF extends Builder<SELF>> extends XmlSerializer.Builder<SELF> { 159 160 private static final Cache<HashKey,HtmlSerializer> CACHE = Cache.of(HashKey.class, HtmlSerializer.class).build(); 161 162 private boolean addBeanTypesHtml; 163 private boolean addKeyValueTableHeaders; 164 private boolean disableDetectLabelParameters; 165 private boolean disableDetectLinksInStrings; 166 private String labelParameter; 167 private AnchorText uriAnchorText; 168 169 /** 170 * Constructor, default settings. 171 */ 172 protected Builder() { 173 produces("text/html"); 174 addBeanTypesHtml = env("HtmlSerializer.addBeanTypesHtml", false); 175 addKeyValueTableHeaders = env("HtmlSerializer.addKeyValueTableHeaders", false); 176 disableDetectLabelParameters = env("HtmlSerializer.disableDetectLabelParameters", false); 177 disableDetectLinksInStrings = env("HtmlSerializer.disableDetectLinksInStrings", false); 178 uriAnchorText = env("HtmlSerializer.uriAnchorText", AnchorText.TO_STRING); 179 labelParameter = env("HtmlSerializer.labelParameter", "label"); 180 } 181 182 /** 183 * Copy constructor. 184 * 185 * @param copyFrom The builder to copy from. 186 * <br>Cannot be <jk>null</jk>. 187 */ 188 protected Builder(Builder<?> copyFrom) { 189 super(reqnn("copyFrom", copyFrom)); 190 addBeanTypesHtml = copyFrom.addBeanTypesHtml; 191 addKeyValueTableHeaders = copyFrom.addKeyValueTableHeaders; 192 disableDetectLabelParameters = copyFrom.disableDetectLabelParameters; 193 disableDetectLinksInStrings = copyFrom.disableDetectLinksInStrings; 194 labelParameter = copyFrom.labelParameter; 195 uriAnchorText = copyFrom.uriAnchorText; 196 } 197 198 /** 199 * Copy constructor. 200 * 201 * @param copyFrom The bean to copy from. 202 * <br>Cannot be <jk>null</jk>. 203 */ 204 protected Builder(HtmlSerializer copyFrom) { 205 super(reqnn("copyFrom", copyFrom)); 206 addBeanTypesHtml = copyFrom.addBeanTypesHtml; 207 addKeyValueTableHeaders = copyFrom.addKeyValueTableHeaders; 208 disableDetectLabelParameters = ! copyFrom.detectLabelParameters; 209 disableDetectLinksInStrings = ! copyFrom.detectLinksInStrings; 210 labelParameter = copyFrom.labelParameter; 211 uriAnchorText = copyFrom.uriAnchorText; 212 } 213 214 /** 215 * Add <js>"_type"</js> properties when needed. 216 * 217 * <p> 218 * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred 219 * through reflection. 220 * 221 * <p> 222 * When present, this value overrides the {@link org.apache.juneau.marshall.serializer.Serializer.Builder#addBeanTypes() Serializer.Builder#addBeanTypes()} setting and is 223 * provided to customize the behavior of specific serializers in a {@link SerializerSet}. 224 * 225 * @return This object. 226 */ 227 public SELF addBeanTypesHtml() { 228 return addBeanTypesHtml(true); 229 } 230 231 /** 232 * Same as {@link #addBeanTypesHtml()} but allows you to explicitly specify the value. 233 * 234 * @param value The value for this setting. 235 * @return This object. 236 */ 237 public SELF addBeanTypesHtml(boolean value) { 238 addBeanTypesHtml = value; 239 return self(); 240 } 241 242 /** 243 * <i><l>HtmlSerializer</l> configuration property: </i> Add key/value headers on bean/map tables. 244 * 245 * <p> 246 * When enabled, <bc>key</bc> and <bc>value</bc> column headers are added to tables. 247 * 248 * <h5 class='section'>Example:</h5> 249 * <p class='bjson'> 250 * <jc>// Our bean class.</jc> 251 * <jk>public class</jk> MyBean { 252 * <jk>public</jk> String <jf>f1</jf> = <js>"foo"</js>; 253 * <jk>public</jk> String <jf>f2</jf> = <js>"bar"</js>; 254 * } 255 * 256 * <jc>// Serializer without headers.</jc> 257 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer.<jsf>DEFAULT</jsf>; 258 * 259 * <jc>// Serializer with headers.</jc> 260 * WriterSerializer <jv>serializer2</jv> = HtmlSerializer 261 * .<jsm>create</jsm>() 262 * .addKeyValueTableHeaders() 263 * .build(); 264 * 265 * String <jv>withoutHeaders</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 266 * String <jv>withHeaders</jv> = <jv>serializer2</jv>.write(<jk>new</jk> MyBean()); 267 * </p> 268 * 269 * <p> 270 * The following shows the difference between the two generated outputs: 271 * 272 * <table class='styled'> 273 * <tr> 274 * <th><c>withoutHeaders</c></th> 275 * <th><c>withHeaders</c></th> 276 * </tr> 277 * <tr> 278 * <td> 279 * <table class='unstyled'> 280 * <tr><td>f1</td><td>foo</td></tr> 281 * <tr><td>f2</td><td>bar</td></tr> 282 * </table> 283 * </td> 284 * <td> 285 * <table class='unstyled'> 286 * <tr><th>key</th><th>value</th></tr> 287 * <tr><td>f1</td><td>foo</td></tr> 288 * <tr><td>f2</td><td>bar</td></tr> 289 * </table> 290 * </td> 291 * </tr> 292 * </table> 293 * 294 * @return This object. 295 */ 296 public SELF addKeyValueTableHeaders() { 297 return addKeyValueTableHeaders(true); 298 } 299 300 /** 301 * Same as {@link #addKeyValueTableHeaders()} but allows you to explicitly specify the value. 302 * 303 * @param value The value for this setting. 304 * @return This object. 305 */ 306 public SELF addKeyValueTableHeaders(boolean value) { 307 addKeyValueTableHeaders = value; 308 return self(); 309 } 310 311 @Override /* Overridden from Context.Builder<?> */ 312 public HtmlSerializer build() { 313 return cache(CACHE).build(HtmlSerializer.class); 314 } 315 316 @Override /* Overridden from Context.Builder<?> */ 317 public abstract SELF copy(); 318 319 /** 320 * <i><l>HtmlSerializer</l> configuration property: </i> Dont look for link labels in URIs. 321 * 322 * <p> 323 * Disables the feature where if the URL has a label parameter (e.g. <js>"?label=foobar"</js>), then use that as the anchor text of the link. 324 * 325 * <p> 326 * The parameter name can be changed via the {@link #labelParameter(String)} property. 327 * 328 * <h5 class='section'>Example:</h5> 329 * <p class='bjson'> 330 * <jc>// Our bean class with a property containing what looks like a URL.</jc> 331 * <jk>public class</jk> MyBean { 332 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"http://www.apache.org?label=Apache%20Foundation"</js>); 333 * } 334 * 335 * <jc>// Serializer with label detection.</jc> 336 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer 337 * .<jsm>create</jsm>() 338 * .addKeyValueTableHeaders() 339 * .build(); 340 * 341 * <jc>// Serializer without label detection.</jc> 342 * WriterSerializer <jv>serializer2</jv> = HtmlSerializer 343 * .<jsm>create</jsm>() 344 * .addKeyValueTableHeaders() 345 * .disableDetectLabelParameters() 346 * .build(); 347 * 348 * String <jv>withLabels</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 349 * String <jv>withoutLabels</jv> = <jv>serializer2</jv>.write(<jk>new</jk> MyBean()); 350 * </p> 351 * 352 * <p> 353 * The following shows the difference between the two generated outputs. 354 * <br>Note that they're both hyperlinks, but the anchor text differs: 355 * 356 * <table class='styled'> 357 * <tr> 358 * <th><c>withLabels</c></th> 359 * <th><c>withoutLabels</c></th> 360 * </tr> 361 * <tr> 362 * <td> 363 * <table class='unstyled'> 364 * <tr><th>key</th><th>value</th></tr> 365 * <tr><td>f1</td><td><a href='http://www.apache.org?label=Apache%20Foundation'>Apache Foundation</a></td></tr> 366 * </table> 367 * </td> 368 * <td> 369 * <table class='unstyled'> 370 * <tr><th>key</th><th>value</th></tr> 371 * <tr><td>f1</td><td><a href='http://www.apache.org?label=Apache%20Foundation'>http://www.apache.org?label=Apache%20Foundation</a></td></tr> 372 * </table> 373 * </td> 374 * </tr> 375 * </table> 376 * 377 * @return This object. 378 */ 379 public SELF disableDetectLabelParameters() { 380 return disableDetectLabelParameters(true); 381 } 382 383 /** 384 * Same as {@link #disableDetectLabelParameters()} but allows you to explicitly specify the value. 385 * 386 * @param value The value for this setting. 387 * @return This object. 388 */ 389 public SELF disableDetectLabelParameters(boolean value) { 390 disableDetectLabelParameters = value; 391 return self(); 392 } 393 394 /** 395 * <i><l>HtmlSerializer</l> configuration property: </i> Don't look for URLs in {@link String Strings}. 396 * 397 * <p> 398 * Disables the feature where if a string looks like a URL (i.e. starts with <js>"http://"</js> or <js>"https://"</js>, then treat it like a URL 399 * and make it into a hyperlink based on the rules specified by {@link Builder#uriAnchorText(AnchorText)}. 400 * 401 * <h5 class='section'>Example:</h5> 402 * <p class='bjson'> 403 * <jc>// Our bean class with a property containing what looks like a URL.</jc> 404 * <jk>public class</jk> MyBean { 405 * <jk>public</jk> String <jf>f1</jf> = <js>"http://www.apache.org"</js>; 406 * } 407 * 408 * <jc>// Serializer with link detection.</jc> 409 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer 410 * .<jsm>create</jsm>() 411 * .addKeyValueTableHeaders() 412 * .build(); 413 * 414 * <jc>// Serializer without link detection.</jc> 415 * WriterSerializer <jv>serializer2</jv> = HtmlSerializer 416 * .<jsm>create</jsm>() 417 * .addKeyValueTableHeaders() 418 * .disableDetectLinksInStrings() 419 * .build(); 420 * 421 * String <jv>withLinks</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 422 * String <jv>withoutLinks</jv> = <jv>serializer2</jv>.write(<jk>new</jk> MyBean()); 423 * </p> 424 * 425 * <p> 426 * The following shows the difference between the two generated outputs: 427 * 428 * <table class='styled'> 429 * <tr> 430 * <th><c>withLinks</c></th> 431 * <th><c>withoutLinks</c></th> 432 * </tr> 433 * <tr> 434 * <td> 435 * <table class='unstyled'> 436 * <tr><th>key</th><th>value</th></tr> 437 * <tr><td>f1</td><td><a href='http://www.apache.org'>http://www.apache.org</a></td></tr> 438 * </table> 439 * </td> 440 * <td> 441 * <table class='unstyled'> 442 * <tr><th>key</th><th>value</th></tr> 443 * <tr><td>f1</td><td>http://www.apache.org</td></tr> 444 * </table> 445 * </td> 446 * </tr> 447 * </table> 448 * 449 * @return This object. 450 */ 451 public SELF disableDetectLinksInStrings() { 452 return disableDetectLinksInStrings(true); 453 } 454 455 /** 456 * Same as {@link #disableDetectLinksInStrings()} but allows you to explicitly specify the value. 457 * 458 * @param value The value for this setting. 459 * @return This object. 460 */ 461 public SELF disableDetectLinksInStrings(boolean value) { 462 disableDetectLinksInStrings = value; 463 return self(); 464 } 465 466 @Override /* Overridden from Context.Builder<?> */ 467 public HashKey hashKey() { 468 // @formatter:off 469 return HashKey.of( 470 super.hashKey(), 471 addBeanTypesHtml, 472 addKeyValueTableHeaders, 473 disableDetectLabelParameters, 474 disableDetectLinksInStrings, 475 labelParameter, 476 uriAnchorText 477 ); 478 // @formatter:on 479 } 480 481 /** 482 * <i><l>HtmlSerializer</l> configuration property: </i> Link label parameter name. 483 * 484 * <p> 485 * The parameter name to look for when resolving link labels}. 486 * 487 * @param value 488 * The new value for this property. 489 * <br>Cannot be <jk>null</jk>. 490 * <br>The default is <js>"label"</js>. 491 * @return This object. 492 */ 493 public SELF labelParameter(String value) { 494 labelParameter = reqnn("value", value); 495 return self(); 496 } 497 498 /** 499 * <i><l>HtmlSerializer</l> configuration property: </i> Anchor text source. 500 * 501 * <p> 502 * When creating anchor tags (e.g. <code><xt><a</xt> <xa>href</xa>=<xs>'...'</xs> 503 * <xt>></xt>text<xt></a></xt></code>) in HTML, this setting defines what to set the inner text to. 504 * 505 * <p> 506 * The possible values are: 507 * <ul> 508 * <li class='jc'>{@link AnchorText} 509 * <ul> 510 * <li class='jf'>{@link AnchorText#TO_STRING TO_STRING} (default) - Set to whatever is returned by {@link #toString()} on the object. 511 * <br> 512 * <h5 class='section'>Example:</h5> 513 * <p class='bjson'> 514 * <jc>// Our bean class with a URI property.</jc> 515 * <jk>public class</jk> MyBean { 516 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"http://www.apache.org?foo=bar#myAnchor"</js>); 517 * } 518 * 519 * <jc>// Serializer with TO_STRING anchor text.</jc> 520 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer.<jsm>create</jsm>().anchorText(<jsf>TO_STRING</jsf>).build(); 521 * 522 * <jc>// Produces: <a href='http://www.apache.org?foo=bar#myAnchor'>http://www.apache.org?foo=bar#myAnchor</a></jc> 523 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 524 * </p> 525 * <li class='jf'>{@link AnchorText#PROPERTY_NAME PROPERTY_NAME} - Set to the bean property name. 526 * <br> 527 * <h5 class='section'>Example:</h5> 528 * <p class='bjson'> 529 * <jc>// Our bean class with a URI property.</jc> 530 * <jk>public class</jk> MyBean { 531 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"http://www.apache.org?foo=bar#myAnchor"</js>); 532 * } 533 * 534 * <jc>// Serializer with PROPERTY_NAME anchor text.</jc> 535 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer.<jsm>create</jsm>().anchorText(<jsf>PROPERTY_NAME</jsf>).build(); 536 * 537 * <jc>// Produces: <a href='http://www.apache.org?foo=bar#myAnchor'>f1</a></jc> 538 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 539 * </p> 540 * <li class='jf'>{@link AnchorText#URI URI} - Set to the URI value. 541 * <br> 542 * <h5 class='section'>Example:</h5> 543 * <p class='bjson'> 544 * <jc>// Our bean class with a URI property.</jc> 545 * <jk>public class</jk> MyBean { 546 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"http://www.apache.org?foo=bar#myAnchor"</js>); 547 * } 548 * 549 * <jc>// Serializer with URI anchor text.</jc> 550 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer.<jsm>create</jsm>().anchorText(<jsf>URI</jsf>).build(); 551 * 552 * <jc>// Produces: <a href='http://www.apache.org?foo=bar#myAnchor'>http://www.apache.org?foo=bar</a></jc> 553 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 554 * </p> 555 * <li class='jf'>{@link AnchorText#LAST_TOKEN LAST_TOKEN} - Set to the last token of the URI value. 556 * <br> 557 * <h5 class='section'>Example:</h5> 558 * <p class='bjson'> 559 * <jc>// Our bean class with a URI property.</jc> 560 * <jk>public class</jk> MyBean { 561 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"http://www.apache.org/foo/bar?baz=qux#myAnchor"</js>); 562 * } 563 * 564 * <jc>// Serializer with LAST_TOKEN anchor text.</jc> 565 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer.<jsm>create</jsm>().anchorText(<jsf>LAST_TOKEN</jsf>).build(); 566 * 567 * <jc>// Produces: <a href='http://www.apache.org/foo/bar?baz=qux#myAnchor'>bar</a></jc> 568 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 569 * </p> 570 * <li class='jf'>{@link AnchorText#URI_ANCHOR URI_ANCHOR} - Set to the anchor of the URL. 571 * <br> 572 * <h5 class='section'>Example:</h5> 573 * <p class='bjson'> 574 * <jc>// Our bean class with a URI property.</jc> 575 * <jk>public class</jk> MyBean { 576 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"http://www.apache.org/foo/bar?baz=qux#myAnchor"</js>); 577 * } 578 * 579 * <jc>// Serializer with URI_ANCHOR anchor text.</jc> 580 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer.<jsm>create</jsm>().anchorText(<jsf>URI_ANCHOR</jsf>).build(); 581 * 582 * <jc>// Produces: <a href='http://www.apache.org/foo/bar?baz=qux#myAnchor'>myAnchor</a></jc> 583 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 584 * </p> 585 * <li class='jf'>{@link AnchorText#CONTEXT_RELATIVE CONTEXT_RELATIVE} - Same as {@link AnchorText#TO_STRING TO_STRING} but assumes it's a context-relative path. 586 * <br> 587 * <h5 class='section'>Example:</h5> 588 * <p class='bjson'> 589 * <jc>// Our bean class with a URI property.</jc> 590 * <jk>public class</jk> MyBean { 591 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"bar/baz"</js>); 592 * } 593 * 594 * <jc>// Serializer with CONTEXT_RELATIVE anchor text.</jc> 595 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer 596 * .<jsm>create</jsm>() 597 * .anchorText(<jsf>CONTEXT_RELATIVE</jsf>) 598 * .uriResolution(<jsf>ROOT_RELATIVE</jsf>) 599 * .uriRelativity(<jsf>RESOURCE</jsf>) 600 * .uriContext(<js>"{authority:'http://localhost:10000',contextRoot:'/myContext',servletPath:'/myServlet',pathInfo:'/foo'}"</js>) 601 * .build(); 602 * 603 * <jc>// Produces: <a href='/myContext/myServlet/bar/baz'>myServlet/bar/baz</a></jc> 604 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 605 * </p> 606 * <li class='jf'>{@link AnchorText#SERVLET_RELATIVE SERVLET_RELATIVE} - Same as {@link AnchorText#TO_STRING TO_STRING} but assumes it's a servlet-relative path. 607 * <br> 608 * <h5 class='section'>Example:</h5> 609 * <p class='bjson'> 610 * <jc>// Our bean class with a URI property.</jc> 611 * <jk>public class</jk> MyBean { 612 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"bar/baz"</js>); 613 * } 614 * 615 * <jc>// Serializer with SERVLET_RELATIVE anchor text.</jc> 616 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer 617 * .<jsm>create</jsm>() 618 * .anchorText(<jsf>SERVLET_RELATIVE</jsf>) 619 * .uriResolution(<jsf>ROOT_RELATIVE</jsf>) 620 * .uriRelativity(<jsf>RESOURCE</jsf>) 621 * .uriContext(<js>"{authority:'http://localhost:10000',contextRoot:'/myContext',servletPath:'/myServlet',pathInfo:'/foo'}"</js>) 622 * .build(); 623 * 624 * <jc>// Produces: <a href='/myContext/myServlet/bar/baz'>bar/baz</a></jc> 625 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 626 * </p> 627 * <li class='jf'>{@link AnchorText#PATH_RELATIVE PATH_RELATIVE} - Same as {@link AnchorText#TO_STRING TO_STRING} but assumes it's a path-relative path. 628 * <br> 629 * <h5 class='section'>Example:</h5> 630 * <p class='bjson'> 631 * <jc>// Our bean class with a URI property.</jc> 632 * <jk>public class</jk> MyBean { 633 * <jk>public</jk> URI <jf>f1</jf> = URI.<jsm>create</jsm>(<js>"bar/baz"</js>); 634 * } 635 * 636 * <jc>// Serializer with PATH_RELATIVE anchor text.</jc> 637 * WriterSerializer <jv>serializer1</jv> = HtmlSerializer 638 * .<jsm>create</jsm>() 639 * .anchorText(<jsf>PATH_RELATIVE</jsf>) 640 * .uriResolution(<jsf>ROOT_RELATIVE</jsf>) 641 * .uriRelativity(<jsf>PATH_INFO</jsf>) 642 * .uriContext(<js>"{authority:'http://localhost:10000',contextRoot:'/myContext',servletPath:'/myServlet',pathInfo:'/foo'}"</js>) 643 * .build(); 644 * 645 * <jc>// Produces: <a href='/myContext/myServlet/foo/bar/baz'>bar/baz</a></jc> 646 * String <jv>html</jv> = <jv>serializer1</jv>.write(<jk>new</jk> MyBean()); 647 * </p> 648 * </ul> 649 * </ul> 650 * 651 * @param value 652 * The new value for this property. 653 * <br>Cannot be <jk>null</jk>. 654 * <br>The default is {@link AnchorText#TO_STRING}. 655 * @return This object. 656 */ 657 public SELF uriAnchorText(AnchorText value) { 658 uriAnchorText = reqnn("value", value); 659 return self(); 660 } 661 662 663 } 664 665 /** 666 * Concrete default builder leaf for the non-subclassed {@link HtmlSerializer#create()} / {@link HtmlSerializer#copy()} path. 667 */ 668 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 669 670 DefaultBuilder() {} 671 672 DefaultBuilder(HtmlSerializer copyFrom) { 673 super(copyFrom); 674 } 675 676 DefaultBuilder(Builder<?> copyFrom) { 677 super(copyFrom); 678 } 679 680 @Override /* Overridden from Context.Builder<?> */ 681 public DefaultBuilder copy() { 682 return new DefaultBuilder(this); 683 } 684 } 685 686 /** Default serializer, single quotes. */ 687 public static class Sq extends HtmlSerializer { 688 689 /** 690 * Constructor. 691 * 692 * @param builder The builder for this object. 693 */ 694 public Sq(Builder<?> builder) { 695 super(builder.quoteChar('\'')); 696 } 697 } 698 699 /** Default serializer, single quotes, whitespace added. */ 700 public static class SqReadable extends HtmlSerializer { 701 702 /** 703 * Constructor. 704 * 705 * @param builder The builder for this object. 706 */ 707 public SqReadable(Builder<?> builder) { 708 super(builder.quoteChar('\'').useWhitespace()); 709 } 710 } 711 712 /** Default serializer, all default settings. */ 713 public static final HtmlSerializer DEFAULT = new HtmlSerializer(create()); 714 715 /** Default serializer, single quotes. */ 716 public static final HtmlSerializer DEFAULT_SQ = new HtmlSerializer.Sq(create()); 717 /** Default serializer, single quotes, whitespace added. */ 718 public static final HtmlSerializer DEFAULT_SQ_READABLE = new HtmlSerializer.SqReadable(create()); 719 720 /** Default serializer, single quotes, simplified (no JSON type tags on strings). */ 721 public static final HtmlSerializer DEFAULT_SIMPLE_SQ = new HtmlSerializer(create().sq().disableJsonTags()); 722 723 /** 724 * Creates a new builder for this object. 725 * 726 * @return A new builder. 727 */ 728 @SuppressWarnings({ 729 "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type 730 }) 731 public static Builder<?> create() { 732 return new DefaultBuilder(); 733 } 734 735 protected final boolean addBeanTypesHtml; 736 protected final boolean addKeyValueTableHeaders; 737 protected final boolean detectLabelParameters; 738 protected final boolean detectLinksInStrings; 739 protected final AnchorText uriAnchorText; 740 protected final String labelParameter; 741 742 private final Map<ClassMeta<?>,HtmlClassMeta> htmlClassMetas = new ConcurrentHashMap<>(); 743 private final Map<BeanPropertyMeta,HtmlBeanPropertyMeta> htmlBeanPropertyMetas = new ConcurrentHashMap<>(); 744 745 private final AtomicReference<HtmlSchemaSerializer> schemaSerializer = new AtomicReference<>(); 746 747 /** 748 * Constructor. 749 * 750 * @param builder The builder for this object. 751 */ 752 public HtmlSerializer(Builder<?> builder) { 753 super(builder); 754 addBeanTypesHtml = builder.addBeanTypesHtml; 755 addKeyValueTableHeaders = builder.addKeyValueTableHeaders; 756 detectLabelParameters = ! builder.disableDetectLabelParameters; 757 detectLinksInStrings = ! builder.disableDetectLinksInStrings; 758 labelParameter = builder.labelParameter; 759 uriAnchorText = builder.uriAnchorText; 760 } 761 762 @Override /* Overridden from Context */ 763 public Builder<?> copy() { 764 return new DefaultBuilder(this); 765 } 766 767 @Override /* Overridden from Context */ 768 public HtmlSerializerSession.Builder<?> createSession() { 769 return HtmlSerializerSession.create(this); 770 } 771 772 @Override /* Overridden from HtmlMetaProvider */ 773 public HtmlBeanPropertyMeta getHtmlBeanPropertyMeta(BeanPropertyMeta bpm) { 774 if (bpm == null) 775 return HtmlBeanPropertyMeta.DEFAULT; 776 return htmlBeanPropertyMetas.computeIfAbsent(bpm, k -> new HtmlBeanPropertyMeta(k.getDelegateFor(), this.getAnnotationProvider(), this)); 777 } 778 779 @Override /* Overridden from HtmlMetaProvider */ 780 public HtmlClassMeta getHtmlClassMeta(ClassMeta<?> cm) { 781 return htmlClassMetas.computeIfAbsent(cm, k -> new HtmlClassMeta(k, this)); 782 } 783 784 /** 785 * Returns the schema serializer. 786 * 787 * @return The schema serializer. 788 */ 789 public HtmlSerializer getSchemaSerializer() { 790 HtmlSchemaSerializer result = schemaSerializer.get(); 791 if (result == null) { 792 result = HtmlSchemaSerializer.create().marshallingContext(getMarshallingContext()).build(); 793 if (! schemaSerializer.compareAndSet(null, result)) { 794 result = schemaSerializer.get(); 795 } 796 } 797 return result; 798 } 799 800 @Override /* Overridden from Context */ 801 public HtmlSerializerSession getSession() { return createSession().build(); } 802 803 /** 804 * Link label parameter name. 805 * 806 * @see Builder#labelParameter(String) 807 * @return 808 * The parameter name to look for when resolving link labels. 809 */ 810 protected final String getLabelParameter() { return labelParameter; } 811 812 /** 813 * Anchor text source. 814 * 815 * @see Builder#uriAnchorText(AnchorText) 816 * @return 817 * When creating anchor tags (e.g. <code><xt><a</xt> <xa>href</xa>=<xs>'...'</xs> 818 * <xt>></xt>text<xt></a></xt></code>) in HTML, this setting defines what to set the inner text to. 819 */ 820 protected final AnchorText getUriAnchorText() { return uriAnchorText; } 821 822 /** 823 * Add <js>"_type"</js> properties when needed. 824 * 825 * @see Builder#addBeanTypesHtml() 826 * @return 827 * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred 828 * through reflection. 829 */ 830 @Override 831 protected final boolean isAddBeanTypes() { return addBeanTypesHtml || super.isAddBeanTypes(); } 832 833 /** 834 * Add key/value headers on bean/map tables. 835 * 836 * @see Builder#addKeyValueTableHeaders() 837 * @return 838 * <jk>true</jk> if <bc>key</bc> and <bc>value</bc> column headers are added to tables. 839 */ 840 protected final boolean isAddKeyValueTableHeaders() { return addKeyValueTableHeaders; } 841 842 /** 843 * Look for link labels in URIs. 844 * 845 * @see Builder#disableDetectLabelParameters() 846 * @return 847 * <jk>true</jk> if we should look for URL label parameters (e.g. <js>"?label=foobar"</js>). 848 */ 849 protected final boolean isDetectLabelParameters() { return detectLabelParameters; } 850 851 /** 852 * Look for URLs in {@link String Strings}. 853 * 854 * @see Builder#disableDetectLinksInStrings() 855 * @return 856 * <jk>true</jk> if we should automatically convert strings to URLs if they look like a URL. 857 */ 858 protected final boolean isDetectLinksInStrings() { return detectLinksInStrings; } 859 860 @Override /* Overridden from XmlSerializer */ 861 protected FluentMap<String,Object> properties() { 862 return super.properties() 863 .a("addBeanTypesHtml", addBeanTypesHtml) 864 .a("addKeyValueTableHeaders", addKeyValueTableHeaders) 865 .a("detectLabelParameters", detectLabelParameters) 866 .a("detectLinksInStrings", detectLinksInStrings) 867 .a("labelParameter", labelParameter) 868 .a("uriAnchorText", uriAnchorText); 869 } 870}