package com.ctc.wstx.dom; import java.text.MessageFormat; 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.XMLStreamLocation2; import org.codehaus.stax2.XMLStreamReader2; import org.codehaus.stax2.XMLStreamWriter2; import org.codehaus.stax2.validation.ValidationProblemHandler; import org.codehaus.stax2.validation.XMLValidationSchema; import org.codehaus.stax2.validation.XMLValidator; import com.ctc.wstx.api.WriterConfig; import com.ctc.wstx.api.WstxOutputProperties; import com.ctc.wstx.cfg.ErrorConsts; import com.ctc.wstx.sw.SimpleOutputElement; import com.ctc.wstx.util.BijectiveNsMap; import com.ctc.wstx.util.DefaultXmlSymbolTable; import com.ctc.wstx.util.EmptyNamespaceContext; /** * This is an adapter class that allows building a DOM tree using * {@link 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: *

* * @author Tatu Saloranta * @author Dan Diephouse */ public class DOMWrappingWriter implements XMLStreamWriter2 { /* //////////////////////////////////////////////////// // Constants //////////////////////////////////////////////////// */ final protected static String sPrefixXml = DefaultXmlSymbolTable.getXmlSymbol(); final protected static String sPrefixXmlns = DefaultXmlSymbolTable.getXmlnsSymbol(); final protected static String ERR_NSDECL_WRONG_STATE = "Trying to write a namespace declaration when there is no open start element."; /* //////////////////////////////////////////////////// // Configuration //////////////////////////////////////////////////// */ protected final WriterConfig mConfig; protected final boolean mNsAware; protected final boolean mNsRepairing; /** * This member variable is to keep information about encoding * that seems to be used for the document (or fragment) to output, * if known. */ protected String mEncoding = null; /** * If we are being given info about existing bindings, it'll come * as a NamespaceContet. */ protected NamespaceContext mNsContext; /* //////////////////////////////////////////////////// // State //////////////////////////////////////////////////// */ /** * We need a reference to the document hosting nodes to * be able to create new nodes */ protected final Document mDocument; /** * 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. */ // protected Element mParentElem; protected DOMOutputElement mParentElem; /** * This element is non-null right after a call to * either writeStartElement and * writeEmptyElement, and can be used to * add attributes and namespace declarations. */ // protected Element mOpenElement; protected DOMOutputElement mOpenElement; /** * for NsRepairing mode */ protected int[] mAutoNsSeq; protected String mSuggestedDefNs = null; protected String mAutomaticNsPrefix; /** * 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 mSuggestedPrefixes = null; /* //////////////////////////////////////////////////// // Life-cycle //////////////////////////////////////////////////// */ private DOMWrappingWriter(WriterConfig cfg, Node treeRoot) throws XMLStreamException { if (treeRoot == null) { throw new IllegalArgumentException("Can not pass null Node for constructing a DOM-based XMLStreamWriter"); } mConfig = cfg; mNsAware = cfg.willSupportNamespaces(); mNsRepairing = mNsAware && cfg.automaticNamespacesEnabled(); mAutoNsSeq = null; mAutomaticNsPrefix = null; /* 15-Sep-2007, TSa: Repairing mode not yet supported, so better * signal that right away */ if (mNsRepairing) { // throw new XMLStreamException("Repairing mode not (yet) supported with DOM-backed writer"); mAutomaticNsPrefix = mConfig.getAutomaticNsPrefix(); } Element elem = null; /* Ok; we need a document node; or an element node; or a document * fragment node. */ switch (treeRoot.getNodeType()) { case Node.DOCUMENT_NODE: // fine mDocument = (Document) treeRoot; /* Should try to find encoding, version and stand-alone * settings... but is there a standard way of doing that? */ break; case Node.ELEMENT_NODE: // can make sub-tree... ok mDocument = treeRoot.getOwnerDocument(); elem = (Element) treeRoot; break; case Node.DOCUMENT_FRAGMENT_NODE: // as with element... mDocument = treeRoot.getOwnerDocument(); // Above types are fine break; default: // other Nodes not usable throw new XMLStreamException("Can not create an XMLStreamWriter for a DOM node of type "+treeRoot.getClass()); } if (mDocument == null) { throw new XMLStreamException("Can not create an XMLStreamWriter for given node (of type "+treeRoot.getClass()+"): did not have owner document"); } mParentElem = DOMOutputElement.createRoot(mDocument); mOpenElement = null; if(elem != null) { // mOpenElement = mParentElem = mParentElem.createChild(elem); mParentElem = mParentElem.createChild(); mParentElem.setElement(elem); mOpenElement = mParentElem; } } public static DOMWrappingWriter createFrom(WriterConfig cfg, DOMResult dst) throws XMLStreamException { Node rootNode = dst.getNode(); return new DOMWrappingWriter(cfg, rootNode); } /* //////////////////////////////////////////////////// // XMLStreamWriter API (Stax 1.0) //////////////////////////////////////////////////// */ public void close() { // NOP } public void flush() { // NOP } public NamespaceContext getNamespaceContext() { if (!mNsAware) { return EmptyNamespaceContext.getInstance(); } // // !!! TBI: // return mNsContext; return mParentElem; } public String getPrefix(String uri) { if (!mNsAware) { return null; } // // !!! TBI: // return null; String prefix = mNsContext.getPrefix(uri); return prefix != null ? prefix : mParentElem.getPrefix(uri); } public Object getProperty(String name) { // // !!! TBI // return null; return mConfig.getProperty(name); } public void setDefaultNamespace(String uri) { // !!! TBI mSuggestedDefNs = (uri == null || uri.length() == 0) ? null : uri; } public void setNamespaceContext(NamespaceContext context) { mNsContext = context; } public void setPrefix(String prefix, String uri) throws XMLStreamException { // !!! TBI 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"); } /* 25-Sep-2004, TSa: Let's check that "xml" and "xmlns" are not * (re-)defined to any other value, nor that value they * are bound to are bound to other prefixes. */ /* 01-Apr-2005, TSa: And let's not leave it optional: such * bindings should never succeed. */ // ... perhaps it really should be optional though? { if (prefix.equals(sPrefixXml)) { // prefix "xml" if (!uri.equals(XMLConstants.XML_NS_URI)) { throwOutputError(ErrorConsts.ERR_NS_REDECL_XML, uri); } } else if (prefix.equals(sPrefixXmlns)) { // prefix "xmlns" if (!uri.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) { throwOutputError(ErrorConsts.ERR_NS_REDECL_XMLNS, uri); } } else { // Neither of prefixes.. but how about URIs? if (uri.equals(XMLConstants.XML_NS_URI)) { throwOutputError(ErrorConsts.ERR_NS_REDECL_XML_URI, prefix); } else if (uri.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) { throwOutputError(ErrorConsts.ERR_NS_REDECL_XMLNS_URI, prefix); } } } if (mSuggestedPrefixes == null) { mSuggestedPrefixes = new HashMap(16); } mSuggestedPrefixes.put(uri, prefix); } public void writeAttribute(String localName, String value) throws XMLStreamException { outputAttribute(null, null, localName, value); } public void writeAttribute(String nsURI, String localName, String value) throws XMLStreamException { outputAttribute(nsURI, null, localName, value); } public void writeAttribute(String prefix, String nsURI, String localName, String value) throws XMLStreamException { outputAttribute(nsURI, prefix, localName, value); } public void writeCData(String data) { appendLeaf(mDocument.createCDATASection(data)); } public void writeCharacters(char[] text, int start, int len) { writeCharacters(new String(text, start, len)); } public void writeCharacters(String text) { appendLeaf(mDocument.createTextNode(text)); } public void writeComment(String data) { appendLeaf(mDocument.createCDATASection(data)); } public void writeDefaultNamespace(String nsURI) { // writeNamespace(null, nsURI); if (mOpenElement == null) { throw new IllegalStateException("No currently open START_ELEMENT, cannot write attribute"); } Element elem = mOpenElement.getElement(); elem.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns", nsURI); } public void writeDTD(String dtd) { // Would have to parse, not worth trying... reportUnsupported("writeDTD()"); } public void writeEmptyElement(String localName) throws XMLStreamException { writeEmptyElement(null, localName); } public void writeEmptyElement(String nsURI, String localName) throws XMLStreamException { /* 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); } public void writeEmptyElement(String prefix, String localName, String nsURI) throws XMLStreamException { createStartElem(nsURI, prefix, localName, true); } public void writeEndDocument() { mParentElem = mOpenElement = null; } public void writeEndElement() { // Simple, just need to traverse up... if we can if (mParentElem == null || mParentElem.isRoot()) { throw new IllegalStateException("No open start element to close"); } mOpenElement = null; // just in case it was open // Node parent = mParentElem.getParentNode(); // mParentElem = (parent == mDocument) ? null : (Element) parent; mParentElem = mParentElem.getParent(); } public void writeEntityRef(String name) { appendLeaf(mDocument.createEntityReference(name)); } public void writeNamespace(String prefix, String nsURI) throws XMLStreamException { if (!mNsAware) { throwOutputError("NsAware is false."); } else { // !!! TBI /* For now, let's output it (in non-repairing ns-aware * mode), but not keep track of bindings. */ outputAttribute(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, null, prefix, nsURI); mParentElem.addPrefix(prefix, nsURI); } } public void writeProcessingInstruction(String target) { writeProcessingInstruction(target, null); } public void writeProcessingInstruction(String target, String data) { appendLeaf(mDocument.createProcessingInstruction(target, data)); } public void writeSpace(char[] text, int start, int len) { writeSpace(new String(text, start, len)); } public void writeSpace(String text) { /* This won't work all that well, given there's no way to * prevent quoting/escaping. But let's do what we can, since * the alternative (throwing an exception) doesn't seem * especially tempting choice. */ writeCharacters(text); } public void writeStartDocument() { writeStartDocument(WstxOutputProperties.DEFAULT_OUTPUT_ENCODING, WstxOutputProperties.DEFAULT_XML_VERSION); } public void writeStartDocument(String version) { writeStartDocument(null, version); } public void writeStartDocument(String encoding, String version) { // Is there anything here we can or should do? No? mEncoding = encoding; } public void writeStartElement(String localName) throws XMLStreamException{ writeStartElement(null, localName); } public void writeStartElement(String nsURI, String localName) throws XMLStreamException { createStartElem(nsURI, null, localName, false); } public void writeStartElement(String prefix, String localName, String nsURI) throws XMLStreamException { createStartElem(nsURI, prefix, localName, false); } /* //////////////////////////////////////////////////// // XMLStreamWriter2 API (Stax2 v2.0) //////////////////////////////////////////////////// */ public boolean isPropertySupported(String name) { // !!! TBI: not all these properties are really supported return mConfig.isPropertySupported(name); } 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 mConfig.setProperty(name, value); } public XMLValidator validateAgainst(XMLValidationSchema schema) throws XMLStreamException { // !!! TBI return null; } public XMLValidator stopValidatingAgainst(XMLValidationSchema schema) throws XMLStreamException { // !!! TBI return null; } public XMLValidator stopValidatingAgainst(XMLValidator validator) throws XMLStreamException { // !!! TBI return null; } public ValidationProblemHandler setValidationProblemHandler(ValidationProblemHandler h) { // Not implemented by the basic reader return null; } public XMLStreamLocation2 getLocation() { return null; } public String getEncoding() { return mEncoding; } public void writeCData(char[] text, int start, int len) throws XMLStreamException { writeCData(new String(text, start, len)); } 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 (mParentElem != null) { throw new IllegalStateException("Operation only allowed to the document before adding root element"); } reportUnsupported("writeDTD()"); } public void writeFullEndElement() throws XMLStreamException { // No difference with DOM writeEndElement(); } public void writeStartDocument(String version, String encoding, boolean standAlone) throws XMLStreamException { writeStartDocument(encoding, version); } /* /////////////////////////////// // Stax2, pass-through methods /////////////////////////////// */ public void writeRaw(String text) throws XMLStreamException { reportUnsupported("writeRaw()"); } public void writeRaw(String text, int start, int offset) throws XMLStreamException { reportUnsupported("writeRaw()"); } public void writeRaw(char[] text, int offset, int length) throws XMLStreamException { reportUnsupported("writeRaw()"); } public void copyEventFromReader(XMLStreamReader2 r, boolean preserveEventData) throws XMLStreamException { // !!! TBI } /* /////////////////////////////// // Internal methods /////////////////////////////// */ protected void appendLeaf(Node n) throws IllegalStateException { // if (mParentElem == null) { // to add to document // mDocument.appendChild(n); // } else { // // Will also close the open element, if any // mOpenElement = null; // mParentElem.appendChild(n); // } if(mParentElem.isRoot()) { mParentElem.getDocument().appendChild(n); } else { mOpenElement = null; mParentElem.getElement().appendChild(n); } } protected void createStartElem(String nsURI, String prefix, String localName, boolean isEmpty) throws XMLStreamException { Element elem; if(!mNsAware) { if(nsURI == null || nsURI.length() == 0) { elem = mDocument.createElement(localName); mParentElem = mParentElem.createChild(elem); } else { throwOutputError("NsAware mode is off."); } } else { if(mNsRepairing) { prefix = findElemPrefix(nsURI, mParentElem); String actPrefix = validateElemPrefix(prefix, nsURI, mParentElem); //What shoud i do for validating? checkStartElement(localName, ""); if(actPrefix != null && actPrefix.length() != 0) {// fine, an existing binding we can use: elem = mDocument.createElementNS(nsURI, actPrefix+":"+localName); mParentElem = mParentElem.createChild(elem); } else { // nah, need to create a new binding... /* Need to ensure that we'll pass "" as prefix, not null, so * that it is understood as "I want to use the default NS", not * as "whatever prefix, I don't care" */ actPrefix = generateElemPrefix(actPrefix, nsURI, mParentElem); if(actPrefix.length() != 0) { localName = actPrefix + ":" + localName; } elem = mDocument.createElementNS(nsURI, localName); mParentElem = mParentElem.createChild(elem); } } else { String actPrefix = validateElemPrefix(prefix, nsURI, mParentElem); if(actPrefix == null) { throwOutputError("invalid prefix"); } if(actPrefix.length() != 0) { localName = actPrefix + ":" +localName; } elem = mDocument.createElementNS(nsURI, localName); mParentElem = mParentElem.createChild(elem); } } // appendLeaf(elem); // mOpenElement = mParentElem; if (!isEmpty) { // mParentElem = mOpenElement; mOpenElement = mParentElem; } } protected void outputAttribute(String nsURI, String prefix, String localName, String value) throws XMLStreamException { if (mOpenElement == null) { throw new IllegalStateException("No currently open START_ELEMENT, cannot write attribute"); } if (mNsAware) { if (mNsRepairing) { prefix = findOrCreateAttrPrefix(prefix, nsURI, mOpenElement); } if (prefix != null && prefix.length() > 0) { localName = prefix + ":" + localName; } mOpenElement.getElement().setAttributeNS(nsURI, localName, value); } else { // non-ns, simple if (prefix != null && prefix.length() > 0) { localName = prefix + ":" + localName; } mOpenElement.getElement().setAttribute(localName, value); } } private void reportUnsupported(String operName) { throw new UnsupportedOperationException(operName+" can not be used with DOM-backed writer"); } 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; } /* Hmmh... now here's bit of dilemma: that particular prefix is * either not bound, or is masked... but it is possible some other * prefix would be bound. Should we search for another one, or * try to re-define suggested one? Let's do latter, for now; * caller can then (try to) bind the preferred 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. * * @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 mParentElem.getPrefix(nsURI); } /** * 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 (mSuggestedDefNs != null && mSuggestedDefNs.equals(nsURI)) { suggPrefix = ""; } else { suggPrefix = (mSuggestedPrefixes == null) ? null: (String) mSuggestedPrefixes.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 (mAutoNsSeq == null) { mAutoNsSeq = new int[1]; mAutoNsSeq[0] = 1; } suggPrefix = elem.generateMapping(mAutomaticNsPrefix, nsURI, mAutoNsSeq); } } } // 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. * * @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 == SimpleOutputElement.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 == SimpleOutputElement.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 (mSuggestedPrefixes != null) { prefix = (String) mSuggestedPrefixes.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 (mAutoNsSeq == null) { mAutoNsSeq = new int[1]; mAutoNsSeq[0] = 1; } prefix = mParentElem.generateMapping(mAutomaticNsPrefix, nsURI, mAutoNsSeq); } // Ok; so far so good: let's now bind and output the namespace: elem.addPrefix(prefix, nsURI); writeNamespace(prefix, nsURI); return prefix; } /* //////////////////////////////////////////////////// // Package methods, basic output problem reporting //////////////////////////////////////////////////// */ protected static void throwOutputError(String msg) throws XMLStreamException { throw new XMLStreamException(msg); } protected static void throwOutputError(String format, Object arg) throws XMLStreamException { String msg = MessageFormat.format(format, new Object[] { arg }); throwOutputError(msg); } }