package com.ctc.wstx.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.ctc.wstx.api.WriterConfig;
import com.ctc.wstx.cfg.ErrorConsts;
import com.ctc.wstx.sw.OutputElementBase;
/* TODO:
*
* - validator interface implementation
*/
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:
- Validation functionality not implemented
Author: Tatu Saloranta, Dan Diephouse
/**
* 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>Validation functionality not implemented
* </li>
* </ul>
*
* @author Tatu Saloranta
* @author Dan Diephouse
*/
public class WstxDOMWrappingWriter
extends DOMWrappingWriter
{
/*
///////////////////////////////////////////////////////////
// Constants
///////////////////////////////////////////////////////////
*/
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;
/*
///////////////////////////////////////////////////////////
// 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 mCurrElem;
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 mCurrElem
, 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 #mCurrElem},
* 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 mOpenElement;
for NsRepairing mode
/**
* 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 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> mSuggestedPrefixes = null;
/*
///////////////////////////////////////////////////////////
// Life-cycle
///////////////////////////////////////////////////////////
*/
private WstxDOMWrappingWriter(WriterConfig cfg, Node treeRoot)
throws XMLStreamException
{
super(treeRoot, cfg.willSupportNamespaces(), cfg.automaticNamespacesEnabled());
mConfig = cfg;
mAutoNsSeq = null;
mAutomaticNsPrefix = mNsRepairing ? mConfig.getAutomaticNsPrefix() : null;
/* 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
mCurrElem = DOMOutputElement.createRoot(treeRoot);
mOpenElement = null;
break;
case Node.ELEMENT_NODE: // can make sub-tree... ok
{
// still need a virtual root node as parent
DOMOutputElement root = DOMOutputElement.createRoot(treeRoot);
Element elem = (Element) treeRoot;
mOpenElement = mCurrElem = 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 WstxDOMWrappingWriter createFrom(WriterConfig cfg, DOMResult dst)
throws XMLStreamException
{
Node rootNode = dst.getNode();
return new WstxDOMWrappingWriter(cfg, rootNode);
}
/*
///////////////////////////////////////////////////////////
// XMLStreamWriter API (Stax 1.0)
///////////////////////////////////////////////////////////
*/
//public void close() { }
//public void flush() { }
@Override
public NamespaceContext getNamespaceContext()
{
if (!mNsAware) {
return EmptyNamespaceContext.getInstance();
}
return mCurrElem;
}
@Override
public String getPrefix(String uri)
{
if (!mNsAware) {
return null;
}
if (mNsContext != null) {
String prefix = mNsContext.getPrefix(uri);
if (prefix != null) {
return prefix;
}
}
return mCurrElem.getPrefix(uri);
}
@Override
public Object getProperty(String name) {
return mConfig.getProperty(name);
}
@Override
public void setDefaultNamespace(String uri) {
mSuggestedDefNs = (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(ErrorConsts.ERR_NS_REDECL_XML, uri);
}
} else if (prefix.equals("xmlns")) { // prefix "xmlns"
if (!uri.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) {
throwOutputError(ErrorConsts.ERR_NS_REDECL_XMLNS, uri);
}
// 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(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<String,String>(16);
}
mSuggestedPrefixes.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 (mOpenElement == null) {
throw new IllegalStateException("No currently open START_ELEMENT, cannot write attribute");
}
setDefaultNamespace(nsURI);
mOpenElement.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()
{
mCurrElem = mOpenElement = null;
}
@Override
public void writeEndElement()
{
// Simple, just need to traverse up... if we can
if (mCurrElem == null || mCurrElem.isRoot()) {
throw new IllegalStateException("No open start element to close");
}
mOpenElement = null; // just in case it was open
mCurrElem = mCurrElem.getParent();
}
//public void writeEntityRef(String name)
@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);
mCurrElem.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 v3.0):
// additional accessors
///////////////////////////////////////////////////////////
*/
//public XMLStreamLocation2 getLocation()
//public String getEncoding()
@Override
public boolean isPropertySupported(String name)
{
// !!! TBI: not all these properties are really supported
return mConfig.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 mConfig.setProperty(name, value);
}
/*
///////////////////////////////////////////////////////////
// XMLStreamWriter2 API (Stax2 v2.0):
// extended write methods
///////////////////////////////////////////////////////////
*/
//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 (mCurrElem != 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)
/*
///////////////////////////////////////////////////////////
// XMLStreamWriter2 API (Stax2 v2.0): validation
///////////////////////////////////////////////////////////
*/
//public XMLValidator validateAgainst(XMLValidationSchema schema)
//public XMLValidator stopValidatingAgainst(XMLValidationSchema schema)
//public XMLValidator stopValidatingAgainst(XMLValidator validator)
//public ValidationProblemHandler setValidationProblemHandler(ValidationProblemHandler h)
/*
///////////////////////////////////////////////////////////
// Impls of abstract methods from base class
///////////////////////////////////////////////////////////
*/
@Override
protected void appendLeaf(Node n)
throws IllegalStateException
{
mCurrElem.appendNode(n);
mOpenElement = 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 = mCurrElem.createAndAttachChild(mDocument.createElement(localName));
} else {
if (mNsRepairing) {
String actPrefix = validateElemPrefix(prefix, nsURI, mCurrElem);
if (actPrefix != null) { // fine, an existing binding we can use:
if (actPrefix.length() != 0) {
elem = mCurrElem.createAndAttachChild(mDocument.createElementNS(nsURI, actPrefix+":"+localName));
} else {
elem = mCurrElem.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, mCurrElem);
boolean hasPrefix = (actPrefix.length() != 0);
if (hasPrefix) {
localName = actPrefix + ":" + localName;
}
elem = mCurrElem.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)
*/
mOpenElement = 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 = (mSuggestedPrefixes == null) ? null : mSuggestedPrefixes.get(nsURI);
if (prefix == null) {
throwOutputError("Can not find prefix for namespace \""+nsURI+"\"");
}
}
if (prefix != null && prefix.length() != 0) {
localName = prefix + ":" +localName;
}
elem = mCurrElem.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
*/
mOpenElement = elem;
if (!isEmpty) {
mCurrElem = elem;
}
}
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.addAttribute(nsURI, localName, value);
} else { // non-ns, simple
if (prefix != null && prefix.length() > 0) {
localName = prefix + ":" + localName;
}
mOpenElement.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 mCurrElem.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 (mSuggestedDefNs != null && mSuggestedDefNs.equals(nsURI)) {
suggPrefix = "";
} else {
suggPrefix = (mSuggestedPrefixes == null) ? null: 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.
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 (mSuggestedPrefixes != null) {
prefix = 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 = mCurrElem.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;
}
}