001: /*
002: * $Id: XMLStreamUtils.java,v 1.8 2004/07/09 17:30:50 cniles Exp $
003: *
004: * Copyright (c) 2004, Christian Niles, Unit12
005: * All rights reserved.
006: *
007: * Redistribution and use in source and binary forms, with or without
008: * modification, are permitted provided that the following conditions are met:
009: *
010: * * Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * * Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in the
015: * documentation and/or other materials provided with the distribution.
016: *
017: * * Neither the name of Christian Niles, Unit12, nor the names of its
018: * contributors may be used to endorse or promote products derived from
019: * this software without specific prior written permission.
020: *
021: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
022: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
023: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
024: * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
025: * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
026: * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
027: * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
028: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
029: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
030: * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
031: * POSSIBILITY OF SUCH DAMAGE.
032: *
033: */
034: package javanet.staxutils;
035:
036: import java.util.HashMap;
037: import java.util.Iterator;
038: import java.util.Map;
039:
040: import javax.xml.namespace.QName;
041: import javax.xml.stream.XMLEventFactory;
042: import javax.xml.stream.XMLEventReader;
043: import javax.xml.stream.XMLEventWriter;
044: import javax.xml.stream.XMLInputFactory;
045: import javax.xml.stream.XMLOutputFactory;
046: import javax.xml.stream.XMLStreamConstants;
047: import javax.xml.stream.XMLStreamException;
048: import javax.xml.stream.XMLStreamReader;
049: import javax.xml.stream.XMLStreamWriter;
050: import javax.xml.stream.events.Attribute;
051: import javax.xml.stream.events.StartElement;
052: import javax.xml.stream.events.XMLEvent;
053: import javax.xml.stream.util.XMLEventConsumer;
054: import javax.xml.transform.Result;
055: import javax.xml.transform.Source;
056:
057: /**
058: * Static utility methods useful when handling XML Streams.
059: *
060: * @author Christian Niles
061: * @version $Revision: 1.8 $
062: */
063: public class XMLStreamUtils {
064:
065: private static XMLInputFactory inputFactory = XMLInputFactory
066: .newInstance();
067:
068: private static XMLOutputFactory outputFactory = XMLOutputFactory
069: .newInstance();
070:
071: private static final String[] EVENT_NAMES = new String[16];
072: static {
073:
074: EVENT_NAMES[0] = ""; // no event has 0 index
075: EVENT_NAMES[XMLStreamConstants.ATTRIBUTE] = "ATTRIBUTE";
076: EVENT_NAMES[XMLStreamConstants.CDATA] = "CDATA";
077: EVENT_NAMES[XMLStreamConstants.CHARACTERS] = "CHARACTERS";
078: EVENT_NAMES[XMLStreamConstants.COMMENT] = "COMMENT";
079: EVENT_NAMES[XMLStreamConstants.DTD] = "DTD";
080: EVENT_NAMES[XMLStreamConstants.END_DOCUMENT] = "END_DOCUMENT";
081: EVENT_NAMES[XMLStreamConstants.END_ELEMENT] = "END_ELEMENT";
082: EVENT_NAMES[XMLStreamConstants.ENTITY_DECLARATION] = "ENTITY_DECLARATION";
083: EVENT_NAMES[XMLStreamConstants.ENTITY_REFERENCE] = "ENTITY_REFERENCE";
084: EVENT_NAMES[XMLStreamConstants.NAMESPACE] = "NAMESPACE";
085: EVENT_NAMES[XMLStreamConstants.NOTATION_DECLARATION] = "NOTATION_DECLARATION";
086: EVENT_NAMES[XMLStreamConstants.PROCESSING_INSTRUCTION] = "PROCESSING_INSTRUCTION";
087: EVENT_NAMES[XMLStreamConstants.SPACE] = "SPACE";
088: EVENT_NAMES[XMLStreamConstants.START_DOCUMENT] = "START_DOCUMENT";
089: EVENT_NAMES[XMLStreamConstants.START_ELEMENT] = "START_ELEMENT";
090:
091: }
092:
093: /**
094: * Returns the name of the specified stream event constant.
095: *
096: * @param eventType The event constant, such as
097: * {@link XMLStreamConstants#START_DOCUMENT}.
098: * @return The name of the specified event, or <code>"UNKNOWN"</code> if the
099: * constant isn't valid.
100: */
101: public static final String getEventTypeName(int eventType) {
102:
103: if (eventType > 0 || eventType < EVENT_NAMES.length) {
104:
105: return EVENT_NAMES[eventType];
106:
107: } else {
108:
109: return "UNKNOWN";
110:
111: }
112:
113: }
114:
115: /**
116: * Returns the value of the attribute with the given non-qualified name.
117: *
118: * @param reader The xml stream reader
119: * @param name The name of the attribute.
120: * @return The value of the unqualified attribute, or <code>null</code>
121: * if the attribute wasn't present.
122: */
123: public static final String attributeValue(XMLStreamReader reader,
124: String name) {
125:
126: return reader.getAttributeValue("", name);
127:
128: }
129:
130: /**
131: * Returns the value of the attribute with the given name.
132: *
133: * @param reader The xml stream reader
134: * @param name The name of the attribute.
135: * @return The value of the attribute, or <code>null</code> if the
136: * attribute wasn't present.
137: */
138: public static final String attributeValue(XMLStreamReader reader,
139: QName name) {
140:
141: return reader.getAttributeValue(name.getNamespaceURI(), name
142: .getLocalPart());
143:
144: }
145:
146: /**
147: * Skips all events within a single element, including its start and end
148: * tags. The provided reader must be positioned directly in front of a
149: * <code>StartElement</code> event or it will have no effect. After this
150: * method completes, the reader will be positioned before the event
151: * following the end tag (the end tag will have been read).
152: *
153: * @param reader The event stream to read.
154: * @throws XMLStreamException If an error occurs reading events.
155: */
156: public static final void skipElement(XMLEventReader reader)
157: throws XMLStreamException {
158:
159: copyElement(reader, null);
160:
161: }
162:
163: /**
164: * Copies an element and all its content from the provided event reader, to
165: * the provided event consumer. The event reader must be positioned before a
166: * start element event, or this method has no effect.
167: *
168: * @param reader The reader from which to read the events.
169: * @param consumer The destination for read events, or <code>null</code> to
170: * ignore all events.
171: * @throws XMLStreamException If an error occurs reading or writing the
172: * events.
173: */
174: public static final void copyElement(XMLEventReader reader,
175: XMLEventConsumer consumer) throws XMLStreamException {
176:
177: if (!reader.hasNext())
178: return;
179:
180: XMLEvent event = reader.peek();
181: if (!event.isStartElement())
182: return;
183:
184: int depth = 0;
185: do {
186:
187: XMLEvent currEvt = reader.nextEvent();
188: if (currEvt.isStartElement()) {
189:
190: depth++;
191:
192: } else if (currEvt.isEndElement()) {
193:
194: depth--;
195:
196: }
197:
198: if (consumer != null) {
199:
200: consumer.add(currEvt);
201:
202: }
203:
204: } while (depth > 0 && reader.hasNext());
205:
206: }
207:
208: /**
209: * Skips all events within a <code>StartElement</code> until the matching
210: * <code>EndElement</code> is reached. This method assumes that the reader
211: * is positioned after the <code>StartElement</code> event, and when the
212: * method completes, the stream will be positioned before the
213: * <code>EndElement</code> event, but it will not consume the end tag.
214: *
215: * @param reader The event stream to read, positioned after the
216: * <code>StartElement</code>
217: * @throws XMLStreamException If an error occurs reading events.
218: */
219: public static final void skipElementContent(XMLEventReader reader)
220: throws XMLStreamException {
221:
222: copyElementContent(reader, null);
223:
224: }
225:
226: /**
227: * Copies all events within a <code>StartElement</code> until the matching
228: * <code>EndElement</code> is reached. This method assumes that the reader
229: * is positioned after the <code>StartElement</code> event, and when the
230: * method completes, the stream will be positioned before the
231: * <code>EndElement</code> event, but it will not consume the end tag.
232: *
233: * @param reader The event stream to read, positioned after the
234: * <code>StartElement</code>
235: * @param consumer The destination for events read from teh stream, or
236: * <code>null</code> to ignore the events completely.
237: * @throws XMLStreamException If an error occurs reading events.
238: */
239: public static final void copyElementContent(XMLEventReader reader,
240: XMLEventConsumer consumer) throws XMLStreamException {
241:
242: if (!reader.hasNext())
243: return;
244:
245: for (int depth = 1; true;) {
246:
247: // peek and see if we're at the end element
248: XMLEvent currEvt = reader.peek();
249: if (currEvt.isEndElement()) {
250:
251: depth--;
252: if (depth == 0) {
253:
254: break;
255:
256: }
257:
258: } else if (currEvt.isStartElement()) {
259:
260: depth++;
261:
262: }
263:
264: // consume the event
265: currEvt = reader.nextEvent();
266:
267: if (consumer != null) {
268:
269: consumer.add(currEvt);
270:
271: }
272:
273: }
274:
275: }
276:
277: /**
278: * Skips the complete content of the element at the specified reader's
279: * cursor. The reader's current event type must be START_ELEMENT, otherwise
280: * this method will have no effect. Upon completion, the reader's cursor
281: * will be at the END_ELEMENT event for the skipped element.
282: *
283: * @param reader An XML stream reader currently in the START_ELEMENT event.
284: */
285: public static final void skipElement(XMLStreamReader reader)
286: throws XMLStreamException {
287:
288: if (reader.isStartElement()) {
289:
290: skipElementContent(reader);
291:
292: }
293:
294: }
295:
296: /**
297: * Skips an element's complete content. This method assumes that the
298: * <code>START_ELEMENT</code> has already be passed, and when it terminates,
299: * the stream will be positioned at the <code>END_ELEMENT</code>.
300: *
301: * @param reader The stream reader to read.
302: * @throws XMLStreamException If an error occurs reading the stream.
303: */
304: public static final void skipElementContent(XMLStreamReader reader)
305: throws XMLStreamException {
306:
307: int depth = 0;
308: while (depth >= 0) {
309:
310: reader.next();
311: if (reader.isStartElement()) {
312:
313: depth++;
314:
315: } else if (reader.isEndElement()) {
316:
317: depth--;
318:
319: }
320:
321: }
322:
323: }
324:
325: /**
326: * Static utility method that throws an exception if the supplied reader's
327: * cursor doesn't point to a START_ELEMENT with the given name.
328: *
329: * @param reader The reader to test.
330: * @param name The name of the element to require.
331: * @throws XMLStreamException If the reader state is an element with the
332: * specified name.
333: */
334: public static final void requireElement(XMLStreamReader reader,
335: QName name) throws XMLStreamException {
336:
337: reader.require(XMLStreamReader.START_ELEMENT, name
338: .getNamespaceURI(), name.getLocalPart());
339:
340: }
341:
342: /**
343: * Copies the content read from the specified source stream to the provided
344: * result stream. This method is exactly the same as calling
345: * {@link XMLEventWriter#add(XMLEventReader)}, and is provided only for
346: * completeness.
347: *
348: * @param reader The source stream.
349: * @param consumer The destination stream.
350: * @throws XMLStreamException If an error occurs copying the stream
351: * contents.
352: */
353: public static final void copy(XMLEventReader reader,
354: XMLEventConsumer consumer) throws XMLStreamException {
355:
356: if (consumer instanceof XMLEventWriter) {
357:
358: copy(reader, (XMLEventWriter) consumer);
359:
360: } else {
361:
362: while (reader.hasNext()) {
363:
364: consumer.add(reader.nextEvent());
365:
366: }
367:
368: }
369:
370: }
371:
372: /**
373: * Copies the content read from the specified source stream to the provided
374: * result stream. This method is exactly the same as calling
375: * {@link XMLEventWriter#add(XMLEventReader)}, and is provided only for
376: * completeness.
377: *
378: * @param reader The source stream.
379: * @param writer The destination stream.
380: * @throws XMLStreamException If an error occurs copying the stream
381: * contents.
382: */
383: public static final void copy(XMLEventReader reader,
384: XMLEventWriter writer) throws XMLStreamException {
385:
386: writer.add(reader);
387:
388: }
389:
390: /**
391: * Copies the content read from the specified source stream to the provided
392: * result stream.
393: *
394: * @param reader The source stream.
395: * @param writer The destination stream.
396: * @throws XMLStreamException If an error occurs copying the stream
397: * contents.
398: */
399: public static final void copy(XMLStreamReader reader,
400: XMLStreamWriter writer) throws XMLStreamException {
401:
402: XMLEventReader r = inputFactory.createXMLEventReader(reader);
403: XMLEventWriter w = new XMLStreamEventWriter(writer);
404:
405: try {
406:
407: w.add(r);
408:
409: } finally {
410:
411: // force any cached events to the underlying writer
412: w.flush();
413:
414: }
415:
416: }
417:
418: /**
419: * Copies the content read from the specified source stream to the provided
420: * result stream.
421: *
422: * @param reader The source stream.
423: * @param writer The destination stream.
424: * @param factory An optional input factory used to create any intermediate
425: * streams.
426: * @throws XMLStreamException If an error occurs copying the stream
427: * contents.
428: */
429: public static final void copy(XMLStreamReader reader,
430: XMLStreamWriter writer, XMLInputFactory factory)
431: throws XMLStreamException {
432:
433: if (factory == null) {
434:
435: factory = inputFactory;
436:
437: }
438:
439: XMLEventReader r = factory.createXMLEventReader(reader);
440: XMLEventWriter w = new XMLStreamEventWriter(writer);
441:
442: try {
443:
444: w.add(r);
445:
446: } finally {
447:
448: // force any cached events to the underlying writer
449: w.flush();
450:
451: }
452:
453: }
454:
455: /**
456: * Copies the content read from a TrAX {@link Source} to a StAX
457: * {@link XMLStreamWriter}.
458: *
459: * @param source The content source.
460: * @param writer The destination stream.
461: * @throws XMLStreamException If an error occurs copying the content to the
462: * stream.
463: */
464: public static final void copy(Source source, XMLStreamWriter writer)
465: throws XMLStreamException {
466:
467: XMLStreamReader reader = inputFactory
468: .createXMLStreamReader(source);
469: copy(reader, writer);
470:
471: }
472:
473: /**
474: * Copies the content read from a TrAX {@link Source} to a StAX
475: * {@link XMLEventWriter}.
476: *
477: * @param source The content source.
478: * @param writer The destination event stream.
479: * @throws XMLStreamException If an error occurs copying the content to the
480: * event stream.
481: */
482: public static final void copy(Source source, XMLEventWriter writer)
483: throws XMLStreamException {
484:
485: XMLEventReader reader = inputFactory
486: .createXMLEventReader(source);
487: copy(reader, writer);
488:
489: }
490:
491: /**
492: * Copies the content read from a StAX {@link XMLEventReader} to a TrAX
493: * {@link Result}.
494: *
495: * @param reader The source event stream.
496: * @param result The destination {@link Result}.
497: * @throws XMLStreamException If an error occurs copying the content to the
498: * result.
499: */
500: public static final void copy(XMLEventReader reader, Result result)
501: throws XMLStreamException {
502:
503: XMLEventWriter writer = outputFactory
504: .createXMLEventWriter(result);
505:
506: copy(reader, writer);
507:
508: // force any cached events to the result
509: writer.flush();
510:
511: }
512:
513: /**
514: * Copies the content read from a StAX {@link XMLStreamReader} to a TrAX
515: * {@link Result}.
516: *
517: * @param reader The source stream.
518: * @param result The destination {@link Result}.
519: * @throws XMLStreamException If an error occurs copying the content to the
520: * result.
521: */
522: public static final void copy(XMLStreamReader reader, Result result)
523: throws XMLStreamException {
524:
525: XMLStreamWriter writer = outputFactory
526: .createXMLStreamWriter(result);
527:
528: copy(reader, writer);
529:
530: // force any cached content to the result
531: writer.flush();
532:
533: }
534:
535: /**
536: * Utility method that throws an exception if the provided reader is not
537: * positioned before a StartElement event with the specified tag name.
538: *
539: * @param reader The reader to test.
540: * @param qname The required name of the start-tag. If <code>null</code>,
541: * any start tag is accepted.
542: * @throws XMLStreamException If an error occurs reading from the stream.
543: */
544: public static final void requireStartElement(XMLEventReader reader,
545: QName qname) throws XMLStreamException {
546:
547: if (reader.hasNext()) {
548:
549: XMLEvent nextEvent = reader.peek();
550: if (nextEvent.isStartElement()) {
551:
552: if (qname != null) {
553:
554: StartElement start = nextEvent.asStartElement();
555: QName name = start.getName();
556: if (!name.equals(qname)) {
557:
558: throw new XMLStreamException(
559: "Encountered unexpected element; expected "
560: + qname + ", but found " + name);
561:
562: }
563:
564: }
565:
566: } else {
567:
568: throw new XMLStreamException(
569: "Encountered unexpected event; expected "
570: + qname
571: + " start-tag, but found event "
572: + nextEvent);
573:
574: }
575:
576: } else {
577:
578: throw new XMLStreamException(
579: "Encountered unexpected end of stream; expected element "
580: + qname);
581:
582: }
583:
584: }
585:
586: /**
587: * Constructs a new StartElement that merges the attributes and namespaces
588: * found in the specified StartElement, with the provided attributes. The
589: * returned StartElement will contain all the attributes and namespaces of
590: * the original, plus those defined in the map.
591: *
592: * @param tag The original StartElement
593: * @param attrs An iterator of Atributes to add to the element.
594: * @return A new StartElement that contains all the original attributes and
595: * namespaces, plus the provided attributes.
596: */
597: public static StartElement mergeAttributes(StartElement tag,
598: Iterator attrs, XMLEventFactory factory) {
599:
600: // create Attribute map
601: Map attributes = new HashMap();
602:
603: // iterate through start tag's attributes
604: for (Iterator i = tag.getAttributes(); i.hasNext();) {
605:
606: Attribute attr = (Attribute) i.next();
607: attributes.put(attr.getName(), attr);
608:
609: }
610:
611: // iterate through new attributes
612: while (attrs.hasNext()) {
613:
614: Attribute attr = (Attribute) attrs.next();
615: attributes.put(attr.getName(), attr);
616:
617: }
618:
619: factory.setLocation(tag.getLocation());
620:
621: QName tagName = tag.getName();
622: return factory.createStartElement(tagName.getPrefix(), tagName
623: .getNamespaceURI(), tagName.getLocalPart(), attributes
624: .values().iterator(), tag.getNamespaces(), tag
625: .getNamespaceContext());
626:
627: }
628:
629: /**
630: * Reads the text content of an element. The reader should be positioned in
631: * front of a StartElement event, and will be read up to and including the
632: * end element tag.
633: *
634: * @param reader The event stream from which to read the element text.
635: * @param elemName The optional name of the element being read. If this
636: * paramter is non- <code>null</code> then an exception will
637: * be thrown if the element read doesn't have the same name.
638: * @return The text read from the element.
639: * @throws XMLStreamException If an error occurs reading the stream, or if
640: * the read element doesn't match the provided QName.
641: */
642: public static final String readTextElement(XMLEventReader reader,
643: QName elemName) throws XMLStreamException {
644:
645: if (elemName != null) {
646:
647: requireStartElement(reader, elemName);
648:
649: }
650:
651: // read text
652: String text = reader.getElementText();
653:
654: // consume the end tag
655: reader.nextEvent();
656:
657: return text;
658:
659: }
660:
661: /**
662: * Advances the event stream until it encounters a start or end tag, but
663: * does not actaully read the event.
664: *
665: * @param reader The reader to peek.
666: * @return The next StartElement or EndElement event, retrieved using
667: * <code>peek()</code>, or <code>null</code> if the end of the
668: * stream was encountered before any tag event.
669: * @throws XMLStreamException If an error occurs reading the stream.
670: */
671: public static final XMLEvent nextTag(XMLEventReader reader)
672: throws XMLStreamException {
673:
674: while (reader.hasNext()) {
675:
676: XMLEvent nextEvent = reader.peek();
677: if (nextEvent.isStartElement() || nextEvent.isEndElement()) {
678:
679: return nextEvent;
680:
681: } else {
682:
683: // eat the event.
684: reader.nextEvent();
685:
686: }
687:
688: }
689:
690: return null;
691:
692: }
693:
694: /**
695: * Reads the events from the provided event stream until either a start or
696: * end tag is encountered. In the former case, the start tag will be
697: * returned, but if an end tag is encountered, <code>null</code> will be
698: * returned. After returning, the stream will be positioned just before the
699: * returned start element. The start element will not be consumed by this
700: * method.
701: *
702: * @param reader The event stream from which to read.
703: * @return The StartElement read from the stream, or <code>null</code> if
704: * an end tag was found first, or the stream ended before a start
705: * element was found.
706: * @throws XMLStreamException If an error occurs reading the stream.
707: */
708: public static final StartElement nextElement(XMLEventReader reader)
709: throws XMLStreamException {
710:
711: return nextElement(reader, null);
712:
713: }
714:
715: /**
716: * Reads the events from the provided event stream until either a start or
717: * end tag is encountered. In the former case, the start tag will be
718: * returned if it matches the specified QName, but if it doesn't match, an
719: * end tag is encountered, or the stream ends, <code>null</code> will be
720: * returned. After returning, the stream will be positioned just before the
721: * start element. The start element will not be consumed by this method.
722: *
723: * @param reader The event stream from which to read.
724: * @param name The name of the element to read, or <code>null</code> to
725: * read any start tag.
726: * @return The StartElement read from the stream, or <code>null</code> if
727: * the encountered start tag didn't match the specified QName, an
728: * end tag was found first, or the stream ended before a start
729: * element was found.
730: * @throws XMLStreamException If an error occurs reading the stream.
731: */
732: public static final StartElement nextElement(XMLEventReader reader,
733: QName name) throws XMLStreamException {
734:
735: while (reader.hasNext()) {
736:
737: XMLEvent nextEvent = reader.peek();
738: if (nextEvent.isStartElement()) {
739:
740: StartElement start = nextEvent.asStartElement();
741: if (name == null || start.getName().equals(name)) {
742:
743: return start;
744:
745: } else {
746:
747: break;
748:
749: }
750:
751: } else if (nextEvent.isEndElement()) {
752:
753: break;
754:
755: } else {
756:
757: // consume the event.
758: reader.nextEvent();
759:
760: }
761:
762: }
763:
764: return null;
765:
766: }
767:
768: }
|