com.fasterxml.aalto.dom.DOMWriterImpl Maven / Gradle / Ivy
Show all versions of aalto-xml Show documentation
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
* {@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:
*
* - Namespace-repairing mode not implemented
*
-
*
- Validation functionality not implemented
*
-
*
* @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.
*/
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 {@link #_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.
*/
protected DOMOutputElement _openElement;
/**
* 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
* {@link #setPrefix}, and they are only used as hints for binding;
* if there are conflicts, repairing writer can just use some other
* prefix.
*/
HashMap _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(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.
*
* @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.
*
* @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 {@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.
*
* @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;
}
}