package org.compass.core.xml.javax.converter.support; import javax.xml.XMLConstants; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.DocumentType; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Outputs a DOM document using a {@link javax.xml.stream.XMLStreamWriter} * provided. */ public class Dom2StaxSerializer { protected final XMLStreamWriter mWriter; protected final boolean mRepairing; public Dom2StaxSerializer(XMLStreamWriter sw) { mWriter = sw; Object o = sw.getProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES); mRepairing = (o instanceof Boolean) && ((Boolean) o).booleanValue(); } // // // Public output methods public void output(Document doc) throws XMLStreamException { mWriter.writeStartDocument(); NsStack nsStack = NsStack.defaultInstance(); for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { doOutputNode(child, nsStack); } mWriter.writeEndDocument(); doClose(); } public void outputFragment(NodeList nodes) throws XMLStreamException { NsStack nss = NsStack.defaultInstance(); for (int i = 0, len = nodes.getLength(); i < len; ++i) { doOutputNode((Node) nodes.item(i), nss); } } public void outputFragment(Node node) throws XMLStreamException { doOutputNode(node, NsStack.defaultInstance()); } // // // Internal output methods /** * @param elem Element to output */ protected void doOutputElement(Element elem, NsStack nsStack) throws XMLStreamException { boolean sharedNsStack = true; // flag to indicate if we need a copy String elemPrefix = elem.getPrefix(); if (elemPrefix == null) { elemPrefix = ""; } String elemUri = elem.getNamespaceURI(); if (elemUri == null) { elemUri = ""; } mWriter.writeStartElement(elemPrefix, elem.getLocalName(), elemUri); // Hmmh. In non-repairing mode, we need to output namespaces... if (!mRepairing) { // First, is the namespace element itself uses bound? if (!nsStack.hasBinding(elemPrefix, elemUri)) { nsStack = nsStack.childInstance(); sharedNsStack = false; nsStack.addBinding(elemPrefix, elemUri); if (elemPrefix.length() == 0) { //def ns mWriter.setDefaultNamespace(elemUri); mWriter.writeDefaultNamespace(elemUri); } else { mWriter.setPrefix(elemPrefix, elemUri); mWriter.writeNamespace(elemPrefix, elemUri); } } } // And in any case, may have attributes: NamedNodeMap attrs = elem.getAttributes(); for (int i = 0, len = attrs.getLength(); i < len; ++i) { Attr attr = (Attr) attrs.item(i); String aPrefix = attr.getPrefix(); String ln = attr.getLocalName(); String value = attr.getValue(); /* With attributes things are bit simpler: they will never use * the default namespace, so if prefix is empty, they will bound * to the empty namespace. */ if (aPrefix == null || aPrefix.length() == 0) { // no NS mWriter.writeAttribute(ln, value); } else { String aNsUri = attr.getNamespaceURI(); // Attribute NS declared? if (!mRepairing && !nsStack.hasBinding(aPrefix, aNsUri)) { if (sharedNsStack) { nsStack = nsStack.childInstance(); sharedNsStack = false; } nsStack.addBinding(aPrefix, aNsUri); // Binding prefix is optional, but let's do it nonetheless mWriter.setPrefix(aPrefix, aNsUri); mWriter.writeNamespace(aPrefix, aNsUri); } mWriter.writeAttribute(aPrefix, aNsUri, attr.getLocalName(), attr.getValue()); } } // And then children, recursively: for (Node child = elem.getFirstChild(); child != null; child = child.getNextSibling()) { doOutputNode(child, nsStack); } mWriter.writeEndElement(); } protected void doOutputNode(Node node, NsStack nsStack) throws XMLStreamException { switch (node.getNodeType()) { case Node.ELEMENT_NODE: doOutputElement((Element) node, nsStack); break; case Node.TEXT_NODE: // Do we care about whether it's actually CDATA? mWriter.writeCharacters(node.getNodeValue()); break; case Node.CDATA_SECTION_NODE: mWriter.writeCData(node.getNodeValue()); break; case Node.COMMENT_NODE: mWriter.writeComment(node.getNodeValue()); break; case Node.ENTITY_REFERENCE_NODE: mWriter.writeEntityRef(node.getNodeName()); break; case Node.PROCESSING_INSTRUCTION_NODE: String target = node.getNodeName(); String data = node.getNodeValue(); if (data == null || data.length() == 0) { mWriter.writeProcessingInstruction(target); } else { mWriter.writeProcessingInstruction(target, data); } break; case Node.DOCUMENT_TYPE_NODE: mWriter.writeDTD(buildDTD((DocumentType) node)); break; default: throw new XMLStreamException("Unrecognized or unexpected node class: " + node.getClass().getName()); } } protected void doClose() throws XMLStreamException { mWriter.close(); } protected String buildDTD(DocumentType doctype) { /* For StAX 1.0, need to construct it: for StAX2 we could * pass these as they are... */ StringBuffer sb = new StringBuffer(); sb.append("<!DOCTYPE "); // root elem should never be null sb.append(doctype.getName()); String pubId = doctype.getPublicId(); String sysId = doctype.getSystemId(); if (pubId == null || pubId.length() == 0) { // no public id? if (sysId != null && sysId.length() > 0) { // but have sys id sb.append("SYSTEM \""); sb.append(sysId); sb.append('"'); } } else { sb.append("PUBLIC \""); sb.append(pubId); sb.append("\" \""); // System id can not be null, if so sb.append(sysId); sb.append('"'); } String intSubset = doctype.getInternalSubset(); if (intSubset != null && intSubset.length() > 0) { sb.append(" ["); sb.append(intSubset); sb.append(']'); } sb.append('>'); return sb.toString(); } /** * Internal helper class, used for keeping track of bound namespaces. * It is only needed since JDom has nasty habit of not keeping good track * of changes to the namespace binding of the element itself -- all other * declarations are properly stored as "additional" namespaces, and can * be easily bound on output... but not this primary namespace. Yuck. */ private final static class NsStack { final static NsStack sEmptyStack; static { String[] predefd = new String[]{ "xml", XMLConstants.XML_NS_URI, "xmlns", XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "", "", }; sEmptyStack = new NsStack(predefd, predefd.length); } String[] mNsData; int mEnd = 0; private NsStack(String[] data, int end) { mNsData = data; mEnd = end; } public static NsStack defaultInstance() { return sEmptyStack; } public NsStack childInstance() { // Can not share array of the root instance if (this == sEmptyStack) { String[] data = new String[16]; System.arraycopy(mNsData, 0, data, 0, mEnd); return new NsStack(data, mEnd); } return new NsStack(mNsData, mEnd); } public boolean hasBinding(String prefix, String uri) { int i = mEnd - 2; for (; i >= 0; i -= 2) { if (mNsData[i].equals(prefix)) { // This is the most recent binding... return mNsData[i + 1].equals(uri); } } return false; } public void addBinding(String prefix, String uri) { if (prefix == null) { prefix = ""; } if (mEnd >= mNsData.length) { String[] old = mNsData; mNsData = new String[old.length * 2]; System.arraycopy(old, 0, mNsData, 0, old.length); } mNsData[mEnd] = prefix; mNsData[mEnd + 1] = uri; mEnd += 2; } } /* Simple test driver to see how round-trip parsing and outputting works * for StAXOutputter. */ public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Usage: java ... [file]"); System.exit(1); } String filename = args[0]; java.io.Reader r = new java.io.FileReader(filename); javax.xml.stream.XMLInputFactory f = javax.xml.stream.XMLInputFactory.newInstance(); XMLStreamReader sr = f.createXMLStreamReader(r); Stax2DomBuilder builder = new Stax2DomBuilder(); Document domDoc = builder.build(sr); java.io.PrintWriter pw = new java.io.PrintWriter(System.out); javax.xml.stream.XMLOutputFactory of = javax.xml.stream.XMLOutputFactory.newInstance(); // Repairing? //of.setProperty(XMLOutputFactory.OUTPUT_REPAIRING_NAMESPACES, Boolean.TRUE); XMLStreamWriter sw = of.createXMLStreamWriter(pw); Dom2StaxSerializer outputter = new Dom2StaxSerializer(sw); outputter.output(domDoc); sw.flush(); sw.close(); } }