001    
002    /*
003     * Copyright (C) 2012 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: AnnotatedXMLEventReader.java 226 2012-01-18 16:30:35Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.xml;
009    
010    import javax.xml.stream.XMLEventReader;
011    import javax.xml.stream.XMLStreamException;
012    import javax.xml.stream.events.Comment;
013    import javax.xml.stream.events.StartElement;
014    import javax.xml.stream.events.XMLEvent;
015    import javax.xml.stream.util.EventReaderDelegate;
016    
017    /**
018     * {@link XMLEventReader} that reads and removes an initial annotation element from an XML document.
019     * The annotation element, if present, must be the first element inside the top-level document element.
020     * When the annotation element is encountered, {@link #readAnnotationElement readAnnotationElement()} will be invoked.
021     *
022     * <p>
023     * This class can be used in combination with {@link AnnotatedXMLEventWriter} to transparently annotate XML documents.
024     *
025     * @see AnnotatedXMLEventWriter
026     */
027    public abstract class AnnotatedXMLEventReader extends EventReaderDelegate {
028    
029        // State:
030        //  0 = before document element
031        //  1 = after document element but before ananotation element (if any)
032        //  2 = after annotation element (if any)
033        private byte state;
034    
035        /**
036         * Constructor.
037         *
038         * <p>
039         * The given parameter should expect to read the XML document without the annotation element.
040         *
041         * @param inner nested reader
042         */
043        public AnnotatedXMLEventReader(XMLEventReader inner) {
044            super(inner);
045        }
046    
047        @Override
048        public void setParent(XMLEventReader reader) {
049            throw new UnsupportedOperationException();
050        }
051    
052        @Override
053        public Object next() {
054            try {
055                return this.nextEvent();
056            } catch (XMLStreamException e) {
057                throw new RuntimeException(e);
058            }
059        }
060    
061        @Override
062        public XMLEvent peek() throws XMLStreamException {
063            XMLEvent event = super.peek();
064            if (this.state == 1)
065                event = this.checkAnnotation(event);
066            return event;
067        }
068    
069        @Override
070        public XMLEvent nextEvent() throws XMLStreamException {
071            switch (this.state) {
072            case 0:
073                XMLEvent event = super.nextEvent();
074                if (event.isStartElement())
075                    this.state++;
076                return event;
077            case 1:
078                this.peek();
079                return super.nextEvent();
080            default:
081                return super.nextEvent();
082            }
083        }
084    
085        /**
086         * Determine if the given event represents the start of the annotation element.
087         */
088        protected abstract boolean isAnnotationElement(StartElement event);
089    
090        /**
091         * Read the annotation element.
092         *
093         * <p>
094         * When this method is invoked, {@code event} represents the {@link StartElement} event for the annotation element
095         * (i.e., {@link #isAnnotationElement isAnnotationElement(event)} has returned true); it is also the next event in the
096         * pipeline, i.e., the next event returned by {@link #nextEvent}.
097         *
098         * <p>
099         * The last event consumed by this method should be the {@link javax.xml.stream.events.EndElement} event for the
100         * annotation element, and the event returned by {@link #peek} should be the next event after the
101         * {@link javax.xml.stream.events.EndElement}.
102         *
103         * @param event the {@link StartElement} event for the annotation element
104         */
105        protected abstract void readAnnotationElement(StartElement event) throws XMLStreamException;
106    
107        /**
108         * Skip whitespace.
109         *
110         * @param event the next event, as returned by {@link #peek}
111         * @param comments whether to also skip comments
112         * @return the next event, as returned by {@link #peek}, after skipping any leading whitespace (and, optionally, comments)
113         */
114        protected XMLEvent skipWhiteSpace(XMLEvent event, boolean comments) throws XMLStreamException {
115            while (event.isCharacters() && event.asCharacters().isWhiteSpace() || (comments && event instanceof Comment)) {
116                super.nextEvent();
117                event = super.peek();
118            }
119            return event;
120        }
121    
122        /**
123         * Advance one event in the event stream.
124         *
125         * @return the next event, as returned by {@link #peek}, after the advance
126         */
127        protected XMLEvent advance() throws XMLStreamException {
128            super.nextEvent();
129            return super.peek();
130        }
131    
132        /**
133         * Scan for the annotation. We only allow whitespace and comments between the first {@link StartElement} and the
134         * annotation {@link StartElement}. Whitespace after the annotation {@link javax.xml.stream.events.EndElement} is removed.
135         *
136         * Pre-condition:
137         *  - Given event comes after the document element and is before or equal to updates start element
138         *  - Given event is the current "peek event" for this stream
139         *
140         * Post-condition:
141         *  - If event was updates start element, updates were consumed, else no change
142         *  - If updates were consumed:
143         *      - New "peek event" is whatever follows updates and trailing whitespace
144         *      - State has advanced
145         *  - Returned event is the (possibly new) "peek event"
146         */
147        private XMLEvent checkAnnotation(XMLEvent event) throws XMLStreamException {
148    
149            // Skip over leading whitespace and comments
150            assert this.state == 1;
151            if (event.isCharacters() && event.asCharacters().isWhiteSpace())
152                return event;
153            if (event instanceof Comment)
154                return event;
155    
156            // Anything else means we either we have found the annotation element or it is not there
157            this.state++;
158            if (!event.isStartElement() || !this.isAnnotationElement(event.asStartElement()))
159                return event;
160    
161            // It is there, so read it
162            this.readAnnotationElement(event.asStartElement());
163    
164            // Skip whitespace after annotation element
165            return this.skipWhiteSpace(super.peek(), false);
166        }
167    }
168