package com.fasterxml.aalto.dom;

import java.util.*;

import javax.xml.XMLConstants;
import javax.xml.namespace.*;
import javax.xml.stream.*;
import javax.xml.transform.dom.DOMResult;

import org.w3c.dom.*;

import org.codehaus.stax2.ri.EmptyNamespaceContext;
import org.codehaus.stax2.ri.dom.DOMWrappingWriter;

import com.fasterxml.aalto.out.WriterConfig;

This is an adapter class that allows building a DOM tree using XMLStreamWriter interface.

Note that the implementation is only to be used for use with javax.xml.transform.dom.DOMResult.

Some notes regarding missing/incomplete functionality:

  • Namespace-repairing mode not implemented
  • Validation functionality not implemented
Author:Tatu Saloranta
/** * This is an adapter class that allows building a DOM tree using * {@link XMLStreamWriter} interface. *<p> * Note that the implementation is only to be used for use with * <code>javax.xml.transform.dom.DOMResult</code>. *<p> * Some notes regarding missing/incomplete functionality: * <ul> * <li>Namespace-repairing mode not implemented * <li> * <li>Validation functionality not implemented * <li> * </ul> * @author Tatu Saloranta */
public final class DOMWriterImpl extends DOMWrappingWriter { /* //////////////////////////////////////////////////// // Configuration //////////////////////////////////////////////////// */ protected final WriterConfig _config; /* //////////////////////////////////////////////////// // State //////////////////////////////////////////////////// */
This element is the current context element, under which all other nodes are added, until matching end element is output. Null outside of the main element tree.

Note: explicit empty element (written using writeEmptyElement) will never become current element.

/** * This element is the current context element, under which * all other nodes are added, until matching end element * is output. Null outside of the main element tree. *<p> * Note: explicit empty element (written using * <code>writeEmptyElement</code>) will never become * current element. */
protected DOMOutputElement _currElem;
This element is non-null right after a call to either writeStartElement and writeEmptyElement, and can be used to add attributes and namespace declarations.

Note: while this is often the same as _currElem, it's not always. Specifically, an empty element (written explicitly using writeEmptyElement) will become open element but NOT current element. Conversely, regular elements will remain current element when non elements are written (text, comments, PI), but not the open element.

/** * This element is non-null right after a call to * either <code>writeStartElement</code> and * <code>writeEmptyElement</code>, and can be used to * add attributes and namespace declarations. *<p> * Note: while this is often the same as {@link #_currElem}, * it's not always. Specifically, an empty element (written * explicitly using <code>writeEmptyElement</code>) will * become open element but NOT current element. Conversely, * regular elements will remain current element when * non elements are written (text, comments, PI), but * not the open element. */
protected DOMOutputElement _openElement;
for NsRepairing mode
/** * for NsRepairing mode */
protected int[] _autoNsSeq; protected String _suggestedDefNs = null; protected String _automaticNsPrefix;
Map that contains URI-to-prefix entries that point out suggested prefixes for URIs. These are populated by calls to setPrefix, and they are only used as hints for binding; if there are conflicts, repairing writer can just use some other prefix.
/** * Map that contains URI-to-prefix entries that point out suggested * prefixes for URIs. These are populated by calls to * {@link #setPrefix}, and they are only used as hints for binding; * if there are conflicts, repairing writer can just use some other * prefix. */
HashMap<String,String> _suggestedPrefixes = null; /* //////////////////////////////////////////////////// // Life-cycle //////////////////////////////////////////////////// */ private DOMWriterImpl(WriterConfig cfg, Node treeRoot) throws XMLStreamException { super(treeRoot, true, cfg.willRepairNamespaces()); _config = cfg; _autoNsSeq = null; _automaticNsPrefix = cfg.getAutomaticNsPrefix(); /* Ok; we need a document node; or an element node; or a document * fragment node. */ switch (treeRoot.getNodeType()) { case Node.DOCUMENT_NODE: case Node.DOCUMENT_FRAGMENT_NODE: // both are ok, but no current element _currElem = DOMOutputElement.createRoot(); _openElement = null; break; case Node.ELEMENT_NODE: // can make sub-tree... ok { // still need a virtual root node as parent DOMOutputElement root = DOMOutputElement.createRoot(); Element elem = (Element) treeRoot; _openElement = _currElem = root.createChild(elem); } break; default: // other Nodes not usable throw new XMLStreamException("Can not create an XMLStreamWriter for a DOM node of type "+treeRoot.getClass()); } } public static DOMWriterImpl createFrom(WriterConfig cfg, DOMResult dst) throws XMLStreamException { Node rootNode = dst.getNode(); return new DOMWriterImpl(cfg, rootNode); } /* //////////////////////////////////////////////////// // XMLStreamWriter API (Stax 1.0) //////////////////////////////////////////////////// */ //public void close() { } //public void flush() { } @Override public NamespaceContext getNamespaceContext() { if (!mNsAware) { return EmptyNamespaceContext.getInstance(); } return _currElem; } @Override public String getPrefix(String uri) { if (!mNsAware) { return null; } if (mNsContext != null) { String prefix = mNsContext.getPrefix(uri); if (prefix != null) { return prefix; } } return _currElem.getPrefix(uri); } @Override public Object getProperty(String name) { /* Here we don't want to throw an exception, should the property * not be supported; thus passing false as second arg */ return _config.getProperty(name, false); } @Override public void setDefaultNamespace(String uri) { _suggestedDefNs = (uri == null || uri.length() == 0) ? null : uri; } //public void setNamespaceContext(NamespaceContext context) @Override public void setPrefix(String prefix, String uri) throws XMLStreamException { if (prefix == null) { throw new NullPointerException("Can not pass null 'prefix' value"); } // Are we actually trying to set the default namespace? if (prefix.length() == 0) { setDefaultNamespace(uri); return; } if (uri == null) { throw new NullPointerException("Can not pass null 'uri' value"); } /* Let's verify that xml/xmlns are never (mis)declared; as * mandated by XML NS specification */ { if (prefix.equals("xml")) { if (!uri.equals(XMLConstants.XML_NS_URI)) { throwOutputError("Trying to redeclare prefix 'xml' from its default URI '"+XMLConstants.XML_NS_URI+"' to \""+uri+"\""); } } else if (prefix.equals("xmlns")) { // prefix "xmlns" if (!uri.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) { throwOutputError("Trying to declare prefix 'xmlns' (illegal as per NS 1.1 #4)"); } // At any rate; we are NOT to output it return; } else { // Neither of prefixes.. but how about URIs? if (uri.equals(XMLConstants.XML_NS_URI)) { throwOutputError("Trying to bind URI '" +XMLConstants.XML_NS_URI+" to prefix \""+prefix+"\" (can only bind to 'xml')"); } else if (uri.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) { throwOutputError("Trying to bind URI '" +XMLConstants.XMLNS_ATTRIBUTE_NS_URI+" to prefix \""+prefix+"\" (can not be explicitly bound)"); } } } if (_suggestedPrefixes == null) { _suggestedPrefixes = new HashMap<String,String>(16); } _suggestedPrefixes.put(uri, prefix); } @Override public void writeAttribute(String localName, String value) throws XMLStreamException { outputAttribute(null, null, localName, value); } @Override public void writeAttribute(String nsURI, String localName, String value) throws XMLStreamException { outputAttribute(nsURI, null, localName, value); } @Override public void writeAttribute(String prefix, String nsURI, String localName, String value) throws XMLStreamException { outputAttribute(nsURI, prefix, localName, value); } //public void writeCData(String data) //public void writeCharacters(char[] text, int start, int len) //public void writeCharacters(String text) //public void writeComment(String data) @Override public void writeDefaultNamespace(String nsURI) { if (_openElement == null) { throw new IllegalStateException("No currently open START_ELEMENT, cannot write attribute"); } setDefaultNamespace(nsURI); _openElement.addAttribute(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns", nsURI); } //public void writeDTD(String dtd) @Override public void writeEmptyElement(String localName) throws XMLStreamException { writeEmptyElement(null, localName); } @Override public void writeEmptyElement(String nsURI, String localName) throws XMLStreamException { // First things first: must /* Note: can not just call writeStartElement(), since this * element will only become the open elem, but not a parent elem */ createStartElem(nsURI, null, localName, true); } @Override public void writeEmptyElement(String prefix, String localName, String nsURI) throws XMLStreamException { if (prefix == null) { // passing null would mean "dont care", if repairing prefix = ""; } createStartElem(nsURI, prefix, localName, true); } @Override public void writeEndDocument() { _currElem = _openElement = null; } @Override public void writeEndElement() { // Simple, just need to traverse up... if we can if (_currElem == null || _currElem.isRoot()) { throw new IllegalStateException("No open start element to close"); } _openElement = null; // just in case it was open _currElem = _currElem.getParent(); } @Override public void writeNamespace(String prefix, String nsURI) throws XMLStreamException { if (prefix == null || prefix.length() == 0) { writeDefaultNamespace(nsURI); return; } if (!mNsAware) { throwOutputError("Can not write namespaces with non-namespace writer."); } outputAttribute(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns", prefix, nsURI); _currElem.addPrefix(prefix, nsURI); } //public void writeProcessingInstruction(String target) //public void writeProcessingInstruction(String target, String data) //public void writeStartDocument() //public void writeStartDocument(String version) //public void writeStartDocument(String encoding, String version) @Override public void writeStartElement(String localName) throws XMLStreamException { writeStartElement(null, localName); } @Override public void writeStartElement(String nsURI, String localName) throws XMLStreamException { createStartElem(nsURI, null, localName, false); } @Override public void writeStartElement(String prefix, String localName, String nsURI) throws XMLStreamException { createStartElem(nsURI, prefix, localName, false); } /* //////////////////////////////////////////////////// // XMLStreamWriter2 API (Stax2 v2.0) //////////////////////////////////////////////////// */ @Override public boolean isPropertySupported(String name) { // !!! TBI: not all these properties are really supported return _config.isPropertySupported(name); } @Override public boolean setProperty(String name, Object value) { /* Note: can not call local method, since it'll return false for * recognized but non-mutable properties */ return _config.setProperty(name, value); } //public XMLValidator validateAgainst(XMLValidationSchema schema) //public XMLValidator stopValidatingAgainst(XMLValidationSchema schema) //public XMLValidator stopValidatingAgainst(XMLValidator validator) //public ValidationProblemHandler setValidationProblemHandler(ValidationProblemHandler h) //public XMLStreamLocation2 getLocation() //public String getEncoding() { //public void writeCData(char[] text, int start, int len) @Override public void writeDTD(String rootName, String systemId, String publicId, String internalSubset) throws XMLStreamException { /* Alas: although we can create a DocumentType object, there * doesn't seem to be a way to attach it in DOM-2! */ if (_currElem != null) { throw new IllegalStateException("Operation only allowed to the document before adding root element"); } reportUnsupported("writeDTD()"); } //public void writeFullEndElement() throws XMLStreamException //public void writeSpace(char[] text, int start, int len) //public void writeSpace(String text) //public void writeStartDocument(String version, String encoding, boolean standAlone) /* //////////////////////////////////////////// // Impls of abstract methods from base class //////////////////////////////////////////// */ @Override protected void appendLeaf(Node n) throws IllegalStateException { _currElem.appendNode(n); _openElement = null; } /* /////////////////////////////// // Internal methods /////////////////////////////// */ /* Note: copied from regular RepairingNsStreamWriter#writeStartOrEmpty * (and its non-repairing counterpart). */
Method called by all start element write methods.
Params:
  • nsURI – Namespace URI to use: null and empty String denote 'no namespace'
/** * Method called by all start element write methods. * * @param nsURI Namespace URI to use: null and empty String denote 'no namespace' */
protected void createStartElem(String nsURI, String prefix, String localName, boolean isEmpty) throws XMLStreamException { DOMOutputElement elem; if (!mNsAware) { if(nsURI != null && nsURI.length() > 0) { throwOutputError("Can not specify non-empty uri/prefix in non-namespace mode"); } elem = _currElem.createAndAttachChild(mDocument.createElement(localName)); } else { if (mNsRepairing) { String actPrefix = validateElemPrefix(prefix, nsURI, _currElem); if (actPrefix != null) { // fine, an existing binding we can use: if (actPrefix.length() != 0) { elem = _currElem.createAndAttachChild(mDocument.createElementNS(nsURI, actPrefix+":"+localName)); } else { elem = _currElem.createAndAttachChild(mDocument.createElementNS(nsURI, localName)); } } else { // nah, need to create a new binding... /* Need to ensure that we'll pass "" as prefix, not null, * so it is understood as "I want to use the default NS", * not as "whatever prefix, I don't care" */ if (prefix == null) { prefix = ""; } actPrefix = generateElemPrefix(prefix, nsURI, _currElem); boolean hasPrefix = (actPrefix.length() != 0); if (hasPrefix) { localName = actPrefix + ":" + localName; } elem = _currElem.createAndAttachChild(mDocument.createElementNS(nsURI, localName)); /* Hmmh. writeNamespace method requires open element * to be defined. So we'll need to set it first * (will be set again at a later point -- would be * good to refactor this method into separate * sub-classes or so) */ _openElement = elem; // Need to add new ns declaration as well if (hasPrefix) { writeNamespace(actPrefix, nsURI); elem.addPrefix(actPrefix, nsURI); } else { writeDefaultNamespace(nsURI); elem.setDefaultNsUri(nsURI); } } } else { /* Non-repairing; if non-null prefix (including "" to * indicate "no prefix") passed, use as is, otherwise * try to locate the prefix if got namespace. */ if (prefix == null && nsURI != null && nsURI.length() > 0) { prefix = (_suggestedPrefixes == null) ? null : (String) _suggestedPrefixes.get(nsURI); if (prefix == null) { throwOutputError("Can not find prefix for namespace \""+nsURI+"\""); } } if (prefix != null && prefix.length() != 0) { localName = prefix + ":" +localName; } elem = _currElem.createAndAttachChild(mDocument.createElementNS(nsURI, localName)); } } /* Got the element; need to make it the open element, and * if it's not an (explicit) empty element, current element as well */ _openElement = elem; if (!isEmpty) { _currElem = elem; } } protected void outputAttribute(String nsURI, String prefix, String localName, String value) throws XMLStreamException { if (_openElement == null) { throw new IllegalStateException("No currently open START_ELEMENT, cannot write attribute"); } if (mNsAware) { if (mNsRepairing) { prefix = findOrCreateAttrPrefix(prefix, nsURI, _openElement); } if (prefix != null && prefix.length() > 0) { localName = prefix + ":" + localName; } _openElement.addAttribute(nsURI, localName, value); } else { // non-ns, simple if (prefix != null && prefix.length() > 0) { localName = prefix + ":" + localName; } _openElement.addAttribute(localName, value); } } private final String validateElemPrefix(String prefix, String nsURI, DOMOutputElement elem) throws XMLStreamException { /* 06-Feb-2005, TSa: Special care needs to be taken for the * "empty" (or missing) namespace: * (see comments from findOrCreatePrefix()) */ if (nsURI == null || nsURI.length() == 0) { String currURL = elem.getDefaultNsUri(); if (currURL == null || currURL.length() == 0) { // Ok, good: return ""; } // Nope, needs to be re-bound: return null; } int status = elem.isPrefixValid(prefix, nsURI, true); if (status == DOMOutputElement.PREFIX_OK) { return prefix; } return null; } /* //////////////////////////////////////////////////// // Internal methods //////////////////////////////////////////////////// */
Method called to find an existing prefix for the given namespace, if any exists in the scope. If one is found, it's returned (including "" for the current default namespace); if not, null is returned.
Params:
  • nsURI – URI of namespace for which we need a prefix
/** * Method called to find an existing prefix for the given namespace, * if any exists in the scope. If one is found, it's returned (including * "" for the current default namespace); if not, null is returned. * * @param nsURI URI of namespace for which we need a prefix */
protected final String findElemPrefix(String nsURI, DOMOutputElement elem) throws XMLStreamException { /* Special case: empty NS URI can only be bound to the empty * prefix... */ if (nsURI == null || nsURI.length() == 0) { String currDefNsURI = elem.getDefaultNsUri(); if (currDefNsURI != null && currDefNsURI.length() > 0) { // Nope; won't do... has to be re-bound, but not here: return null; } return ""; } return _currElem.getPrefix(nsURI); }
Method called after findElemPrefix has returned null, to create and bind a namespace mapping for specified namespace.
/** * Method called after {@link #findElemPrefix} has returned null, * to create and bind a namespace mapping for specified namespace. */
protected final String generateElemPrefix(String suggPrefix, String nsURI, DOMOutputElement elem) throws XMLStreamException { /* Ok... now, since we do not have an existing mapping, let's * see if we have a preferred prefix to use. */ /* Except if we need the empty namespace... that can only be * bound to the empty prefix: */ if (nsURI == null || nsURI.length() == 0) { return ""; } /* Ok; with elements this is easy: the preferred prefix can * ALWAYS be used, since it can mask preceding bindings: */ if (suggPrefix == null) { // caller wants this URI to map as the default namespace? if (_suggestedDefNs != null && _suggestedDefNs.equals(nsURI)) { suggPrefix = ""; } else { suggPrefix = (_suggestedPrefixes == null) ? null: (String) _suggestedPrefixes.get(nsURI); if (suggPrefix == null) { /* 16-Oct-2005, TSa: We have 2 choices here, essentially; * could make elements always try to override the def * ns... or can just generate new one. Let's do latter * for now. */ if (_autoNsSeq == null) { _autoNsSeq = new int[1]; _autoNsSeq[0] = 1; } suggPrefix = elem.generateMapping(_automaticNsPrefix, nsURI, _autoNsSeq); } } } // Ok; let's let the caller deal with bindings return suggPrefix; }
Method called to somehow find a prefix for given namespace, to be used for a new start element; either use an existing one, or generate a new one. If a new mapping needs to be generated, it will also be automatically bound, and necessary namespace declaration output.
Params:
  • suggPrefix – Suggested prefix to bind, if any; may be null to indicate "no preference"
  • nsURI – URI of namespace for which we need a prefix
  • elem – Currently open start element, on which the attribute will be added.
/** * Method called to somehow find a prefix for given namespace, to be * used for a new start element; either use an existing one, or * generate a new one. If a new mapping needs to be generated, * it will also be automatically bound, and necessary namespace * declaration output. * * @param suggPrefix Suggested prefix to bind, if any; may be null * to indicate "no preference" * @param nsURI URI of namespace for which we need a prefix * @param elem Currently open start element, on which the attribute * will be added. */
protected final String findOrCreateAttrPrefix(String suggPrefix, String nsURI, DOMOutputElement elem) throws XMLStreamException { if (nsURI == null || nsURI.length() == 0) { /* Attributes never use the default namespace; missing * prefix always leads to the empty ns... so nothing * special is needed here. */ return null; } // Maybe the suggested prefix is properly bound? if (suggPrefix != null) { int status = elem.isPrefixValid(suggPrefix, nsURI, false); if (status == OutputElementBase.PREFIX_OK) { return suggPrefix; } /* Otherwise, if the prefix is unbound, let's just bind * it -- if caller specified a prefix, it probably prefers * binding that prefix even if another prefix already existed? * The remaining case (already bound to another URI) we don't * want to touch, at least not yet: it may or not be safe * to change binding, so let's just not try it. */ if (status == OutputElementBase.PREFIX_UNBOUND) { elem.addPrefix(suggPrefix, nsURI); writeNamespace(suggPrefix, nsURI); return suggPrefix; } } // If not, perhaps there's another existing binding available? String prefix = elem.getExplicitPrefix(nsURI); if (prefix != null) { // already had a mapping for the URI... cool. return prefix; } /* Nope, need to create one. First, let's see if there's a * preference... */ if (suggPrefix != null) { prefix = suggPrefix; } else if (_suggestedPrefixes != null) { prefix = (String) _suggestedPrefixes.get(nsURI); // note: def ns is never added to suggested prefix map } if (prefix != null) { /* Can not use default namespace for attributes. * Also, re-binding is tricky for attributes; can't * re-bind anything that's bound on this scope... or * used in this scope. So, to simplify life, let's not * re-bind anything for attributes. */ if (prefix.length() == 0 || (elem.getNamespaceURI(prefix) != null)) { prefix = null; } } if (prefix == null) { if (_autoNsSeq == null) { _autoNsSeq = new int[1]; _autoNsSeq[0] = 1; } prefix = _currElem.generateMapping(_automaticNsPrefix, nsURI, _autoNsSeq); } // Ok; so far so good: let's now bind and output the namespace: elem.addPrefix(prefix, nsURI); writeNamespace(prefix, nsURI); return prefix; } }