/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package org.dom4j.io;

import java.util.List;

import org.dom4j.*;
import org.dom4j.dtd.Decl;
import org.dom4j.tree.NamespaceStack;

import org.w3c.dom.DOMImplementation;

/**
 * <code>DOMWriter</code> takes a DOM4J tree and outputs it as a W3C DOM
 * object
 *
 * @author <a href="mailto:james.strachan@metastuff.com">James Strachan </a>
 * @version $Revision: 1.17 $
 */
public class DOMWriter {
    private static boolean loggedWarning = false;

    private static final String[] DEFAULT_DOM_DOCUMENT_CLASSES = {
            "org.apache.xerces.dom.DocumentImpl", // Xerces
            "gnu.xml.dom.DomDocument", // GNU JAXP
            "org.apache.crimson.tree.XmlDocument", // Crimson
            "com.sun.xml.tree.XmlDocument", // Sun's Project X
            "oracle.xml.parser.v2.XMLDocument", // Oracle V2
            "oracle.xml.parser.XMLDocument", // Oracle V1
            "org.dom4j.dom.DOMDocument" // Internal DOM implementation
    };

    // the Class used to create new DOM Document instances
    private Class<?> domDocumentClass;

    /** stack of <code>Namespace</code> objects */
    private NamespaceStack namespaceStack = new NamespaceStack();

    public DOMWriter() {
    }

    public DOMWriter(Class<?> domDocumentClass) {
        this.domDocumentClass = domDocumentClass;
    }

    public Class<?> getDomDocumentClass() throws DocumentException {
        Class<?> result = domDocumentClass;

        if (result == null) {
            // lets try and find one in the classpath
            int size = DEFAULT_DOM_DOCUMENT_CLASSES.length;

            for (String name : DEFAULT_DOM_DOCUMENT_CLASSES) {
                try {
                    result = Class.forName(name, true, DOMWriter.class
                            .getClassLoader());

                    if (result != null) {
                        break;
                    }
                } catch (Exception e) {
                    // could not load class correctly
                    // lets carry on to the next one
                }
            }
        }

        return result;
    }

    /**
     * Sets the DOM {@link org.w3c.dom.Document}implementation class used by
     * the writer when creating DOM documents.
     * 
     * @param domDocumentClass
     *            is the Class implementing the {@link org.w3c.dom.Document}
     *            interface
     */
    public void setDomDocumentClass(Class<?> domDocumentClass) {
        this.domDocumentClass = domDocumentClass;
    }

    /**
     * Sets the DOM {@link org.w3c.dom.Document}implementation class name used
     * by the writer when creating DOM documents.
     * 
     * @param name
     *            is the name of the Class implementing the {@link
     *            org.w3c.dom.Document} interface
     * 
     * @throws DocumentException
     *             if the class could not be loaded
     */
    public void setDomDocumentClassName(String name) throws DocumentException {
        try {
            this.domDocumentClass = Class.forName(name, true, DOMWriter.class
                    .getClassLoader());
        } catch (Exception e) {
            throw new DocumentException("Could not load the DOM Document "
                    + "class: " + name, e);
        }
    }

    public org.w3c.dom.Document write(Document document)
            throws DocumentException {
        if (document instanceof org.w3c.dom.Document) {
            return (org.w3c.dom.Document) document;
        }

        resetNamespaceStack();

        org.w3c.dom.Document domDocument = createDomDocument(document);
        appendDOMTree(domDocument, domDocument, document.content());
        namespaceStack.clear();

        return domDocument;
    }

    public org.w3c.dom.Document write(Document document,
            org.w3c.dom.DOMImplementation domImpl) throws DocumentException {
        if (document instanceof org.w3c.dom.Document) {
            return (org.w3c.dom.Document) document;
        }

        resetNamespaceStack();

        org.w3c.dom.Document domDocument = createDomDocument(document, domImpl);
        appendDOMTree(domDocument, domDocument, document.content());
        namespaceStack.clear();

        return domDocument;
    }

    protected void appendDOMTree(org.w3c.dom.Document domDocument,
            org.w3c.dom.Node domCurrent, List<Node> content) {
        for (Node node : content) {
            if (node instanceof Element) {
                appendDOMTree(domDocument, domCurrent, (Element) node);
            } else if (node instanceof Text) {
                Text text = (Text) node;
                appendDOMTree(domDocument, domCurrent, text.getText());
            } else if (node instanceof CDATA) {
                appendDOMTree(domDocument, domCurrent, (CDATA) node);
            } else if (node instanceof Comment) {
                appendDOMTree(domDocument, domCurrent, (Comment) node);
            } else if (node instanceof Entity) {
                appendDOMTree(domDocument, domCurrent, (Entity) node);
            } else if (node instanceof ProcessingInstruction) {
                appendDOMTree(domDocument, domCurrent,
                        (ProcessingInstruction) node);
            }
        }
    }

    protected void appendDOMTree(org.w3c.dom.Document domDocument,
            org.w3c.dom.Node domCurrent, Element element) {
        String elUri = element.getNamespaceURI();
        String elName = element.getQualifiedName();
        org.w3c.dom.Element domElement = domDocument.createElementNS(elUri,
                elName);

        int stackSize = namespaceStack.size();

        // add the namespace of the element first
        Namespace elementNamespace = element.getNamespace();

        if (isNamespaceDeclaration(elementNamespace)) {
            namespaceStack.push(elementNamespace);
            writeNamespace(domElement, elementNamespace);
        }

        // add the additional declared namespaces
        List<Namespace> declaredNamespaces = element.declaredNamespaces();

        for (int i = 0, size = declaredNamespaces.size(); i < size; i++) {
            Namespace namespace = (Namespace) declaredNamespaces.get(i);

            if (isNamespaceDeclaration(namespace)) {
                namespaceStack.push(namespace);
                writeNamespace(domElement, namespace);
            }
        }

        // add the attributes
        for (Attribute attribute : element.attributes()) {
            String attUri = attribute.getNamespaceURI();
            String attName = attribute.getQualifiedName();
            String value = attribute.getValue();
            domElement.setAttributeNS(attUri, attName, value);
        }

        // add content
        appendDOMTree(domDocument, domElement, element.content());

        domCurrent.appendChild(domElement);

        while (namespaceStack.size() > stackSize) {
            namespaceStack.pop();
        }
    }

    protected void appendDOMTree(org.w3c.dom.Document domDocument,
            org.w3c.dom.Node domCurrent, CDATA cdata) {
        org.w3c.dom.CDATASection domCDATA = domDocument
                .createCDATASection(cdata.getText());
        domCurrent.appendChild(domCDATA);
    }

    protected void appendDOMTree(org.w3c.dom.Document domDocument,
            org.w3c.dom.Node domCurrent, Comment comment) {
        org.w3c.dom.Comment domComment = domDocument.createComment(comment
                .getText());
        domCurrent.appendChild(domComment);
    }

    protected void appendDOMTree(org.w3c.dom.Document domDocument,
            org.w3c.dom.Node domCurrent, String text) {
        org.w3c.dom.Text domText = domDocument.createTextNode(text);
        domCurrent.appendChild(domText);
    }

    protected void appendDOMTree(org.w3c.dom.Document domDocument,
            org.w3c.dom.Node domCurrent, Entity entity) {
        org.w3c.dom.EntityReference domEntity = domDocument
                .createEntityReference(entity.getName());
        domCurrent.appendChild(domEntity);
    }

    protected void appendDOMTree(org.w3c.dom.Document domDoc,
            org.w3c.dom.Node domCurrent, ProcessingInstruction pi) {
        org.w3c.dom.ProcessingInstruction domPI = domDoc
                .createProcessingInstruction(pi.getTarget(), pi.getText());
        domCurrent.appendChild(domPI);
    }

    protected void writeNamespace(org.w3c.dom.Element domElement,
            Namespace namespace) {
        String attributeName = attributeNameForNamespace(namespace);

        // domElement.setAttributeNS("", attributeName, namespace.getURI());
        domElement.setAttribute(attributeName, namespace.getURI());
    }

    protected String attributeNameForNamespace(Namespace namespace) {
        String xmlns = "xmlns";
        String prefix = namespace.getPrefix();

        if (prefix.length() > 0) {
            return xmlns + ":" + prefix;
        }

        return xmlns;
    }

    protected org.w3c.dom.Document createDomDocument(Document document)
            throws DocumentException {
        org.w3c.dom.Document result = null;

        // use the given domDocumentClass (if not null)
        if (domDocumentClass != null) {
            try {
                result = (org.w3c.dom.Document) domDocumentClass.newInstance();
            } catch (Exception e) {
                throw new DocumentException(
                        "Could not instantiate an instance "
                                + "of DOM Document with class: "
                                + domDocumentClass.getName(), e);
            }
        } else {
            // lets try JAXP first before using the hardcoded default parsers
            result = createDomDocumentViaJAXP();

            if (result == null) {
                Class<?> theClass = getDomDocumentClass();

                try {
                    result = (org.w3c.dom.Document) theClass.newInstance();
                } catch (Exception e) {
                    throw new DocumentException("Could not instantiate an "
                            + "instance of DOM Document " + "with class: "
                            + theClass.getName(), e);
                }
            }
        }

        return result;
    }

    protected org.w3c.dom.Document createDomDocumentViaJAXP()
            throws DocumentException {
        try {
            return JAXPHelper.createDocument(false, true);
        } catch (Throwable e) {
            if (!loggedWarning) {
                loggedWarning = true;

                if (SAXHelper.isVerboseErrorReporting()) {
                    // log all exceptions as warnings and carry
                    // on as we have a default SAX parser we can use
                    System.out.println("Warning: Caught exception attempting "
                            + "to use JAXP to create a W3C DOM " + "document");
                    System.out.println("Warning: Exception was: " + e);
                    e.printStackTrace();
                } else {
                    System.out.println("Warning: Error occurred using JAXP to "
                            + "create a DOM document.");
                }
            }
        }

        return null;
    }

    protected org.w3c.dom.Document createDomDocument(Document document,
            DOMImplementation domImpl) throws DocumentException {
        String namespaceURI = null;
        String qualifiedName = null;
        org.w3c.dom.DocumentType docType = null;

        return domImpl.createDocument(namespaceURI, qualifiedName, docType);
    }

    protected boolean isNamespaceDeclaration(Namespace ns) {
        if ((ns != null) && (ns != Namespace.NO_NAMESPACE)
                && (ns != Namespace.XML_NAMESPACE)) {
            String uri = ns.getURI();

            if ((uri != null) && (uri.length() > 0)) {
                if (!namespaceStack.contains(ns)) {
                    return true;
                }
            }
        }

        return false;
    }

    protected void resetNamespaceStack() {
        namespaceStack.clear();
        namespaceStack.push(Namespace.XML_NAMESPACE);
    }
}

/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * 1. Redistributions of source code must retain copyright statements and
 * notices. Redistributions must also contain a copy of this document.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The name "DOM4J" must not be used to endorse or promote products derived
 * from this Software without prior written permission of MetaStuff, Ltd. For
 * written permission, please contact dom4j-info@metastuff.com.
 * 
 * 4. Products derived from this Software may not be called "DOM4J" nor may
 * "DOM4J" appear in their names without prior written permission of MetaStuff,
 * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
 * 
 * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
 * 
 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 */
