001    
002    /*
003     * Copyright (C) 2012 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: AnnotatedXMLEventWriter.java 272 2012-02-02 21:02:52Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.xml;
009    
010    import javax.xml.namespace.NamespaceContext;
011    import javax.xml.stream.XMLEventFactory;
012    import javax.xml.stream.XMLEventReader;
013    import javax.xml.stream.XMLEventWriter;
014    import javax.xml.stream.XMLStreamException;
015    import javax.xml.stream.events.XMLEvent;
016    
017    /**
018     * {@link XMLEventWriter} that adds an extra annotation element to an XML document as it is written.
019     * The annotation element will be added as the first element inside the top-level document element.
020     *
021     * <p>
022     * This class can be used in combination with {@link AnnotatedXMLEventReader} to transparently annotate XML documents.
023     *
024     * @see AnnotatedXMLEventReader
025     */
026    public abstract class AnnotatedXMLEventWriter implements XMLEventWriter {
027    
028        protected final XMLEventFactory xmlEventFactory = XMLEventFactory.newFactory();
029    
030        private final StringBuilder trailingSpace = new StringBuilder();
031        private final XMLEventWriter inner;
032    
033        // State:
034        //  0 = before document element
035        //  1 = after document element but before ananotation element
036        //  2 = after annotation element
037        private byte state;
038    
039        public AnnotatedXMLEventWriter(XMLEventWriter inner) {
040            if (inner == null)
041                throw new IllegalArgumentException("null inner");
042            this.inner = inner;
043        }
044    
045        @Override
046        public void add(XMLEvent event) throws XMLStreamException {
047            switch (this.state) {
048            case 0:
049                if (event.isStartElement())
050                    this.state++;
051                this.inner.add(event);
052                break;
053            case 1:
054                if (event.isCharacters() && event.asCharacters().isWhiteSpace()) {
055                    this.trailingSpace.append(event.asCharacters().getData());
056                    this.inner.add(event);
057                    break;
058                }
059                this.state++;
060                this.addAnnotationElement();
061                if (this.trailingSpace.length() > 0)
062                    this.inner.add(this.xmlEventFactory.createCharacters(this.trailingSpace.toString()));
063                this.inner.add(event);
064                break;
065            case 2:
066                this.inner.add(event);
067                break;
068            default:
069                throw new RuntimeException("internal error");
070            }
071        }
072    
073        @Override
074        public void add(XMLEventReader reader) throws XMLStreamException {
075            if (reader == null)
076                throw new XMLStreamException("null reader");
077            while (reader.hasNext())
078                this.add(reader.nextEvent());
079        }
080    
081        @Override
082        public void flush() throws XMLStreamException {
083            this.inner.flush();
084        }
085    
086        @Override
087        public void close() throws XMLStreamException {
088            this.inner.close();
089        }
090    
091        @Override
092        public String getPrefix(String uri) throws XMLStreamException {
093            return this.inner.getPrefix(uri);
094        }
095    
096        @Override
097        public void setPrefix(String prefix, String uri) throws XMLStreamException {
098            this.inner.setPrefix(prefix, uri);
099        }
100    
101        @Override
102        public void setDefaultNamespace(String uri) throws XMLStreamException {
103            this.inner.setDefaultNamespace(uri);
104        }
105    
106        @Override
107        public void setNamespaceContext(NamespaceContext context) throws XMLStreamException {
108            this.inner.setNamespaceContext(context);
109        }
110    
111        @Override
112        public NamespaceContext getNamespaceContext() {
113            return this.inner.getNamespaceContext();
114        }
115    
116        /**
117         * Get the whitespace found between the opening document tag and the first non-space child.
118         */
119        protected String getTrailingSpace() {
120            return this.trailingSpace.toString();
121        }
122    
123        /**
124         * Add the annotation element.
125         *
126         * <p>
127         * This method should {@link #add add()} the {@link javax.xml.stream.events.StartElement} for the annotation element, followed
128         * by any nested content, and then lastly the {@link javax.xml.stream.events.EndElement} for the annotation element.
129         */
130        protected abstract void addAnnotationElement() throws XMLStreamException;
131    }
132