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.
*<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.
*<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 <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
*/
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<
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.
*
* @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;
}
}