/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.enterprise.jbi.serviceengine.util;

import java.util.HashMap;
import javax.xml.stream.XMLStreamWriter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import java.io.Writer;

import java.util.ArrayList;
import java.util.Map;
import javax.xml.stream.XMLStreamReader;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Attr;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.ProcessingInstruction;


/**
 * This object provides utility methods to manipulate DOM tree.
 *
 * @author bhavanishankar@dev.java.net.
 */
public class DOMUtil {
    /**
     * static object to access the methods of this object.
     */
    public static final DOMUtil UTIL = new DOMUtil();
    
    /**
     * Creates a new instance of DOMUtil.
     */
    public DOMUtil() {
    }
    
    /**
     * Writes namespace attributes and other attributes from the current node
     * of the reader to the current node of the writer.
     *
     * @param reader XMLStreamReader pointing to a START_ELEMENT.
     * @param writer XMLStreamWriter pointing to a START_ELEMENT.
     */
    void writeAttributes(XMLStreamReader reader, XMLStreamWriter writer)
    throws Exception {
        int namespaceCount = reader.getNamespaceCount();
        int attributeCount = reader.getAttributeCount();
        for(int i=0; i<namespaceCount; i++) {
            writer.writeNamespace(reader.getNamespacePrefix(i),
                    reader.getNamespaceURI(i));
        }
        for(int i=0; i<attributeCount; i++) {
            String attrNs = reader.getAttributeNamespace(i);
            if(attrNs != null) {
                writer.writeAttribute(attrNs,
                        reader.getAttributeLocalName(i),
                        reader.getAttributeValue(i));
                
            } else {
                writer.writeAttribute(reader.getAttributeLocalName(i),
                        reader.getAttributeValue(i));
            }
        }
    }
    
    private void writeAncestorNamespaceAttrs(
            XMLStreamReader reader,
            XMLStreamWriter writer,
            Map<String,String> ancestorNamespaceAttrs) {
        try {
            String prefix = reader.getPrefix();
            String namespace = ancestorNamespaceAttrs.remove(prefix);
            if(namespace != null) {
                writer.writeNamespace(prefix, namespace);
            }
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
    
    private String fixNull(String s) {
        return s == null ? "" : s;
    }
    
    /**
     * Inherit the namespace attributes from e's parents and set it
     * in e, if the prefix is not already resolved.
     */
    private void resolvePrefix(
            Element e,
            Map<String,String> resolvedPrefixes) {
        String nodePrefix = fixNull(e.getPrefix());
        String nodeNamespace = e.getNamespaceURI();
        if(nodeNamespace != null) {
            /**
             * Set the namespace attribute if it is not already set
             */
            if(!resolvedPrefixes.containsKey(nodePrefix)) {
                String attrName = (nodePrefix.trim().length() == 0)
                ? "xmlns" : "xmlns:" + nodePrefix;
                e.setAttribute(attrName, nodeNamespace);
                resolvedPrefixes.put(nodePrefix, nodeNamespace);
            }
        }
    }
    
    
    /**
     * writes the node pointed by XMLStreamReader to the XMLStreamWriter
     * Strictly speaking, this method should not be in this class.
     *
     * @param reader XMLStreamReader from where the contents should be read.
     * @param writer XMLStreamWriter to where the contents should be written.
     */
    // TODO : handle attributes.
    public void writeNode(XMLStreamReader reader, XMLStreamWriter writer) throws Exception {
        writeNode(reader, writer, new HashMap<String,String>());
    }
    
    public void writeChildren(XMLStreamReader reader, XMLStreamWriter writer) throws Exception {
        writeChildren(reader, writer, new HashMap<String,String>());
    }
    
    public void writeNode(
            XMLStreamReader reader,
            XMLStreamWriter writer,
            Map<String,String> ancestorNamespaceAttrs) throws Exception {
        int depth = 0;
        int event = reader.getEventType();
        do {
            switch(event) {
                case XMLStreamReader.START_DOCUMENT:
                    reader.next();
                    writeNode(reader, writer);
                    break;
                case XMLStreamReader.START_ELEMENT:
                    String tagName =
                            (reader.getPrefix() != null && reader.getPrefix().length() != 0)
                            ? reader.getPrefix() + ":" + reader.getLocalName()
                            : reader.getLocalName();
                    writer.writeStartElement(tagName);
                    writeAttributes(reader, writer);
                    writeAncestorNamespaceAttrs(reader, writer, ancestorNamespaceAttrs);
                    ++depth;
                    break;
                case XMLStreamReader.CHARACTERS:
                    writer.writeCharacters(reader.getText());
                    break;
                case XMLStreamReader.END_ELEMENT:
                    writer.writeEndElement();
                    --depth;
            }
            event = reader.next();
        } while(depth > 0);
    }
    
    public void writeChildren(
            XMLStreamReader reader,
            XMLStreamWriter writer,
            Map<String,String> ancestorNamespaceAttrs) throws Exception {
        reader.next();
        while(reader.getEventType() != XMLStreamReader.END_ELEMENT) {
            writeNode(reader, writer, ancestorNamespaceAttrs);
        }
        reader.next();
    }
    
    /**
     * Writes the node to the XMLStreamWriter as text.
     *
     * @param node Node from where the contents should be read.
     * @param writer XMLStreamWriter to where the contents should be written.
     */
    public void writeNode(Node node, XMLStreamWriter writer) throws Exception {
        writeNode(node, writer, new HashMap<String,String>());
    }
    
    public void writeChildren(XMLStreamWriter writer, Node node) throws Exception {
        NodeList l = node.getChildNodes();
        int size = l.getLength();
        for ( int i = 0; i < size; i++ ) {
            writeNode(l.item(i), writer, new HashMap<String,String>());
        }
    }
    
    /**
     * namespaceMap keeps track of the namespace attributes set
     * in the the ancestor node.
     */
    public void writeNode(Node node, XMLStreamWriter writer, Map<String,String> resolvedPrefixes) throws Exception {
        
        short type = node.getNodeType();
        switch ( type ) {
            
            case Node.DOCUMENT_NODE: {
                writer.writeStartDocument();
                writeChildren(writer, node, resolvedPrefixes);
                break;
            }
            
            case Node.DOCUMENT_FRAGMENT_NODE: {
                writeChildren(writer, node, resolvedPrefixes);
                break;
            }
            
            case Node.ELEMENT_NODE: {
                Element e = (Element)node;
                String n = e.getTagName();
                
                resolvePrefix(e, resolvedPrefixes);
                
                writer.writeStartElement(n);
                
                NamedNodeMap a = e.getAttributes();
                int size = a.getLength();
                for ( int i = 0; i < size; i++ ) {
                    Attr att = (Attr)a.item(i);
                    writeNode(att, writer, resolvedPrefixes);
                }
                
                if ( e.hasChildNodes() ) {
                    writeChildren(writer, node, resolvedPrefixes);
                    writer.writeEndElement();
                } else
                    writer.writeEndElement();
                break;
            }
            
            case Node.ATTRIBUTE_NODE:
                Attr a = (Attr)node;
                writer.writeAttribute(a.getName(), a.getValue());
                break;
                
            case Node.PROCESSING_INSTRUCTION_NODE: {
                ProcessingInstruction pi = (ProcessingInstruction)node;
                writer.writeProcessingInstruction(pi.getTarget(), pi.getData());
                break;
            }
            
            case Node.TEXT_NODE: {
                writer.writeCharacters(node.getNodeValue());
                break;
            }
            
            case Node.CDATA_SECTION_NODE: {
                writer.writeCData(node.getNodeValue());
                break;
            }
            
            case Node.COMMENT_NODE: {
                writer.writeComment(node.getNodeValue());
                break;
            }
        }
    }
    
    /**
     * This method should be used only from
     * writeNode(node,writer,resolvedPrefixes) method.
     */
    private void writeChildren(XMLStreamWriter writer,
            Node node,
            Map<String,String> resolvedPrefixes) throws Exception {
        NodeList l = node.getChildNodes();
        int size = l.getLength();
        for ( int i = 0; i < size; i++ ) {
            writeNode(l.item(i), writer, resolvedPrefixes);
        }
    }
    
    
    /**
     * gets the element.
     *
     * @param aParentDocument Document for parent node
     * @param aTagName        String for tagname
     * @return Element with tagname
     */
    public Element getElement(
            Document aParentDocument,
            String aTagName) {
        NodeList nodeList = aParentDocument.getElementsByTagName(aTagName);
        
        return (nodeList != null) ? (Element) nodeList.item(0) : null;
    }
    
    /**
     * gets the element.
     *
     * @param elem    Element for parent node
     * @param tagName String for tagname
     * @return Element with tagname
     */
    public Element getElement(
            Element elem,
            String tagName
            ) {
        NodeList nl = elem.getElementsByTagName(tagName);
        Element childElem = (Element) nl.item(0);
        
        return childElem;
    }
    
    /**
     * gets the element value.
     *
     * @param doc      Document for parent node
     * @param elemName String for element name
     * @return Element value
     */
    public String getElementValue(
            Document doc,
            String elemName
            ) {
        String elemVal = null;
        
        Element elem = getElement(doc, elemName);
        elemVal = getTextData(elem);
        
        return elemVal;
    }
    
    /**
     * use this util method to create/retrieve a Text Node in a element.
     *
     * @param aElement Element node
     * @return Text node for text data
     */
    private Text getText(Element aElement) {
        Node node = null;
        aElement.normalize();
        node = aElement.getFirstChild();
        
        if ((node == null) || !(node instanceof Text)) {
            node = aElement.getOwnerDocument().createTextNode("");
            aElement.appendChild(node);
        }
        
        return (Text) node;
    }
    
    /**
     * use this util method to set a Text Data in a element.
     *
     * @param aElement Element for text node
     * @param aData    String contains text
     */
    public void setTextData(
            Element aElement,
            String aData
            ) {
        getText(aElement).setData(aData);
    }
    
    /**
     * use this util method to retrieve a Text Data in a element.
     *
     * @param aElement Element for text node
     * @return String contains text
     */
    public String getTextData(Element aElement) {
        return getText(aElement).getData();
    }
    
    /**
     * save document to stream.
     *
     * @param aDocument Document
     * @param aWriter   is what the aDocument is serialized to.
     * @return String representation of Document
     * @throws Exception If fails to construct a string.
     */
    public String DOM2String(Document aDocument,
            Writer aWriter) throws Exception {
        TransformerFactory transformerFactory =
                TransformerFactory.newInstance();
        Transformer transformer;
        
        try {
            transformer = transformerFactory.newTransformer();
        } catch (javax.xml.transform.TransformerConfigurationException e) {
            e.printStackTrace();
            transformer = null;
            throw e;
        }
        
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
        transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "");
        transformer.setOutputProperty(OutputKeys.INDENT, "no");
        
        try {
            transformer.transform(new DOMSource(aDocument),
                    new StreamResult(aWriter));
        } catch (javax.xml.transform.TransformerException e) {
            e.printStackTrace();
            throw e;
        }
        
        return aWriter.toString();
    }
    
    /**
     * gets list of elements.
     *
     * @param aParentElement Element for parent
     * @param aTagName       String for tagname
     * @return NodeList with tagname
     */
    public NodeList getElements(
            Element aParentElement,
            String aTagName
            ) {
        return aParentElement.getElementsByTagNameNS(
                aParentElement.getNamespaceURI(),
                aTagName
                );
    }
    
    /**
     * gets set of elements.
     *
     * @param aParentDocument Document for parent node
     * @param aTagName        String for tagname
     * @return NodeList with tagname
     */
    public NodeList getElements(
            Document aParentDocument,
            String aTagName
            ) {
        return aParentDocument.getElementsByTagNameNS("*", aTagName);
    }
    
    /**
     * get the children of the same type element tag name.
     *
     * @param aElement        Element for parent node
     * @param aElementTagName String for tagname
     * @return NodeList for list of children with the tagname
     */
    public NodeList getChildElements(
            Element aElement,
            String aElementTagName
            ) {
        NodeList nodeList = aElement.getChildNodes();
        NodeListImpl list = new NodeListImpl();
        int count = nodeList.getLength();
        
        for (int i = 0; i < count; ++i) {
            Node node = nodeList.item(i);
            
            if (node instanceof Element) {
                String tagName = getElementName((Element) node);
                
                if (tagName.equals(aElementTagName)) {
                    list.add(node);
                }
            }
        }
        
        return list;
    }
    
    /**
     * get Element Tag Name with striped prefix.
     *
     * @param aElement Element object
     * @return String with stripped prefix
     */
    public String getElementName(Element aElement) {
        String tagName = aElement.getTagName();
        
        return getName(tagName);
    }
    
    /**
     * strips the prefix of the name if present.
     *
     * @param aName String value of Name with prefix
     * @return String for name after striping prefix
     */
    public String getName(String aName) {
        int lastIdx = aName.lastIndexOf(':');
        
        if (lastIdx >= 0) {
            return aName.substring(lastIdx + 1);
        }
        
        return aName;
    }
    
    /**
     * NodeListImpl.
     */
    public static class NodeListImpl extends ArrayList implements NodeList {
        /**
         * Default Constructor.
         */
        public NodeListImpl() {
            super();
        }
        
        /**
         * nodelist length.
         *
         * @return int for number of nodes in nodelist
         */
        public int getLength() {
            return this.size();
        }
        
        /**
         * return a node.
         *
         * @param aIndex int for the index of the node
         * @return Node at the index
         */
        public Node item(int aIndex) {
            return (Node) this.get(aIndex);
        }
    }
    
}
