001: /*
002: * $Id: BaseXMLEventWriter.java,v 1.9 2005/03/09 22:34:34 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.ArrayList;
037: import java.util.Iterator;
038: import java.util.LinkedHashMap;
039: import java.util.List;
040: import java.util.Map;
041:
042: import javax.xml.namespace.NamespaceContext;
043: import javax.xml.namespace.QName;
044: import javax.xml.stream.XMLEventFactory;
045: import javax.xml.stream.XMLEventReader;
046: import javax.xml.stream.XMLEventWriter;
047: import javax.xml.stream.XMLStreamException;
048: import javax.xml.stream.events.*;
049:
050: /**
051: * Base class for {@link XMLEventWriter} implementations. This implemenation
052: * buffers Attribute and Namespace events as specified in the specification,
053: * maintains a stack of NamespaceContext instances based on the events it
054: * receives, and repairs any missing namespaces. Subclasses should implement the
055: * {@link #sendEvent(XMLEvent)} method to receive the processed events and
056: * perform additional processing.
057: *
058: * @author Christian Niles
059: * @version $Revision: 1.9 $
060: */
061: public abstract class BaseXMLEventWriter implements XMLEventWriter {
062:
063: /**
064: * XMLEventFactory used to construct XMLEvent instances.
065: */
066: protected XMLEventFactory factory;
067:
068: /** list of {@link SimpleNamespaceContext}s. */
069: protected List nsStack = new ArrayList();
070:
071: /**
072: * Reference to the last StartElement sent. This will be null if no
073: * StartElement has been sent, or after a non Attribute/Namespace
074: * event is received.
075: */
076: protected StartElement lastStart;
077:
078: /**
079: * LinkedHashMap of attribute events sent surrounding the last
080: * StartElement. By using LinkedHashMap, the attributes will stay in the
081: * order they were defined.
082: */
083: protected Map attrBuff = new LinkedHashMap();
084:
085: /**
086: * LinkedHashMap of namespace events sent surrounding the last
087: * StartElement. By using LinkedHashMap, the namespaces will stay in the
088: * order they were defined.
089: */
090: protected Map nsBuff = new LinkedHashMap();
091:
092: /**
093: * Whether this writer has been closed or not.
094: */
095: protected boolean closed;
096:
097: protected BaseXMLEventWriter() {
098:
099: this (null, null);
100:
101: }
102:
103: protected BaseXMLEventWriter(XMLEventFactory eventFactory,
104: NamespaceContext nsCtx) {
105:
106: if (nsCtx != null) {
107:
108: nsStack.add(new SimpleNamespaceContext(nsCtx));
109:
110: } else {
111:
112: nsStack.add(new SimpleNamespaceContext());
113:
114: }
115:
116: if (eventFactory != null) {
117:
118: factory = eventFactory;
119:
120: } else {
121:
122: factory = XMLEventFactory.newInstance();
123:
124: }
125:
126: }
127:
128: public synchronized void flush() throws XMLStreamException {
129:
130: if (!closed) {
131:
132: sendCachedEvents();
133:
134: }
135:
136: }
137:
138: /**
139: * Sends any events that have been cached in anticipation of further events.
140: *
141: * @throws XMLStreamException If an error occurs sending the events.
142: */
143: private void sendCachedEvents() throws XMLStreamException {
144:
145: if (lastStart != null) {
146:
147: // A StartElement, and possibly attributes and namespaces, have been
148: // cached. We need to combine them all into a single StartElement
149: // event to send to sendEvent(XMLEvent)
150:
151: // First, construct the new NamespaceContext for the tag
152: SimpleNamespaceContext nsCtx = this .pushNamespaceStack();
153:
154: // List used to store any defaulted/rewritten namespaces
155: List namespaces = new ArrayList();
156:
157: // merge namespaces
158: mergeNamespaces(lastStart.getNamespaces(), namespaces);
159: mergeNamespaces(nsBuff.values().iterator(), namespaces);
160: nsBuff.clear();
161:
162: // merge attributes
163: List attributes = new ArrayList();
164: mergeAttributes(lastStart.getAttributes(), namespaces,
165: attributes);
166: mergeAttributes(attrBuff.values().iterator(), namespaces,
167: attributes);
168: attrBuff.clear();
169:
170: // determine the name of the new start tag
171: QName tagName = lastStart.getName();
172: QName newName = processQName(tagName, namespaces);
173:
174: // construct new element
175: StartElement newStart = factory.createStartElement(newName
176: .getPrefix(), newName.getNamespaceURI(), newName
177: .getLocalPart(), attributes.iterator(), namespaces
178: .iterator(), nsCtx);
179:
180: lastStart = null;
181: sendEvent(newStart);
182:
183: } else {
184:
185: // no start element was cached, but we may have cached some
186: // namespaces and attributes that need to be written.
187:
188: for (Iterator i = nsBuff.values().iterator(); i.hasNext();) {
189:
190: XMLEvent evt = (XMLEvent) i.next();
191: sendEvent(evt);
192:
193: }
194: nsBuff.clear();
195:
196: for (Iterator i = attrBuff.values().iterator(); i.hasNext();) {
197:
198: XMLEvent evt = (XMLEvent) i.next();
199: sendEvent(evt);
200:
201: }
202: attrBuff.clear();
203:
204: }
205:
206: }
207:
208: /**
209: * Merges a set of {@link Attribute} events, possibly adding additional
210: * {@link Namespace} events if the attribute's prefix isn't bound in the
211: * provided context.
212: *
213: * @param iter An {@link Iterator} of {@link Attribute} events.
214: * @param namespaces A {@link List} to which any new {@link Namespace}
215: * events should be added.
216: * @param attributes A {@link List} to which all {@link Attributes} events
217: * should be merged.
218: */
219: private void mergeAttributes(Iterator iter, List namespaces,
220: List attributes) {
221:
222: while (iter.hasNext()) {
223:
224: Attribute attr = (Attribute) iter.next();
225:
226: // check if the attribute QName has the proper mapping
227: QName attrName = attr.getName();
228: QName newName = processQName(attrName, namespaces);
229: if (!attrName.equals(newName)) {
230:
231: // need to generate a new attribute with the new qualified name
232: Attribute newAttr = factory.createAttribute(newName,
233: attr.getValue());
234: attributes.add(newAttr);
235:
236: } else {
237:
238: // the attribute is fine
239: attributes.add(attr);
240:
241: }
242:
243: }
244:
245: }
246:
247: /**
248: * Merges a set of {@link Namespaces} events into the provided {@link List}.
249: *
250: * @param iter An {@link Iterator} of {@link Namespace}s to merge.
251: * @param namespaces A {@link List} containing all added {@link Namespace}
252: * events.
253: * @throws XMLStreamException If a conflicting namespace binding is
254: * encountered.
255: */
256: private void mergeNamespaces(Iterator iter, List namespaces)
257: throws XMLStreamException {
258:
259: // for each namespace, add it to the context, and place it in the list
260: while (iter.hasNext()) {
261:
262: Namespace ns = (Namespace) iter.next();
263: String prefix = ns.getPrefix();
264: String nsURI = ns.getNamespaceURI();
265: SimpleNamespaceContext nsCtx = this .peekNamespaceStack();
266:
267: if (!nsCtx.isPrefixDeclared(prefix)) {
268:
269: // mapping doesn't exist, so add it to the context/list
270: if (prefix == null || prefix.length() == 0) {
271:
272: nsCtx.setDefaultNamespace(nsURI);
273:
274: } else {
275:
276: nsCtx.setPrefix(prefix, nsURI);
277:
278: }
279:
280: namespaces.add(ns);
281:
282: } else if (!nsCtx.getNamespaceURI(prefix).equals(nsURI)) {
283:
284: throw new XMLStreamException(
285: "Prefix already declared: " + ns, ns
286: .getLocation());
287:
288: } else {
289:
290: // duplicate namespace declaration
291:
292: }
293:
294: }
295:
296: }
297:
298: /**
299: * Processes a {@link QName}, possibly rewriting it to match the current
300: * namespace context.
301: *
302: * @param name The {@link QName} to process.
303: * @param namespaces A {@link List} of {@link Namespace} events to which any
304: * new namespace bindings should be added.
305: * @return The new name, or the <code>name</code> parameter if no changes
306: * were necessary.
307: */
308: private QName processQName(QName name, List namespaces) {
309:
310: // get current context
311: SimpleNamespaceContext nsCtx = this .peekNamespaceStack();
312:
313: // get name parts
314: String nsURI = name.getNamespaceURI();
315: String prefix = name.getPrefix();
316: if (prefix != null && prefix.length() > 0) {
317:
318: // prefix provided; see if it is okay in current context, otherwise we'll
319: // have to rewrite it
320: String resolvedNS = nsCtx.getNamespaceURI(prefix);
321: if (resolvedNS != null) {
322:
323: if (!resolvedNS.equals(nsURI)) {
324:
325: // prefix is already bound to a different namespace; we have to
326: // find or generate and alternative prefix
327: String newPrefix = nsCtx.getPrefix(nsURI);
328: if (newPrefix == null) {
329:
330: // no existing prefix; need to generate a new prefix
331: newPrefix = generatePrefix(nsURI, nsCtx,
332: namespaces);
333:
334: }
335:
336: // return the newly prefixed name
337: return new QName(nsURI, name.getLocalPart(),
338: newPrefix);
339:
340: } else {
341:
342: // prefix bound to proper namespace; name is already ok
343:
344: }
345:
346: } else if (nsURI != null && nsURI.length() > 0) {
347:
348: // prefix isn't bound yet; bind it and the name is good to go
349: nsCtx.setPrefix(prefix, nsURI);
350: namespaces.add(factory.createNamespace(prefix, nsURI));
351:
352: }
353:
354: return name;
355:
356: } else if (nsURI != null && nsURI.length() > 0) {
357:
358: // name is namespaced, but has no prefix associated with it. Look for an
359: // existing prefix, or generate one.
360: String newPrefix = nsCtx.getPrefix(nsURI);
361: if (newPrefix == null) {
362:
363: // no existing prefix; need to generate a new prefix
364: newPrefix = generatePrefix(nsURI, nsCtx, namespaces);
365:
366: }
367:
368: // return the newly prefixed name
369: return new QName(nsURI, name.getLocalPart(), newPrefix);
370:
371: } else {
372:
373: // name belongs to no namespace and has no prefix.
374: return name;
375:
376: }
377:
378: }
379:
380: /**
381: * Generates a new namespace prefix for the specified namespace URI that
382: * doesn't collide with any existing prefix.
383: *
384: * @param nsURI The URI for which to generate a prefix.
385: * @param nsCtx The namespace context in which to set the prefix.
386: * @param namespaces A {@link List} of {@link Namespace} events to which the
387: * new prefix will be added.
388: * @return The new prefix.
389: */
390: private String generatePrefix(String nsURI,
391: SimpleNamespaceContext nsCtx, List namespaces) {
392:
393: String newPrefix;
394: int nsCount = 0;
395: do {
396:
397: newPrefix = "ns" + nsCount;
398: nsCount++;
399:
400: } while (nsCtx.getNamespaceURI(newPrefix) != null);
401:
402: nsCtx.setPrefix(newPrefix, nsURI);
403: namespaces.add(factory.createNamespace(newPrefix, nsURI));
404: return newPrefix;
405:
406: }
407:
408: public synchronized void close() throws XMLStreamException {
409:
410: if (closed) {
411:
412: return;
413:
414: }
415:
416: try {
417:
418: flush();
419:
420: } finally {
421:
422: closed = true;
423:
424: }
425:
426: }
427:
428: public synchronized void add(XMLEvent event)
429: throws XMLStreamException {
430:
431: if (closed) {
432:
433: throw new XMLStreamException("Writer has been closed");
434:
435: }
436:
437: // If the event is an Attribute or Namespace, cache it; otherwise, we
438: // should send any previously cached items
439: switch (event.getEventType()) {
440:
441: case XMLEvent.NAMESPACE:
442: cacheNamespace((Namespace) event);
443: return;
444:
445: case XMLEvent.ATTRIBUTE:
446: cacheAttribute((Attribute) event);
447: return;
448:
449: default:
450: // send any cached events
451: sendCachedEvents();
452:
453: }
454:
455: if (event.isStartElement()) {
456:
457: // cache the start element in case any following Attribute or
458: // Namespace events occur
459: lastStart = event.asStartElement();
460: return;
461:
462: } else if (event.isEndElement()) {
463:
464: if (nsStack.isEmpty()) {
465:
466: throw new XMLStreamException(
467: "Mismatched end element event: " + event);
468:
469: }
470:
471: SimpleNamespaceContext nsCtx = this .peekNamespaceStack();
472: EndElement endTag = event.asEndElement();
473: QName endElemName = endTag.getName();
474:
475: String prefix = endElemName.getPrefix();
476: String nsURI = endElemName.getNamespaceURI();
477: if (nsURI != null && nsURI.length() > 0) {
478:
479: // check that the prefix used in the name is bound to the same
480: // namespace
481: String boundURI = nsCtx.getNamespaceURI(prefix);
482: if (!nsURI.equals(boundURI)) {
483:
484: // not equal! now we must see what prefix it's actually
485: // bound to
486: String newPrefix = nsCtx.getPrefix(nsURI);
487: if (newPrefix != null) {
488:
489: QName newName = new QName(nsURI, endElemName
490: .getLocalPart(), newPrefix);
491: event = factory.createEndElement(newName,
492: endTag.getNamespaces());
493:
494: } else {
495:
496: // no prefix is bound! report an error
497: throw new XMLStreamException(
498: "EndElement namespace (" + nsURI
499: + ") isn't bound [" + endTag
500: + "]");
501:
502: }
503:
504: }
505:
506: } else {
507:
508: // default namespace
509: String defaultURI = nsCtx.getNamespaceURI("");
510: if (defaultURI != null && defaultURI.length() > 0) {
511:
512: throw new XMLStreamException(
513: "Unable to write "
514: + event
515: + " because default namespace is occluded by "
516: + defaultURI);
517:
518: }
519:
520: // else, namespace matches and can be written directly
521:
522: }
523:
524: // pop the stack
525: popNamespaceStack();
526:
527: }
528:
529: sendEvent(event);
530:
531: }
532:
533: public void add(XMLEventReader reader) throws XMLStreamException {
534:
535: while (reader.hasNext()) {
536:
537: add(reader.nextEvent());
538:
539: }
540:
541: }
542:
543: public synchronized String getPrefix(String nsURI)
544: throws XMLStreamException {
545:
546: return getNamespaceContext().getPrefix(nsURI);
547:
548: }
549:
550: public synchronized void setPrefix(String prefix, String nsURI)
551: throws XMLStreamException {
552:
553: peekNamespaceStack().setPrefix(prefix, nsURI);
554:
555: }
556:
557: public synchronized void setDefaultNamespace(String nsURI)
558: throws XMLStreamException {
559:
560: peekNamespaceStack().setDefaultNamespace(nsURI);
561:
562: }
563:
564: public synchronized void setNamespaceContext(NamespaceContext root)
565: throws XMLStreamException {
566:
567: SimpleNamespaceContext parent = (SimpleNamespaceContext) nsStack
568: .get(0);
569: parent.setParent(root);
570:
571: }
572:
573: public synchronized NamespaceContext getNamespaceContext() {
574:
575: return peekNamespaceStack();
576:
577: }
578:
579: /**
580: * Removes the active {@link SimpleNamespaceContext} from the top of the
581: * stack.
582: *
583: * @return The {@link SimpleNamespaceContext} removed from the namespace
584: * stack.
585: */
586: protected SimpleNamespaceContext popNamespaceStack() {
587:
588: return (SimpleNamespaceContext) nsStack
589: .remove(nsStack.size() - 1);
590:
591: }
592:
593: /**
594: * Returns the active {@link SimpleNamespaceContext} from the top of the
595: * stack.
596: *
597: * @return The active {@link SimpleNamespaceContext} from the top of the
598: * stack.
599: */
600: protected SimpleNamespaceContext peekNamespaceStack() {
601:
602: return (SimpleNamespaceContext) nsStack.get(nsStack.size() - 1);
603:
604: }
605:
606: /**
607: * Creates a new {@link SimpleNamespaceContext} and adds it to the top of
608: * the stack.
609: *
610: * @return The new {@link SimpleNamespaceContext}.
611: */
612: protected SimpleNamespaceContext pushNamespaceStack() {
613:
614: SimpleNamespaceContext nsCtx;
615:
616: SimpleNamespaceContext parent = peekNamespaceStack();
617: if (parent != null) {
618:
619: nsCtx = new SimpleNamespaceContext(parent);
620:
621: } else {
622:
623: nsCtx = new SimpleNamespaceContext();
624:
625: }
626:
627: nsStack.add(nsCtx);
628: return nsCtx;
629:
630: }
631:
632: /**
633: * Adds the specified {@link Attribute} to the attribute cache.
634: *
635: * @param attr The attribute to cache.
636: */
637: protected void cacheAttribute(Attribute attr) {
638:
639: attrBuff.put(attr.getName(), attr);
640:
641: }
642:
643: /**
644: * Adds the provided {@link Namespace} event to the namespace cache. The
645: * current namespace context will not be affected.
646: *
647: * @param ns The namespace to add to the cache.
648: */
649: protected void cacheNamespace(Namespace ns) {
650:
651: nsBuff.put(ns.getPrefix(), ns);
652:
653: }
654:
655: /**
656: * Called by the methods of this class to write the event to the stream.
657: *
658: * @param event The event to write.
659: * @throws XMLStreamException If an error occurs processing the event.
660: */
661: protected abstract void sendEvent(XMLEvent event)
662: throws XMLStreamException;
663:
664: }
|