/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cocoon.pipeline.util.dom;
import java.util.Map.Entry;
import org.apache.cocoon.pipeline.component.sax.SAXConsumer;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
final class DOMStreamer {
private ElementInfo currentElementInfo;
/** Counter used when generating new namespace prefixes. */
private int newPrefixCounter;
private SAXConsumer xmlConsumer;
public DOMStreamer(SAXConsumer xmlConsumer) {
this.xmlConsumer = xmlConsumer;
}
/**
* Start the production of SAX events.
*/
public void stream(Node node) throws SAXException {
// Start document only if we're streaming a document
boolean isDoc = node.getNodeType() == Node.DOCUMENT_NODE;
if (isDoc) {
xmlConsumer.startDocument();
}
Node top = node;
while (null != node) {
startNode(node);
Node nextNode = node.getFirstChild();
while (null == nextNode) {
endNode(node);
if (top.equals(node)) {
break;
}
nextNode = node.getNextSibling();
if (null == nextNode) {
node = node.getParentNode();
if ((null == node) || (top.equals(node))) {
if (null != node) {
endNode(node);
}
nextNode = null;
break;
}
}
}
node = nextNode;
}
if (isDoc) {
xmlConsumer.endDocument();
}
}
private void startNode(Node node) throws SAXException {
switch (node.getNodeType()) {
case Node.COMMENT_NODE:
// ignore comment
break;
case Node.DOCUMENT_FRAGMENT_NODE:
// ??;
break;
case Node.DOCUMENT_NODE:
break;
case Node.ELEMENT_NODE:
NamedNodeMap atts = node.getAttributes();
int nAttrs = atts.getLength();
// create a list of localy declared namespace prefixes
currentElementInfo = new ElementInfo(currentElementInfo);
for (int i = 0; i < nAttrs; i++) {
Node attr = atts.item(i);
String attrName = attr.getNodeName();
if (attrName.equals("xmlns") || attrName.startsWith("xmlns:")) {
int index = attrName.indexOf(":");
String prefix = index < 0 ? "" : attrName.substring(index + 1);
currentElementInfo.put(prefix, attr.getNodeValue());
}
}
String namespaceURI = node.getNamespaceURI();
String prefix = node.getPrefix();
String localName = node.getLocalName();
if (localName == null) {
// this is an element created with createElement instead of createElementNS
String[] prefixAndLocalName = getPrefixAndLocalName(node.getNodeName());
prefix = prefixAndLocalName[0];
localName = prefixAndLocalName[1];
// note: if prefix is null, there can still be a default namespace...
namespaceURI = getNamespaceForPrefix(prefix, (Element) node);
}
if (namespaceURI != null) {
// no prefix means: make this the default namespace
if (prefix == null) {
prefix = "";
}
// check that is declared
String uri = currentElementInfo.findNamespaceURI(prefix);
if (uri != null && uri.equals(namespaceURI)) {
// System.out.println("namespace is declared");
// prefix is declared correctly, do nothing
} else if (uri != null) {
// System.out.println("prefix is declared with other namespace, overwriting it");
// prefix exists but is bound to another namespace, overwrite it
currentElementInfo.put(prefix, namespaceURI);
} else {
// System.out.println("prefix is not yet declared, declaring it now");
currentElementInfo.put(prefix, namespaceURI);
}
} else {
// element has no namespace
// check if there is a default namespace, if so undeclare it
String uri = currentElementInfo.findNamespaceURI("");
if (uri != null && uri.length() > 0) {
// System.out.println("undeclaring default namespace");
currentElementInfo.put("", "");
}
}
// SAX uses empty string to denote no namespace, while DOM uses null.
if (namespaceURI == null) {
namespaceURI = "";
}
String qName;
if (prefix != null && prefix.length() > 0) {
qName = prefix + ":" + localName;
} else {
qName = localName;
}
// make the attributes
AttributesImpl newAttrs = new AttributesImpl();
for (int i = 0; i < nAttrs; i++) {
Node attr = atts.item(i);
String attrName = attr.getNodeName();
String assignedAttrPrefix = null;
// only do non-namespace attributes
if (!(attrName.equals("xmlns") || attrName.startsWith("xmlns:"))) {
String attrPrefix;
String attrLocalName;
String attrNsURI;
if (attr.getLocalName() == null) {
// this is an attribute created with setAttribute instead of setAttributeNS
String[] prefixAndLocalName = getPrefixAndLocalName(attrName);
attrPrefix = prefixAndLocalName[0];
// the statement below causes the attribute to keep its prefix even if it is not
// bound to a namespace (to support pre-namespace XML).
assignedAttrPrefix = attrPrefix;
attrLocalName = prefixAndLocalName[1];
// note: if prefix is null, the attribute has no namespace (namespace defaulting
// does not apply to attributes)
if (attrPrefix != null) {
attrNsURI = getNamespaceForPrefix(attrPrefix, (Element) node);
} else {
attrNsURI = null;
}
} else {
attrLocalName = attr.getLocalName();
attrPrefix = attr.getPrefix();
attrNsURI = attr.getNamespaceURI();
}
if (attrNsURI != null) {
String declaredUri = currentElementInfo.findNamespaceURI(attrPrefix);
// if the prefix is null, or the prefix has not been declared, or conflicts
// with an in-scope binding
if (declaredUri == null || !declaredUri.equals(attrNsURI)) {
String availablePrefix = currentElementInfo.findPrefix(attrNsURI);
if (availablePrefix != null && !availablePrefix.equals("")) {
assignedAttrPrefix = availablePrefix;
} else {
if (attrPrefix != null && declaredUri == null) {
// prefix is not null and is not yet declared: declare it
assignedAttrPrefix = attrPrefix;
currentElementInfo.put(assignedAttrPrefix, attrNsURI);
} else {
// attribute has no prefix (which is not allowed for namespaced attributes) or
// the prefix is already bound to something else: generate a new prefix
newPrefixCounter++;
assignedAttrPrefix = "NS" + newPrefixCounter;
currentElementInfo.put(assignedAttrPrefix, attrNsURI);
}
}
} else {
assignedAttrPrefix = attrPrefix;
}
}
String assignedAttrNsURI = attrNsURI != null ? attrNsURI : "";
String attrQName;
if (assignedAttrPrefix != null) {
attrQName = assignedAttrPrefix + ":" + attrLocalName;
} else {
attrQName = attrLocalName;
}
newAttrs.addAttribute(assignedAttrNsURI, attrLocalName, attrQName, "CDATA", attr.getNodeValue());
}
}
// add local namespace declaration and fire startPrefixMapping events
if (currentElementInfo.namespaceDeclarations != null && currentElementInfo.namespaceDeclarations.size() > 0) {
for (Entry<String, String> entry : currentElementInfo.namespaceDeclarations.entrySet()) {
String pr = (String) entry.getKey();
String ns = (String) entry.getValue();
// the following lines enable the creation of explicit xmlns attributes
// String pr1 = pr.equals("") ? "xmlns" : pr;
// String qn = pr.equals("") ? "xmlns" : "xmlns:" + pr;
// newAttrs.addAttribute("", pr1, qn, "CDATA", ns);
// System.out.println("starting prefix mapping for prefix " + pr + " for " + ns);
xmlConsumer.startPrefixMapping(pr, ns);
}
}
xmlConsumer.startElement(namespaceURI, localName, qName, newAttrs);
currentElementInfo.localName = localName;
currentElementInfo.namespaceURI = namespaceURI;
currentElementInfo.qName = qName;
break;
case Node.PROCESSING_INSTRUCTION_NODE:
ProcessingInstruction pi = (ProcessingInstruction) node;
xmlConsumer.processingInstruction(pi.getNodeName(), pi.getData());
break;
case Node.CDATA_SECTION_NODE:
if (xmlConsumer != null) {
xmlConsumer.startCDATA();
}
dispatchChars(node);
if (xmlConsumer != null) {
xmlConsumer.endCDATA();
}
break;
case Node.TEXT_NODE:
dispatchChars(node);
break;
case Node.ENTITY_REFERENCE_NODE:
EntityReference eref = (EntityReference) node;
if (xmlConsumer != null) {
xmlConsumer.startEntity(eref.getNodeName());
} else {
// warning("Can not output entity to a pure SAX xmlConsumer");
}
break;
default:
}
}
private void endNode(Node node) throws SAXException {
switch (node.getNodeType()) {
case Node.DOCUMENT_NODE:
break;
case Node.ELEMENT_NODE:
xmlConsumer.endElement(currentElementInfo.namespaceURI, currentElementInfo.localName, currentElementInfo.qName);
// generate endPrefixMapping events if needed
if (currentElementInfo.namespaceDeclarations != null && currentElementInfo.namespaceDeclarations.size() > 0) {
for (Entry<String, String> entry : currentElementInfo.namespaceDeclarations.entrySet()) {
xmlConsumer.endPrefixMapping((String) entry.getKey());
}
}
currentElementInfo = currentElementInfo.parent;
break;
case Node.CDATA_SECTION_NODE:
break;
case Node.ENTITY_REFERENCE_NODE:
EntityReference eref = (EntityReference) node;
if (xmlConsumer != null) {
xmlConsumer.endEntity(eref.getNodeName());
}
break;
default:
}
}
private void dispatchChars(Node node) throws SAXException {
final String data = ((Text) node).getData();
if (data != null) {
xmlConsumer.characters(data.toCharArray(), 0, data.length());
}
}
/**
* Searches the namespace for a given namespace prefix starting from a given Element.
*
* <p>
* Note that this resolves the prefix in the orginal DOM-tree, not in the {@link ElementInfo} objects. This is used to
* resolve prefixes of elements or attributes created with createElement or setAttribute instead of createElementNS or
* setAttributeNS.
*
* <p>
* The code in this method is largely based on org.apache.xml.utils.DOMHelper.getNamespaceForPrefix() (from Xalan).
*
* @param prefix the prefix to look for, can be empty or null to find the default namespace
*
* @return the namespace, or null if not found.
*/
private String getNamespaceForPrefix(String prefix, Element namespaceContext) {
if (prefix == null) {
prefix = "";
}
String namespace = null;
if (prefix.equals("xml")) {
namespace = "http://www.w3.org/XML/1998/namespace";
} else if (prefix.equals("xmlns")) {
namespace = "http://www.w3.org/2000/xmlns/";
} else {
// Attribute name for this prefix's declaration
String declname = (prefix.length() == 0) ? "xmlns" : "xmlns:" + prefix;
// Scan until we run out of Elements or have resolved the namespace
Node parent = namespaceContext;
int type = -1;
if (parent != null) {
type = parent.getNodeType();
}
while (parent != null && namespace == null && (type == Node.ELEMENT_NODE || type == Node.ENTITY_REFERENCE_NODE)) {
if (type == Node.ELEMENT_NODE) {
Attr attr = ((Element) parent).getAttributeNode(declname);
if (attr != null) {
namespace = attr.getNodeValue();
break;
}
}
parent = parent.getParentNode();
if (parent != null) {
type = parent.getNodeType();
}
}
}
return namespace;
}
/**
* Splits a nodeName into a prefix and a localName
*
* @return an array containing two elements, the first one is the prefix (can be null), the second one is the localName
*/
private String[] getPrefixAndLocalName(String nodeName) {
String prefix;
String localName;
int colonPos = nodeName.indexOf(":");
if (colonPos != -1) {
prefix = nodeName.substring(0, colonPos);
localName = nodeName.substring(colonPos + 1, nodeName.length());
} else {
prefix = null;
localName = nodeName;
}
return new String[] { prefix, localName };
}
}