All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javanet.staxutils.BaseXMLEventWriter Maven / Gradle / Ivy

There is a newer version: 2.2.1
Show newest version
/*
 * $Id: BaseXMLEventWriter.java,v 1.9 2005/03/09 22:34:34 cniles Exp $
 * 
 * Copyright (c) 2004, Christian Niles, Unit12
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *		*   Redistributions of source code must retain the above copyright
 *          notice, this list of conditions and the following disclaimer.
 * 
 *	    *	Redistributions in binary form must reproduce the above copyright
 *          notice, this list of conditions and the following disclaimer in the
 *          documentation and/or other materials provided with the distribution.
 * 
 *      *   Neither the name of Christian Niles, Unit12, nor the names of its
 *          contributors may be used to endorse or promote products derived from
 *          this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 */
package javanet.staxutils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

/**
 * Base class for {@link XMLEventWriter} implementations. This implemenation
 * buffers Attribute and Namespace events as specified in the specification,
 * maintains a stack of NamespaceContext instances based on the events it
 * receives, and repairs any missing namespaces. Subclasses should implement the
 * {@link #sendEvent(XMLEvent)} method to receive the processed events and
 * perform additional processing.
 * 
 * @author Christian Niles
 * @version $Revision: 1.9 $
 */
public abstract class BaseXMLEventWriter implements XMLEventWriter {

  /**
   * XMLEventFactory used to construct XMLEvent instances.
   */
  protected XMLEventFactory factory;

  /** list of {@link SimpleNamespaceContext}s. */
  protected List nsStack = new ArrayList();

  /**
   * Reference to the last StartElement sent. This will be null if no
   * StartElement has been sent, or after a non Attribute/Namespace event is
   * received.
   */
  protected StartElement lastStart;

  /**
   * LinkedHashMap of attribute events sent surrounding the last StartElement.
   * By using LinkedHashMap, the attributes will stay in the order they were
   * defined.
   */
  protected Map attrBuff = new LinkedHashMap();

  /**
   * LinkedHashMap of namespace events sent surrounding the last StartElement.
   * By using LinkedHashMap, the namespaces will stay in the order they were
   * defined.
   */
  protected Map nsBuff = new LinkedHashMap();

  /**
   * Whether this writer has been closed or not.
   */
  protected boolean closed;

  protected BaseXMLEventWriter() {

    this(null, null);

  }

  protected BaseXMLEventWriter(XMLEventFactory eventFactory, NamespaceContext nsCtx) {

    if (nsCtx != null) {

      nsStack.add(new SimpleNamespaceContext(nsCtx));

    } else {

      nsStack.add(new SimpleNamespaceContext());

    }

    if (eventFactory != null) {

      factory = eventFactory;

    } else {

      factory = XMLEventFactory.newInstance();

    }

  }

  public synchronized void flush() throws XMLStreamException {

    if (!closed) {

      sendCachedEvents();

    }

  }

  /**
   * Sends any events that have been cached in anticipation of further events.
   * 
   * @throws XMLStreamException
   *           If an error occurs sending the events.
   */
  private void sendCachedEvents() throws XMLStreamException {

    if (lastStart != null) {

      // A StartElement, and possibly attributes and namespaces, have been
      // cached. We need to combine them all into a single StartElement
      // event to send to sendEvent(XMLEvent)

      // First, construct the new NamespaceContext for the tag
      SimpleNamespaceContext nsCtx = this.pushNamespaceStack();

      // List used to store any defaulted/rewritten namespaces
      List namespaces = new ArrayList();

      // merge namespaces
      mergeNamespaces(lastStart.getNamespaces(), namespaces);
      mergeNamespaces(nsBuff.values().iterator(), namespaces);
      nsBuff.clear();

      // merge attributes
      List attributes = new ArrayList();
      mergeAttributes(lastStart.getAttributes(), namespaces, attributes);
      mergeAttributes(attrBuff.values().iterator(), namespaces, attributes);
      attrBuff.clear();

      // determine the name of the new start tag
      QName tagName = lastStart.getName();
      QName newName = processQName(tagName, namespaces);

      // construct new element
      StartElement newStart = factory.createStartElement(newName.getPrefix(),
          newName.getNamespaceURI(), newName.getLocalPart(), attributes.iterator(),
          namespaces.iterator(), nsCtx);

      lastStart = null;
      sendEvent(newStart);

    } else {

      // no start element was cached, but we may have cached some
      // namespaces and attributes that need to be written.

      for (Iterator i = nsBuff.values().iterator(); i.hasNext();) {

        XMLEvent evt = (XMLEvent) i.next();
        sendEvent(evt);

      }
      nsBuff.clear();

      for (Iterator i = attrBuff.values().iterator(); i.hasNext();) {

        XMLEvent evt = (XMLEvent) i.next();
        sendEvent(evt);

      }
      attrBuff.clear();

    }

  }

  /**
   * Merges a set of {@link Attribute} events, possibly adding additional
   * {@link Namespace} events if the attribute's prefix isn't bound in the
   * provided context.
   * 
   * @param iter
   *          An {@link Iterator} of {@link Attribute} events.
   * @param namespaces
   *          A {@link List} to which any new {@link Namespace} events should be
   *          added.
   * @param attributes
   *          A {@link List} to which all {@link Attributes} events should be
   *          merged.
   */
  private void mergeAttributes(Iterator iter, List namespaces, List attributes) {

    while (iter.hasNext()) {

      Attribute attr = (Attribute) iter.next();

      // check if the attribute QName has the proper mapping
      QName attrName = attr.getName();
      QName newName = processQName(attrName, namespaces);
      if (!attrName.equals(newName)) {

        // need to generate a new attribute with the new qualified name
        Attribute newAttr = factory.createAttribute(newName, attr.getValue());
        attributes.add(newAttr);

      } else {

        // the attribute is fine
        attributes.add(attr);

      }

    }

  }

  /**
   * Merges a set of {@link Namespaces} events into the provided {@link List}.
   * 
   * @param iter
   *          An {@link Iterator} of {@link Namespace}s to merge.
   * @param namespaces
   *          A {@link List} containing all added {@link Namespace} events.
   * @throws XMLStreamException
   *           If a conflicting namespace binding is encountered.
   */
  private void mergeNamespaces(Iterator iter, List namespaces) throws XMLStreamException {

    // for each namespace, add it to the context, and place it in the list
    while (iter.hasNext()) {

      Namespace ns = (Namespace) iter.next();
      String prefix = ns.getPrefix();
      String nsURI = ns.getNamespaceURI();
      SimpleNamespaceContext nsCtx = this.peekNamespaceStack();

      if (!nsCtx.isPrefixDeclared(prefix)) {

        // mapping doesn't exist, so add it to the context/list
        if (prefix == null || prefix.length() == 0) {

          nsCtx.setDefaultNamespace(nsURI);

        } else {

          nsCtx.setPrefix(prefix, nsURI);

        }

        namespaces.add(ns);

      } else if (!nsCtx.getNamespaceURI(prefix).equals(nsURI)) {

        throw new XMLStreamException("Prefix already declared: " + ns, ns.getLocation());

      } else {

        // duplicate namespace declaration

      }

    }

  }

  /**
   * Processes a {@link QName}, possibly rewriting it to match the current
   * namespace context.
   * 
   * @param name
   *          The {@link QName} to process.
   * @param namespaces
   *          A {@link List} of {@link Namespace} events to which any new
   *          namespace bindings should be added.
   * @return The new name, or the name parameter if no changes were
   *         necessary.
   */
  private QName processQName(QName name, List namespaces) {

    // get current context
    SimpleNamespaceContext nsCtx = this.peekNamespaceStack();

    // get name parts
    String nsURI = name.getNamespaceURI();
    String prefix = name.getPrefix();
    if (prefix != null && prefix.length() > 0) {

      // prefix provided; see if it is okay in current context, otherwise
      // we'll
      // have to rewrite it
      String resolvedNS = nsCtx.getNamespaceURI(prefix);
      if (resolvedNS != null) {

        if (!resolvedNS.equals(nsURI)) {

          // prefix is already bound to a different namespace; we have
          // to
          // find or generate and alternative prefix
          String newPrefix = nsCtx.getPrefix(nsURI);
          if (newPrefix == null) {

            // no existing prefix; need to generate a new prefix
            newPrefix = generatePrefix(nsURI, nsCtx, namespaces);

          }

          // return the newly prefixed name
          return new QName(nsURI, name.getLocalPart(), newPrefix);

        } else {

          // prefix bound to proper namespace; name is already ok

        }

      } else if (nsURI != null && nsURI.length() > 0) {

        // prefix isn't bound yet; bind it and the name is good to go
        nsCtx.setPrefix(prefix, nsURI);
        namespaces.add(factory.createNamespace(prefix, nsURI));

      }

      return name;

    } else if (nsURI != null && nsURI.length() > 0) {

      // name is namespaced, but has no prefix associated with it. Look
      // for an
      // existing prefix, or generate one.
      String newPrefix = nsCtx.getPrefix(nsURI);
      if (newPrefix == null) {

        // no existing prefix; need to generate a new prefix
        newPrefix = generatePrefix(nsURI, nsCtx, namespaces);

      }

      // return the newly prefixed name
      return new QName(nsURI, name.getLocalPart(), newPrefix);

    } else {

      // name belongs to no namespace and has no prefix.
      return name;

    }

  }

  /**
   * Generates a new namespace prefix for the specified namespace URI that
   * doesn't collide with any existing prefix.
   * 
   * @param nsURI
   *          The URI for which to generate a prefix.
   * @param nsCtx
   *          The namespace context in which to set the prefix.
   * @param namespaces
   *          A {@link List} of {@link Namespace} events to which the new prefix
   *          will be added.
   * @return The new prefix.
   */
  private String generatePrefix(String nsURI, SimpleNamespaceContext nsCtx, List namespaces) {

    String newPrefix;
    int nsCount = 0;
    do {

      newPrefix = "ns" + nsCount;
      nsCount++;

    } while (nsCtx.getNamespaceURI(newPrefix) != null);

    nsCtx.setPrefix(newPrefix, nsURI);
    namespaces.add(factory.createNamespace(newPrefix, nsURI));
    return newPrefix;

  }

  public synchronized void close() throws XMLStreamException {

    if (closed) {

      return;

    }

    try {

      flush();

    } finally {

      closed = true;

    }

  }

  public synchronized void add(XMLEvent event) throws XMLStreamException {

    if (closed) {

      throw new XMLStreamException("Writer has been closed");

    }

    // If the event is an Attribute or Namespace, cache it; otherwise, we
    // should send any previously cached items
    switch (event.getEventType()) {

    case XMLEvent.NAMESPACE:
      cacheNamespace((Namespace) event);
      return;

    case XMLEvent.ATTRIBUTE:
      cacheAttribute((Attribute) event);
      return;

    default:
      // send any cached events
      sendCachedEvents();

    }

    if (event.isStartElement()) {

      // cache the start element in case any following Attribute or
      // Namespace events occur
      lastStart = event.asStartElement();
      return;

    } else if (event.isEndElement()) {

      if (nsStack.isEmpty()) {

        throw new XMLStreamException("Mismatched end element event: " + event);

      }

      SimpleNamespaceContext nsCtx = this.peekNamespaceStack();
      EndElement endTag = event.asEndElement();
      QName endElemName = endTag.getName();

      String prefix = endElemName.getPrefix();
      String nsURI = endElemName.getNamespaceURI();
      if (nsURI != null && nsURI.length() > 0) {

        // check that the prefix used in the name is bound to the same
        // namespace
        String boundURI = nsCtx.getNamespaceURI(prefix);
        if (!nsURI.equals(boundURI)) {

          // not equal! now we must see what prefix it's actually
          // bound to
          String newPrefix = nsCtx.getPrefix(nsURI);
          if (newPrefix != null) {

            QName newName = new QName(nsURI, endElemName.getLocalPart(), newPrefix);
            event = factory.createEndElement(newName, endTag.getNamespaces());

          } else {

            // no prefix is bound! report an error
            throw new XMLStreamException("EndElement namespace (" + nsURI + ") isn't bound ["
                + endTag + "]");

          }

        }

      } else {

        // default namespace
        String defaultURI = nsCtx.getNamespaceURI("");
        if (defaultURI != null && defaultURI.length() > 0) {

          throw new XMLStreamException("Unable to write " + event
              + " because default namespace is occluded by " + defaultURI);

        }

        // else, namespace matches and can be written directly

      }

      // pop the stack
      popNamespaceStack();

    }

    sendEvent(event);

  }

  public void add(XMLEventReader reader) throws XMLStreamException {

    while (reader.hasNext()) {

      add(reader.nextEvent());

    }

  }

  public synchronized String getPrefix(String nsURI) throws XMLStreamException {

    return getNamespaceContext().getPrefix(nsURI);

  }

  public synchronized void setPrefix(String prefix, String nsURI) throws XMLStreamException {

    peekNamespaceStack().setPrefix(prefix, nsURI);

  }

  public synchronized void setDefaultNamespace(String nsURI) throws XMLStreamException {

    peekNamespaceStack().setDefaultNamespace(nsURI);

  }

  public synchronized void setNamespaceContext(NamespaceContext root) throws XMLStreamException {

    SimpleNamespaceContext parent = (SimpleNamespaceContext) nsStack.get(0);
    parent.setParent(root);

  }

  public synchronized NamespaceContext getNamespaceContext() {

    return peekNamespaceStack();

  }

  /**
   * Removes the active {@link SimpleNamespaceContext} from the top of the
   * stack.
   * 
   * @return The {@link SimpleNamespaceContext} removed from the namespace
   *         stack.
   */
  protected SimpleNamespaceContext popNamespaceStack() {

    return (SimpleNamespaceContext) nsStack.remove(nsStack.size() - 1);

  }

  /**
   * Returns the active {@link SimpleNamespaceContext} from the top of the
   * stack.
   * 
   * @return The active {@link SimpleNamespaceContext} from the top of the
   *         stack.
   */
  protected SimpleNamespaceContext peekNamespaceStack() {

    return (SimpleNamespaceContext) nsStack.get(nsStack.size() - 1);

  }

  /**
   * Creates a new {@link SimpleNamespaceContext} and adds it to the top of the
   * stack.
   * 
   * @return The new {@link SimpleNamespaceContext}.
   */
  protected SimpleNamespaceContext pushNamespaceStack() {

    SimpleNamespaceContext nsCtx;

    SimpleNamespaceContext parent = peekNamespaceStack();
    if (parent != null) {

      nsCtx = new SimpleNamespaceContext(parent);

    } else {

      nsCtx = new SimpleNamespaceContext();

    }

    nsStack.add(nsCtx);
    return nsCtx;

  }

  /**
   * Adds the specified {@link Attribute} to the attribute cache.
   * 
   * @param attr
   *          The attribute to cache.
   */
  protected void cacheAttribute(Attribute attr) {

    attrBuff.put(attr.getName(), attr);

  }

  /**
   * Adds the provided {@link Namespace} event to the namespace cache. The
   * current namespace context will not be affected.
   * 
   * @param ns
   *          The namespace to add to the cache.
   */
  protected void cacheNamespace(Namespace ns) {

    nsBuff.put(ns.getPrefix(), ns);

  }

  /**
   * Called by the methods of this class to write the event to the stream.
   * 
   * @param event
   *          The event to write.
   * @throws XMLStreamException
   *           If an error occurs processing the event.
   */
  protected abstract void sendEvent(XMLEvent event) throws XMLStreamException;

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy