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