001 /* 002 * Common usable utilities 003 * 004 * Copyright (c) 2006 Petr Hadraba <hadrabap@gmail.com> 005 * 006 * Author: Petr Hadraba 007 * 008 * -- 009 * 010 * XML Utilities 011 */ 012 013 package global.sandbox.xmlutilities; 014 015 import java.io.Serializable; 016 import java.nio.charset.Charset; 017 import java.util.ArrayList; 018 import java.util.Collections; 019 import java.util.HashMap; 020 import java.util.List; 021 import java.util.Map; 022 import java.util.Properties; 023 import javax.xml.transform.OutputKeys; 024 import javax.xml.transform.Transformer; 025 import org.w3c.dom.Element; 026 027 /** 028 * Convenient harness for configuration of {@link Transformer Transformers'} output intent. 029 * 030 * This class supports the following keys: 031 * <ul> 032 * <li>{@code method}, {@link OutputKeys#METHOD}</li> 033 * <li>{@code version}, {@link OutputKeys#VERSION}</li> 034 * <li>{@code encoding}, {@link OutputKeys#ENCODING}</li> 035 * <li>{@code omit-xml-declaration}, {@link OutputKeys#OMIT_XML_DECLARATION}</li> 036 * <li>{@code standalone}, {@link OutputKeys#STANDALONE}</li> 037 * <li>{@code doctype-public}, {@link OutputKeys#DOCTYPE_PUBLIC}</li> 038 * <li>{@code doctype-system}, {@link OutputKeys#DOCTYPE_SYSTEM}</li> 039 * <li>{@code cdata-section-elements}, {@link OutputKeys#CDATA_SECTION_ELEMENTS}</li> 040 * <li>{@code indent}, {@link OutputKeys#INDENT}</li> 041 * <li>{@code media-type}, {@link OutputKeys#MEDIA_TYPE}</li> 042 * </ul> 043 * 044 * @author Petr Hadraba 045 * 046 * @version 1.2 047 */ 048 public class OutputFormat implements Serializable { 049 050 private static final long serialVersionUID = 1L; 051 052 /** 053 * Default version for XML output method. 054 */ 055 public static final String XML_VERSION_1_0; 056 057 /** 058 * Default version for HTML output method. 059 */ 060 public static final String HTML_VERSION_4_0; 061 062 /** 063 * List of supported keys by this class. 064 */ 065 private static final List<String> SUPPORTED_KEYS; 066 067 static { 068 XML_VERSION_1_0 = "1.0"; 069 HTML_VERSION_4_0 = "4.0"; 070 071 final List<String> supportedKeys = new ArrayList<String>(); 072 supportedKeys.add(OutputKeys.METHOD); 073 supportedKeys.add(OutputKeys.VERSION); 074 supportedKeys.add(OutputKeys.ENCODING); 075 supportedKeys.add(OutputKeys.OMIT_XML_DECLARATION); 076 supportedKeys.add(OutputKeys.STANDALONE); 077 supportedKeys.add(OutputKeys.DOCTYPE_PUBLIC); 078 supportedKeys.add(OutputKeys.DOCTYPE_SYSTEM); 079 supportedKeys.add(OutputKeys.CDATA_SECTION_ELEMENTS); 080 supportedKeys.add(OutputKeys.INDENT); 081 supportedKeys.add(OutputKeys.MEDIA_TYPE); 082 SUPPORTED_KEYS = Collections.unmodifiableList(supportedKeys); 083 } 084 085 /** 086 * output method. 087 */ 088 private OutputMethod method; 089 090 /** 091 * version. 092 */ 093 private String version; 094 095 /** 096 * encoding. 097 */ 098 private String encoding; 099 100 /** 101 * omit XML Declaration flag. 102 */ 103 private Boolean omitXmlDeclaration; 104 105 /** 106 * standalone flag. 107 */ 108 private Boolean standalone; 109 110 /** 111 * DOCTYPE PUBLIC. 112 */ 113 private String doctypePublic; 114 115 /** 116 * DOCTYPE SYSTEM. 117 */ 118 private String doctypeSystem; 119 120 /** 121 * CDATA section element list. 122 */ 123 private final List<String> cdataSectionElements = new ArrayList<String>(); 124 125 /** 126 * Indent flag. 127 */ 128 private Boolean indent; 129 130 /** 131 * Media type. 132 */ 133 private String mediaType; 134 135 /** 136 * Custom properties. 137 */ 138 private final Map<String, String> customProperties = new HashMap<String, String>(); 139 140 /** 141 * Convenient interface for getting rendered codes. 142 */ 143 private interface ConfigurationValue { 144 145 /** 146 * Returns code required by {@link Transformer#setOutputProperties(java.util.Properties) Transformer#setOutputProperties(java.util.Properties)}. 147 * 148 * @return text 149 */ 150 String getValue(); 151 152 } 153 154 /** 155 * Typed representation of output method with appropriate and expected values by {@link Transformer Transformer}. 156 * 157 * @author Petr Hadraba 158 * 159 * @version 1.2 160 */ 161 public enum OutputMethod implements ConfigurationValue { 162 163 /** 164 * XML output method. 165 */ 166 XML("xml"), 167 168 /** 169 * HTML output method. 170 */ 171 HTML("html"), 172 173 /** 174 * Plain text output method. 175 */ 176 TEXT("text"), 177 178 /** 179 * Custom method. 180 */ 181 CUSTOM(null); 182 183 /** 184 * Default code. 185 */ 186 private final String defaultCode; 187 188 /** 189 * Used for custom value. 190 */ 191 private String value; 192 193 /** 194 * Default constructor for default values. 195 * 196 * @param defaultCode code 197 */ 198 OutputMethod(final String defaultCode) { 199 this.defaultCode = defaultCode; 200 } 201 202 /** 203 * {@inheritDoc} 204 */ 205 @Override 206 public String getValue() { 207 if (this == CUSTOM) { 208 if (value == null) { 209 throw new IllegalStateException(String.format("Missing value for type %s.", this.name())); 210 } 211 return value; 212 } 213 return defaultCode; 214 } 215 216 /** 217 * Sets value for custom method. 218 * 219 * @param code code 220 */ 221 public void setValue(final String code) { 222 if (this != CUSTOM) { 223 throw new IllegalStateException(String.format("Cannot set value for type %s.", this.name())); 224 } 225 if (code == null) { 226 throw new IllegalArgumentException("The code cannot be null."); 227 } 228 this.value = code; 229 } 230 231 } 232 233 /** 234 * Represents Boolean value with appropriate value expected by {@link Transformer Transformer}. 235 * 236 * @author Petr Hadraba 237 * 238 * @version 1.2 239 */ 240 public enum Boolean implements ConfigurationValue { 241 242 /** 243 * YES. 244 */ 245 YES("yes"), 246 247 /** 248 * NO. 249 */ 250 NO("no"), 251 252 /** 253 * YES. 254 */ 255 TRUE("yes"), 256 257 /** 258 * NO. 259 */ 260 FALSE("no"); 261 262 /** 263 * Appropriate code. 264 */ 265 private final String value; 266 267 /** 268 * Constructor for default values. 269 * 270 * @param value default code 271 */ 272 Boolean(final String value) { 273 this.value = value; 274 } 275 276 /** 277 * {@inheritDoc} 278 */ 279 @Override 280 public String getValue() { 281 return value; 282 } 283 284 } 285 286 /** 287 * Builder for declarative construction of {@link OutputFormat OutputFormat}. 288 * 289 * @author Petr Hadraba 290 * 291 * @version 1.2 292 */ 293 public static final class Builder { 294 295 /** 296 * Under-layering output format. 297 */ 298 private final OutputFormat outputFormat; 299 300 /** 301 * Creates new Builder with valid output method. 302 * 303 * @param method method 304 * @param customCode custom code for {@link OutputMethod#CUSTOM CUSTOM} 305 */ 306 private Builder(final OutputMethod method, final String customCode) { 307 this.outputFormat = new OutputFormat(); 308 this.outputFormat.setMethod(method); 309 if (method == OutputMethod.CUSTOM) { 310 this.outputFormat.getMethod().setValue(customCode); 311 } 312 } 313 314 /** 315 * Returns under-layering Output Format object. 316 * 317 * @return output format 318 */ 319 public OutputFormat build() { 320 return outputFormat; 321 } 322 323 /** 324 * Creates Properties ready to use in 325 * {@link Transformer#setOutputProperties(java.util.Properties) setOutputProperties(java.util.Properties)}. 326 * 327 * @return Properties 328 */ 329 public Properties buildToTransformerOutputProperties() { 330 return outputFormat.toTransformerOutputProperties(); 331 } 332 333 /** 334 * Sets version. 335 * 336 * @param version text 337 * 338 * @return Builder 339 */ 340 public Builder setVersion(final String version) { 341 if (version == null) { 342 throw new IllegalArgumentException("version can't be null."); 343 } 344 this.outputFormat.setVersion(version); 345 return this; 346 } 347 348 /** 349 * Sets default version for XML and HTML output method. 350 * 351 * @return Builder 352 */ 353 public Builder withDefaultVersion() { 354 switch (this.outputFormat.getMethod()) { 355 case XML: 356 return setVersion(XML_VERSION_1_0); 357 case HTML: 358 return setVersion(HTML_VERSION_4_0); 359 default: 360 throw new IllegalStateException(String.format("Cannot set default version for method %s.", this.outputFormat.getMethod().name())); 361 } 362 } 363 364 /** 365 * Sets encoding. 366 * 367 * @param encoding text 368 * 369 * @return Builder 370 */ 371 public Builder setEncoding(final String encoding) { 372 if (encoding == null) { 373 throw new IllegalArgumentException("encoding can't be null."); 374 } 375 this.outputFormat.setEncoding(encoding); 376 return this; 377 } 378 379 /** 380 * Sets encoding from specified Charset. 381 * 382 * @param charset Charset to use 383 * 384 * @return Builder 385 */ 386 public Builder setEncoding(final Charset charset) { 387 if (charset == null) { 388 throw new IllegalArgumentException("charset can't be null."); 389 } 390 return setEncoding(charset.name()); 391 } 392 393 /** 394 * Sets omit XML Declaration flag. 395 * 396 * @param state new state 397 * 398 * @return Builder 399 */ 400 public Builder setOmitXmlDeclaration(final Boolean state) { 401 if (state == null) { 402 throw new IllegalArgumentException("omitXmlDeclaration can't be null."); 403 } 404 this.outputFormat.setOmitXmlDeclaration(state); 405 return this; 406 } 407 408 /** 409 * Sets omit XML Declaration flag to {@code yes}. 410 * 411 * @return Builder 412 */ 413 public Builder omitXmlDeclaration() { 414 return setOmitXmlDeclaration(Boolean.YES); 415 } 416 417 /** 418 * Sets omit XML Declaration flag to {@code no}. 419 * 420 * @return Builder 421 */ 422 public Builder dontOmitXmlDeclaration() { 423 return setOmitXmlDeclaration(Boolean.NO); 424 } 425 426 /** 427 * Sets standalone flag. 428 * 429 * @param state new state 430 * 431 * @return Builder 432 */ 433 public Builder setStandalone(final Boolean state) { 434 if (state == null) { 435 throw new IllegalArgumentException("standalone can't be null."); 436 } 437 this.outputFormat.setStandalone(state); 438 return this; 439 } 440 441 /** 442 * Sets standalone flag. 443 * 444 * @return Builder 445 */ 446 public Builder asStandalone() { 447 return setStandalone(Boolean.YES); 448 } 449 450 /** 451 * Sets DOCTYPE PUBLIC 452 * 453 * @param doctype DOCTYPE PUBLIC 454 * 455 * @return Builder 456 */ 457 public Builder setDoctypePublic(final String doctype) { 458 if (doctype == null) { 459 throw new IllegalArgumentException("doctype PUBLIC can't be null."); 460 } 461 this.outputFormat.setDoctypePublic(doctype); 462 return this; 463 } 464 465 /** 466 * Sets DOCTYPE SYSTEM. 467 * 468 * @param doctype DOCTYPE SYSTEM 469 * 470 * @return Builder 471 */ 472 public Builder setDoctypeSystem(final String doctype) { 473 if (doctype == null) { 474 throw new IllegalArgumentException("doctype SYSTEM can't be null."); 475 } 476 this.outputFormat.setDoctypeSystem(doctype); 477 return this; 478 } 479 480 /** 481 * Adds element name as is into CDATA section element list. 482 * 483 * @param elementName name to add 484 * 485 * @return Builder 486 */ 487 public Builder addCdataSectionElement(final String elementName) { 488 if (elementName == null) { 489 throw new IllegalArgumentException("elementName can't be null."); 490 } 491 this.outputFormat.addCdataSectionElement(elementName); 492 return this; 493 } 494 495 /** 496 * Adds specified local name with optional name space URI into CDATA section element list. 497 * 498 * @param namespaceUri optional name space URI 499 * @param localName element name 500 * 501 * @return Builder 502 */ 503 public Builder addCdataSectionElement(final String namespaceUri, String localName) { 504 if (localName == null) { 505 throw new IllegalArgumentException("localName can't be null."); 506 } 507 if (namespaceUri == null) { 508 return addCdataSectionElement(localName); 509 } else { 510 return addCdataSectionElement(String.format("{%s}%s", namespaceUri, localName)); 511 } 512 } 513 514 /** 515 * Adds specified {@link Element Element} into CDATA section element list. The element is resolved to 516 * qualified name. 517 * 518 * @param element element to add 519 * 520 * @return Builder 521 */ 522 public Builder addCdataSectionElement(final Element element) { 523 return addCdataSectionElement(element.getNamespaceURI(), element.getLocalName()); 524 } 525 526 /** 527 * Sets indent flag. 528 * 529 * @param state state 530 * 531 * @return Builder 532 */ 533 public Builder setIndent(final Boolean state) { 534 if (state == null) { 535 throw new IllegalArgumentException("state can't be null."); 536 } 537 this.outputFormat.setIndent(state); 538 return this; 539 } 540 541 /** 542 * Sets indent flag. 543 * 544 * @return Builder 545 */ 546 public Builder indent() { 547 return setIndent(Boolean.YES); 548 } 549 550 /** 551 * Sets media type. 552 * 553 * @param mediaType media type 554 * 555 * @return Builder 556 */ 557 public Builder setMediaType(final String mediaType) { 558 if (mediaType == null) { 559 throw new IllegalArgumentException("mediaType can't be null."); 560 } 561 this.outputFormat.setMediaType(mediaType); 562 return this; 563 } 564 565 /** 566 * Adds custom property which does not exist already and is not one of the properties managed by this builder. 567 * 568 * @param key key 569 * @param value value 570 * 571 * @return Builder 572 */ 573 public Builder withCustomProperty(final String key, final String value) { 574 if (key == null) { 575 throw new IllegalArgumentException("key can't be null."); 576 } 577 if (value == null) { 578 throw new IllegalArgumentException("value can't be null."); 579 } 580 if (SUPPORTED_KEYS.contains(key)) { 581 throw new IllegalArgumentException(String.format("The key '%s' is directly controlled by Builder.", key)); 582 } 583 this.outputFormat.addCustomProperty(key, value); 584 return this; 585 } 586 587 } 588 589 /** 590 * Creates builder for XML method intent. 591 * 592 * @return Builder 593 */ 594 public static Builder newXmlMethodBuilder() { 595 return new Builder(OutputMethod.XML, null); 596 } 597 598 /** 599 * Creates builder for HTML method intent. 600 * 601 * @return Builder 602 */ 603 public static Builder newHtmlMethodBuilder() { 604 return new Builder(OutputMethod.HTML, null); 605 } 606 607 /** 608 * Creates builder for Text method intent. 609 * 610 * @return Builder 611 */ 612 public static Builder newTextMethodBuilder() { 613 return new Builder(OutputMethod.TEXT, null); 614 } 615 616 /** 617 * Creates builder for custom method. 618 * 619 * @param method mandatory name of the custom method 620 * 621 * @return Builder 622 */ 623 public static Builder newCustomMethodBuilder(String method) { 624 return new Builder(OutputMethod.CUSTOM, method); 625 } 626 627 /** 628 * Renders {@code this} class into {@link Properties Properties} ready to be used by 629 * {@link Transformer#setOutputProperties(java.util.Properties) setOutputProperties(java.util.Properties)}. 630 * 631 * @return Properties 632 */ 633 public Properties toTransformerOutputProperties() { 634 Properties result = new Properties(); 635 636 addPropertyIfDefined(result, OutputKeys.METHOD, method); 637 addPropertyIfDefined(result, OutputKeys.VERSION, version); 638 addPropertyIfDefined(result, OutputKeys.ENCODING, encoding); 639 addPropertyIfDefined(result, OutputKeys.OMIT_XML_DECLARATION, omitXmlDeclaration); 640 addPropertyIfDefined(result, OutputKeys.STANDALONE, standalone); 641 addPropertyIfDefined(result, OutputKeys.DOCTYPE_PUBLIC, doctypePublic); 642 addPropertyIfDefined(result, OutputKeys.DOCTYPE_SYSTEM, doctypeSystem); 643 addPropertyIfDefined(result, OutputKeys.INDENT, indent); 644 addPropertyIfDefined(result, OutputKeys.MEDIA_TYPE, mediaType); 645 addPropertyIfDefined(result, OutputKeys.CDATA_SECTION_ELEMENTS, renderCdataSectionElementList(cdataSectionElements)); 646 result.putAll(customProperties); 647 648 return result; 649 } 650 651 /** 652 * Adds property if value is not {@code null}. 653 * 654 * @param result target map 655 * @param key key 656 * @param value value 657 */ 658 private static void addPropertyIfDefined(final Properties result, final String key, final String value) { 659 if (value != null) { 660 result.put(key, value); 661 } 662 } 663 664 /** 665 * Adds property if value is not {@code null}. 666 * 667 * @param result target map 668 * @param key key 669 * @param value value 670 */ 671 private static void addPropertyIfDefined(final Properties result, final String key, final ConfigurationValue value) { 672 if (value != null) { 673 result.put(key, value.getValue()); 674 } 675 } 676 677 /** 678 * Renders list of CDATA section elements into property value text. 679 * 680 * @param elements list of CDATA section elements 681 * 682 * @return text 683 */ 684 private static String renderCdataSectionElementList(final List<String> elements) { 685 if (elements.isEmpty()) { 686 return null; 687 } 688 689 StringBuilder sb = new StringBuilder(); 690 for (String element : elements) { 691 if (sb.length() > 0) { 692 sb.append(" "); 693 } 694 sb.append(element); 695 } 696 return sb.toString(); 697 } 698 699 /** 700 * Sets output method. 701 * 702 * @param method method 703 */ 704 public void setMethod(OutputMethod method) { 705 this.method = method; 706 } 707 708 /** 709 * Sets version. 710 * 711 * @param version version string 712 */ 713 public void setVersion(String version) { 714 this.version = version; 715 } 716 717 /** 718 * Sets encoding. 719 * 720 * @param encoding encoding name 721 */ 722 public void setEncoding(String encoding) { 723 this.encoding = encoding; 724 } 725 726 /** 727 * Sets omit XML declaration flag. 728 * 729 * @param omitXmlDeclaration state 730 */ 731 public void setOmitXmlDeclaration(Boolean omitXmlDeclaration) { 732 this.omitXmlDeclaration = omitXmlDeclaration; 733 } 734 735 /** 736 * Sets standalone flag. 737 * 738 * @param standalone state 739 */ 740 public void setStandalone(Boolean standalone) { 741 this.standalone = standalone; 742 } 743 744 /** 745 * Sets DOCTYPE PUBLIC. 746 * 747 * @param doctypePublic value 748 */ 749 public void setDoctypePublic(String doctypePublic) { 750 this.doctypePublic = doctypePublic; 751 } 752 753 /** 754 * Sets DOCTYPE SYSTEM. 755 * 756 * @param doctypeSystem value 757 */ 758 public void setDoctypeSystem(String doctypeSystem) { 759 this.doctypeSystem = doctypeSystem; 760 } 761 762 /** 763 * Sets indent flag. 764 * 765 * @param indent state 766 */ 767 public void setIndent(Boolean indent) { 768 this.indent = indent; 769 } 770 771 /** 772 * Sets media type. 773 * 774 * @param mediaType media type 775 */ 776 public void setMediaType(String mediaType) { 777 this.mediaType = mediaType; 778 } 779 780 /** 781 * Adds qualified element name into CDATA section element list. The element should not exist in the list already. 782 * 783 * @param elementName element to add 784 */ 785 public void addCdataSectionElement(String elementName) { 786 if (this.cdataSectionElements.contains(elementName)) { 787 throw new IllegalArgumentException(String.format("The element '%s' already specified.", elementName)); 788 } 789 this.cdataSectionElements.add(elementName); 790 } 791 792 /** 793 * Adds specified key-value property. The property should not be one of the properties directly supported by 794 * this class. 795 * 796 * @param key key 797 * @param value value 798 */ 799 public void addCustomProperty(final String key, final String value) { 800 if (customProperties.containsKey(key)) { 801 throw new IllegalArgumentException(String.format("The key '%s' is already defined.", key)); 802 } 803 this.customProperties.put(key, value); 804 } 805 806 /** 807 * Returns output method. 808 * 809 * @return output method 810 */ 811 public OutputMethod getMethod() { 812 return method; 813 } 814 815 /** 816 * Returns version. 817 * 818 * @return version 819 */ 820 public String getVersion() { 821 return version; 822 } 823 824 /** 825 * Returns encoding. 826 * 827 * @return encoding 828 */ 829 public String getEncoding() { 830 return encoding; 831 } 832 833 /** 834 * Returns omit XML declaration flag. 835 * 836 * @return omit XML declaration flag 837 */ 838 public Boolean getOmitXmlDeclaration() { 839 return omitXmlDeclaration; 840 } 841 842 /** 843 * Returns standalone. 844 * 845 * @return standalone 846 */ 847 public Boolean getStandalone() { 848 return standalone; 849 } 850 851 /** 852 * Returns DOCTYPE PUBLIC. 853 * 854 * @return DOCTYPE PUBLIC 855 */ 856 public String getDoctypePublic() { 857 return doctypePublic; 858 } 859 860 /** 861 * Returns DOCTYPE SYSTEM. 862 * 863 * @return DOCTYPE SYSTEM 864 */ 865 public String getDoctypeSystem() { 866 return doctypeSystem; 867 } 868 869 /** 870 * Returns list of CDATA section elements. 871 * 872 * @return list of CDATA section elements 873 */ 874 public List<String> getCdataSectionElements() { 875 return cdataSectionElements; 876 } 877 878 /** 879 * Returns indent. 880 * 881 * @return indent 882 */ 883 public Boolean getIndent() { 884 return indent; 885 } 886 887 /** 888 * Returns media type. 889 * 890 * @return media type 891 */ 892 public String getMediaType() { 893 return mediaType; 894 } 895 896 /** 897 * Returns custom properties. 898 * 899 * @return custom properties 900 */ 901 public Map<String, String> getCustomProperties() { 902 return customProperties; 903 } 904 905 }