001    /*--------------------------------------------------------------------------+
002    $Id: XMLWriter.java 26963 2010-03-17 22:28:50Z hummelb $
003    |                                                                          |
004    | Copyright 2005-2010 Technische Universitaet Muenchen                     |
005    |                                                                          |
006    | Licensed under the Apache License, Version 2.0 (the "License");          |
007    | you may not use this file except in compliance with the License.         |
008    | You may obtain a copy of the License at                                  |
009    |                                                                          |
010    |    http://www.apache.org/licenses/LICENSE-2.0                            |
011    |                                                                          |
012    | Unless required by applicable law or agreed to in writing, software      |
013    | distributed under the License is distributed on an "AS IS" BASIS,        |
014    | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
015    | See the License for the specific language governing permissions and      |
016    | limitations under the License.                                           |
017    +--------------------------------------------------------------------------*/
018    package edu.tum.cs.commons.xml;
019    
020    import static edu.tum.cs.commons.string.StringUtils.CR;
021    import static edu.tum.cs.commons.string.StringUtils.SPACE;
022    
023    import java.io.OutputStream;
024    import java.io.OutputStreamWriter;
025    import java.io.PrintWriter;
026    import java.io.UnsupportedEncodingException;
027    import java.util.EmptyStackException;
028    import java.util.HashSet;
029    import java.util.Stack;
030    
031    import edu.tum.cs.commons.filesystem.FileSystemUtils;
032    import edu.tum.cs.commons.string.StringUtils;
033    
034    /**
035     * Utility class for creating XML documents. Please consult test case
036     * {@link XMLWriterTest} to see how this class is intended to be used.
037     * 
038     * @author Florian Deissenboeck
039     * @author $Author: hummelb $
040     * @version $Rev: 26963 $
041     * @levd.rating GREEN Hash: E346D576888FEF33DA193173CEE8337B
042     */
043    public class XMLWriter<E extends Enum<E>, A extends Enum<A>> {
044    
045            /** This enunmeration describes the states of the writer. */
046            private enum EState {
047    
048                    /** Indicates that we are at the beginning of the document. */
049                    DOCUMENT_START,
050    
051                    /** Started a tag but did not close it yet. */
052                    INSIDE_TAG,
053    
054                    /** Inside a text element. */
055                    INSIDE_TEXT,
056    
057                    /** Indicates that we are between to tags but not within a text element. */
058                    OUTSIDE_TAG
059            }
060    
061            /** XML comment end symbol. */
062            private final static String COMMENT_END = " -->";
063    
064            /** XML comment start symbol. */
065            private final static String COMMENT_START = "<!-- ";
066    
067            /** Right angle bracket */
068            private final static String GT = ">";
069    
070            /** Left angle bracket */
071            private final static String LT = "<";
072    
073            /** Resolver used by the writer. */
074            protected final IXMLResolver<E, A> xmlResolver;
075    
076            /**
077             * This set maintains the attributes added to an element to detect duplicate
078             * attributes.
079             * <p>
080             * We can not use {@link java.util.EnumSet} here as we would need a
081             * reference to the defining class.
082             */
083            private final HashSet<A> currentAttributes = new HashSet<A>();
084    
085            /** The current nesting depth is used to calculate the ident. */
086            private int currentNestingDepth = -1;
087    
088            /**
089             * This stack maintains the elements in order of creation. It is used to
090             * check if elements are closed in the correct order.
091             */
092            private final Stack<E> elementStack = new Stack<E>();
093    
094            /** The current state of the writer. */
095            private EState state = EState.DOCUMENT_START;
096    
097            /** The writer to write to. */
098            private final PrintWriter writer;
099    
100            /** This flag indicates if line breaks should be generated or not. */
101            private boolean suppressLineBreaks = false;
102    
103            /**
104             * Create a new writer.
105             * 
106             * @param stream
107             *            the stream to write to.
108             * @param xmlResolver
109             *            resolvers used by this writer
110             */
111            public XMLWriter(OutputStream stream, IXMLResolver<E, A> xmlResolver) {
112                    try {
113                            this.writer = new PrintWriter(new OutputStreamWriter(stream,
114                                            FileSystemUtils.UTF8_ENCODING));
115                    } catch (UnsupportedEncodingException e) {
116                            throw new AssertionError("UTF-8 should always be supported!");
117                    }
118                    this.xmlResolver = xmlResolver;
119            }
120    
121            /**
122             * Create a new writer.
123             * 
124             * @param writer
125             *            the writer to write to.
126             * @param xmlResolver
127             *            resolvers used by this writer
128             */
129            public XMLWriter(PrintWriter writer, IXMLResolver<E, A> xmlResolver) {
130                    this.writer = writer;
131                    this.xmlResolver = xmlResolver;
132            }
133    
134            /**
135             * Toogle line break behavior. If set to <code>true</code> the writer does
136             * not write line breaks. If set to <code>false</code> (default) line breaks
137             * are written.
138             * <p>
139             * This can, for example, be used for HTML where line breaks sometimes
140             * change the layout.
141             */
142            public void setSuppressLineBreaks(boolean supressLineBreaks) {
143                    this.suppressLineBreaks = supressLineBreaks;
144            }
145    
146            /**
147             * Add an XML header.
148             * 
149             * @param version
150             *            version string
151             * @param encoding
152             *            encoding definition
153             */
154            public void addHeader(String version, String encoding) {
155                    if (state != EState.DOCUMENT_START) {
156                            throw new XMLWriterException(
157                                            "Can be called at the beginning of a document only.",
158                                            EXMLWriterExceptionType.HEADER_WITHIN_DOCUMENT);
159                    }
160                    print(LT);
161                    print("?xml version=\"");
162                    print(version);
163                    print("\" encoding=\"");
164                    print(encoding);
165                    print("\"?");
166                    print(GT);
167    
168                    state = EState.OUTSIDE_TAG;
169            }
170    
171            /**
172             * Add public document type definiton
173             * 
174             * @param rootElement
175             *            root element
176             * @param publicId
177             *            public id
178             * @param systemId
179             *            sytem id
180             */
181            public void addPublicDocTypeDefintion(E rootElement, String publicId,
182                            String systemId) {
183                    print(LT);
184                    print("!DOCTYPE ");
185                    print(xmlResolver.resolveElementName(rootElement));
186                    print(" PUBLIC \"");
187                    print(publicId);
188                    print("\" \"");
189                    print(systemId);
190                    print("\"");
191                    print(GT);
192    
193                    state = EState.OUTSIDE_TAG;
194            }
195    
196            /**
197             * Start a new element
198             * 
199             * @param element
200             *            the element to start.
201             */
202            public void openElement(E element) {
203                    if (state == EState.INSIDE_TAG) {
204                            println(GT);
205                    } else if (state == EState.OUTSIDE_TAG) {
206                            println();
207                    }
208    
209                    currentNestingDepth++;
210                    if (state != EState.INSIDE_TEXT) {
211                            printIndent();
212                    }
213                    print(LT);
214                    print(xmlResolver.resolveElementName(element));
215    
216                    state = EState.INSIDE_TAG;
217                    elementStack.push(element);
218                    currentAttributes.clear();
219            }
220    
221            /**
222             * Add a attribute. This only works if a element was started but no other
223             * elements were added yet.
224             * 
225             * @param attribute
226             *            the attribute to create
227             * @param value
228             *            its value
229             * @throws XMLWriterException
230             *             if there's no element to add attributes to (
231             *             {@link EXMLWriterExceptionType#ATTRIBUTE_OUTSIDE_ELEMENT}) or
232             *             if an attribute is added twice (
233             *             {@link EXMLWriterExceptionType#DUPLICATE_ATTRIBUTE}).
234             */
235            public void addAttribute(A attribute, Object value) {
236                    if (state != EState.INSIDE_TAG) {
237                            throw new XMLWriterException("Must be called for an open element.",
238                                            EXMLWriterExceptionType.ATTRIBUTE_OUTSIDE_ELEMENT);
239                    }
240    
241                    if (currentAttributes.contains(attribute)) {
242                            throw new XMLWriterException("Duplicate attribute.",
243                                            EXMLWriterExceptionType.DUPLICATE_ATTRIBUTE);
244                    }
245    
246                    print(SPACE);
247                    print(xmlResolver.resolveAttributeName(attribute));
248                    print("=");
249                    print("\"");
250                    print(escape(value.toString()));
251                    print("\"");
252    
253                    currentAttributes.add(attribute);
254            }
255    
256            /**
257             * Convenience method for adding an element together with (some of) its
258             * attributes.
259             * 
260             * @param element
261             *            The element to be opened (using {@link #openElement(Enum)}).
262             * @param attributes
263             *            the attributes to be added. The number of arguments must be
264             *            even, where the first, third, etc. argument is an attribute
265             *            enum.
266             */
267            public void openElement(E element, Object... attributes) {
268                    if (attributes.length % 2 != 0) {
269                            throw new XMLWriterException(
270                                            "Expected an even number of arguments!",
271                                            EXMLWriterExceptionType.ODD_NUMBER_OF_ARGUMENTS);
272                    }
273                    for (int i = 0; i < attributes.length; i += 2) {
274                            if (!xmlResolver.getAttributeClass().isAssignableFrom(
275                                            attributes[i].getClass())) {
276                                    throw new XMLWriterException("Attribute name (index " + i
277                                                    + ") must be of type "
278                                                    + xmlResolver.getAttributeClass().getName(),
279                                                    EXMLWriterExceptionType.ILLEGAL_ATTRIBUTE_TYPE);
280                            }
281                    }
282                    openElement(element);
283                    for (int i = 0; i < attributes.length; i += 2) {
284                            // this is ok as we checked it above
285                            @SuppressWarnings("unchecked")
286                            A a = (A) attributes[i];
287                            addAttribute(a, attributes[i + 1]);
288                    }
289            }
290    
291            /**
292             * Convenience method for adding an element together with (some of) its
293             * attributes. This is the same as {@link #openElement(Enum, Object[])}, but
294             * also closes the element.
295             */
296            public void addClosedElement(E element, Object... attributes) {
297                    openElement(element, attributes);
298                    closeElement(element);
299            }
300    
301            /**
302             * Convenience method for adding an element together with (some of) its
303             * attributes and text inbetween. This is the same as
304             * {@link #openElement(Enum, Object[])}, but then adds the provided text and
305             * closes the element.
306             */
307            public void addClosedTextElement(E element, String text,
308                            Object... attributes) {
309                    openElement(element, attributes);
310                    addText(text);
311                    closeElement(element);
312            }
313    
314            /**
315             * Close an element.
316             * 
317             * @param element
318             *            the element to close.
319             * @throws XMLWriterException
320             *             on attempt to close the wrong element (
321             *             {@link EXMLWriterExceptionType#UNCLOSED_ELEMENT}).
322             */
323            public void closeElement(E element) {
324                    if (element != elementStack.peek()) {
325                            throw new XMLWriterException("Must close element "
326                                            + elementStack.peek() + " first.",
327                                            EXMLWriterExceptionType.UNCLOSED_ELEMENT);
328                    }
329    
330                    if (state == EState.INSIDE_TAG) {
331                            // if inside a tag, just close the tag and done
332                            print(" /");
333                            print(GT);
334    
335                    } else {
336                            // we're not inside a tag
337    
338                            if (state != EState.INSIDE_TEXT) {
339                                    // if not inside a text element, create new line and indent
340                                    println();
341                                    printIndent();
342                            }
343    
344                            // create closing tag
345                            print(LT);
346                            print("/");
347                            print(xmlResolver.resolveElementName(element));
348                            print(GT);
349                    }
350    
351                    // we're done with this element
352                    elementStack.pop();
353                    currentNestingDepth--;
354                    state = EState.OUTSIDE_TAG;
355            }
356    
357            /**
358             * Add a text element to an element.
359             * 
360             * @param text
361             *            the text to add.
362             */
363            public void addText(String text) {
364                    if (state == EState.INSIDE_TAG) {
365                            print(GT);
366                    }
367                    print(escape(text));
368    
369                    state = EState.INSIDE_TEXT;
370            }
371    
372            /**
373             * Add CDATA section. Added text is not escaped.
374             * 
375             * @throws XMLWriterException
376             *             If the added text contains the CDATA closing tag
377             *             <code>]]></code>. This is not automatically escaped as some
378             *             parsers do not automatically unescape it when reading.
379             */
380            public void addCDataSection(String cdata) {
381                    if (state == EState.INSIDE_TAG) {
382                            print(GT);
383                    }
384    
385                    if (cdata.contains("]]>")) {
386                            throw new XMLWriterException("CDATA contains ']]>'",
387                                            EXMLWriterExceptionType.CDATA_CONTAINS_CDATA_CLOSING_TAG);
388                    }
389    
390                    print("<![CDATA[");
391                    print(cdata);
392                    print("]]>");
393                    state = EState.INSIDE_TEXT;
394            }
395    
396            /**
397             * Add an XML comment.
398             * 
399             * @param text
400             *            comment text.
401             */
402            public void addComment(String text) {
403                    ensureOutsideTag();
404    
405                    currentNestingDepth++;
406                    printIndent();
407                    print(COMMENT_START);
408                    print(escape(text));
409                    print(COMMENT_END);
410                    currentNestingDepth--;
411    
412                    state = EState.OUTSIDE_TAG;
413            }
414    
415            /** Add new line. */
416            public void addNewLine() {
417                    ensureOutsideTag();
418            }
419    
420            /**
421             * Close the writer.
422             * 
423             * @throws XMLWriterException
424             *             if there is a remaining open element.
425             */
426            public void close() {
427                    if (!elementStack.isEmpty()) {
428                            throw new XMLWriterException("Need to close element <"
429                                            + xmlResolver.resolveElementName(elementStack.peek())
430                                            + "> before closing writer.",
431                                            EXMLWriterExceptionType.UNCLOSED_ELEMENT);
432                    }
433                    writer.close();
434            }
435    
436            /** Flushes the underlying writer. */
437            public void flush() {
438                    writer.flush();
439            }
440    
441            /**
442             * Adds the given text unprocessed to the writer. This is useful for adding
443             * chunks of generated XML to avoid having the brackets escaped.
444             */
445            protected void addRawString(String text) {
446                    if (state == EState.INSIDE_TAG) {
447                            print(GT);
448                    }
449                    print(text);
450                    state = EState.INSIDE_TEXT;
451            }
452    
453            /** Get writer this writer writes to. */
454            protected PrintWriter getWriter() {
455                    return writer;
456            }
457    
458            /**
459             * Returns the element we are currently in.
460             * 
461             * @throws EmptyStackException
462             *             if there is no unclosed element.
463             */
464            protected E getCurrentElement() {
465                    return elementStack.peek();
466            }
467    
468            /** Make sure the current tag is closed. */
469            private void ensureOutsideTag() {
470                    if (state == EState.INSIDE_TAG) {
471                            println(GT);
472                            state = EState.OUTSIDE_TAG;
473                    } else if (state == EState.OUTSIDE_TAG) {
474                            println();
475                    }
476            }
477    
478            /**
479             * Escape text for XML. Creates empty string for <code>null</code> value.
480             */
481            public static String escape(String text) {
482                    if (text == null) {
483                            return StringUtils.EMPTY_STRING;
484                    }
485    
486                    text = text.replaceAll("&", "&amp;");
487                    text = text.replaceAll(LT, "&lt;");
488                    text = text.replaceAll(GT, "&gt;");
489                    text = text.replaceAll("\"", "&quot;");
490    
491                    // normalize line breaks
492                    text = StringUtils.replaceLineBreaks(text, CR);
493                    return text;
494            }
495    
496            /** Write to writer. */
497            private void print(String message) {
498                    writer.print(message);
499            }
500    
501            /** Write indent to writer. */
502            private void printIndent() {
503                    if (!suppressLineBreaks) {
504                            writer.print(StringUtils.fillString(currentNestingDepth * 2,
505                                            StringUtils.SPACE_CHAR));
506                    }
507            }
508    
509            /** Write to writer. */
510            private void println() {
511                    if (!suppressLineBreaks) {
512                            print(CR);
513                    }
514            }
515    
516            /** Write to writer. */
517            private void println(String text) {
518                    print(text);
519                    println();
520            }
521    }