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    }