/*
 * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package net.java.sip.communicator.util.xml;


import java.io.*;
import org.w3c.dom.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import net.java.sip.communicator.util.*;

/**
 * Common XML Tasks
 *
 * @author Emil Ivov
 * @author Damian Minkov
 */
public class XMLUtils
{
    private static Logger logger = Logger.getLogger(XMLUtils.class);
    /**
     * Extracts from node the attribute with the specified name.
     * @param node the node whose attribute we'd like to extract.
     * @param name the name of the attribute to extract.
     * @return a String containing the trimmed value of the attribute or null
     * if no such attribute exists
     */
    public static String getAttribute(Node node, String name)
    {
        try
        {
            logger.logEntry();

            if(node == null)
                return null;

            Node attribute = node.getAttributes().getNamedItem(name);
            return (attribute==null)? null : attribute.getNodeValue().trim();
        }
        finally
        {
           logger.logExit();
        }
    }

    /**
     * Extracts the String content of a TXT element.
     *
     * @param parentNode the node containing the data that we'd like to get.
     * @return the string contained by the node or null if none existed.
     */
    public static String getText(Element parentNode)
    {
        try
        {
            logger.logEntry();

            Text text = getTextNode(parentNode);

            if (text == null){
               return null;
            }
            else{
                return text.getData();
            }
        }
        finally
        {
           logger.logExit();
        }
    }

    /**
     * Sets data to be the TEXT content of element
     *
     * @param parentNode the parent element.
     * @param data the data to set.
     */
    public static void setText(Element parentNode, String data)
    {
        try
        {
            logger.logEntry();

            Text txt = getTextNode(parentNode);

            if(txt != null)
                txt.setData(data);
            else
            {
                txt = parentNode.getOwnerDocument().createTextNode(data);
                parentNode.appendChild(txt);
            }
        }
        finally
        {
           logger.logExit();
        }
    }

    /**
     * Sets data to be the CDATA content of element
     *
     * @param element the parent element.
     * @param data the data to set.
     */
    public static void setCData(Element element, String data)
    {
        try
        {
            logger.logEntry();

            CDATASection txt = getCDataNode(element);
            if(txt != null)
                txt.setData(data);
            else
            {
                txt = element.getOwnerDocument().createCDATASection(data);
                element.appendChild(txt);
            }
        }
        finally
        {
           logger.logExit();
        }
    }

    /**
     * Extract the CDATA content of the specified element.
     * @param element the element whose data we need
     * @return a String containing the CDATA value of element.
     */
    public static String getCData(Element element)
    {
        try
        {
            logger.logEntry();

            CDATASection text = getCDataNode(element);
            if(text != null)
                return text.getData().trim();
            else
                return null;
        }
        finally
        {
           logger.logExit();
        }
    }


    /**
     * Returns element's CDATA child node (if it has one).
     * @param element the element whose CDATA we need to get.
     * @return a CDATASection object containing the specified element's CDATA
     * content
     */
    public static CDATASection getCDataNode(Element element)
    {
        try
        {
            logger.logEntry();

            return (CDATASection)getChildByType(element,
                                                Node.CDATA_SECTION_NODE);
        }
        finally
        {
           logger.logExit();
        }
    }

    /**
     * Returns element's TEXT child node (if it has one).
     * @param element the element whose TEXT we need to get.
     * @return a <tt>Text</tt> object containing the specified element's
     * text content.
     */
    public static Text getTextNode(Element element)
    {
        try
        {
            logger.logEntry();

            return (Text)getChildByType(element, Node.TEXT_NODE);
        }
        finally
        {
           logger.logExit();
        }
    }

    /**
     * Returns first of the <tt>element</tt>'s child nodes that is of type
     * <tt>nodeType</tt>.
     * @param element the element whose child we need.
     * @param nodeType the type of the child we need.
     * @return a child of the specified <tt>nodeType</tt> or null if none
     * was found.
     */
    public static Node getChildByType(Element element, short nodeType)
    {
        try{
            logger.logEntry();

            if (element == null)
                return null;

            NodeList nodes = element.getChildNodes();
            if (nodes == null || nodes.getLength() < 1)
                return null;

            Node node;
            String data;
            for (int i = 0; i < nodes.getLength(); i++)
            {
                node = nodes.item(i);
                short type = node.getNodeType();
                if (type == nodeType)
                {
                    if (type == Node.TEXT_NODE ||
                        type == Node.CDATA_SECTION_NODE)
                    {
                        data = ( (Text) node).getData();
                        if (data == null || data.trim().length() < 1)
                            continue;
                    }

                    return node;
                }
            }

            return null;
        }finally{
            logger.logExit();
        }
    }

    /**
     * Writes the specified document to the given file adding indentatation.
     * The default encoding is UTF-8.
     *
     * @param out the output File
     * @param document the document to write
     *
     * @throws java.io.IOException in case a TransformerException is thrown by
     * the underlying Transformer.
     */
    public static void writeXML(Document document, File out)
        throws java.io.IOException
    {
        writeXML(document, new StreamResult(out), null, null);
    }

    /**
     * Writes the specified document to the given file adding indentatation.
     * The default encoding is UTF-8.
     *
     * @param writer the writer to use when writing the File
     * @param document the document to write
     *
     * @throws java.io.IOException in case a TransformerException is thrown by
     * the underlying Transformer.
     */
    public static void writeXML(Document document,
                                Writer   writer)
        throws java.io.IOException
    {
        writeXML(document, new StreamResult(writer), null, null);
    }

    /**
     * Writes the specified document to the given file adding indentatation.
     * The default encoding is UTF-8.
     *
     * @param streamResult the streamResult object where the document should be
     * written
     * @param document the document to write
     * @param doctypeSystem the doctype system of the xml document that we should
     * record in the file or null if none is specified.
     * @param doctypePublic the public identifier to be used in the document
     * type declaration.
     *
     * @throws java.io.IOException in case a TransformerException is thrown by
     * the underlying Transformer.
     */
    public static void writeXML(Document document,
                                StreamResult streamResult,
                                String   doctypeSystem,
                                String   doctypePublic)
        throws java.io.IOException
    {
        try {
           logger.logEntry();

           DOMSource domSource = new DOMSource(document);
           TransformerFactory tf = TransformerFactory.newInstance();
           Transformer serializer = tf.newTransformer();
           if(doctypeSystem != null)
                   serializer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
                                                doctypeSystem);
            if(doctypePublic != null)
                   serializer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
                                                doctypePublic);
           serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
           serializer.setOutputProperty(OutputKeys.INDENT, "yes");
           serializer.transform(domSource, streamResult);
       }
        catch (TransformerException ex) {
            logger.error("Error saving configuration file", ex);
            throw new java.io.IOException(
                "Failed to write the configuration file: "
                + ex.getMessageAndLocation());
        }
        catch (IllegalArgumentException ex) {
            //this one is thrown by the setOutputProperty or in other words -
            //shoudln't happen. so let's just log it down in case ...
            logger.error("Error saving configuration file", ex);
        }
        finally {
            logger.logExit();
        }
    }

    /**
     * A simple implementation of XML writing that also allows for indentation.
     * @param doc the Document that we will be writing.
     * @param wri a Writer to write the document through.
     */
    public static void indentedWriteXML(Document doc, Writer wri)
    {
        Element rootElement = doc.getDocumentElement();
        if (wri != null)
        {
            try
            {
                wri.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
                (new DOMElementWriter()).write(rootElement, wri, 0, "  ");
                wri.flush();
            }
            catch (IOException exc)
            {
                throw new RuntimeException("Unable to write xml", exc);
            }
        }
    }


        /**
     * Whenever you'd need to print a configuration node and/or its children.
     *
     * @param root the root node to print.
     * @param out the print stream that should be used to outpu
     * @param recurse boolean
     * @param prefix String
     */
    public static void printChildElements(Element root,
                                          PrintStream out,
                                          boolean recurse,
                                          String prefix)
    {
        out.print(prefix + "<" + root.getNodeName());
        NamedNodeMap attrs = root.getAttributes();
        Node node;
        for(int i = 0; i < attrs.getLength(); i++)
        {
            node = attrs.item(i);
            out.print(" " + node.getNodeName() + "=\""
                      + node.getNodeValue() + "\"");
        }
        out.println(">");

        String data = getText(root);
        if(data != null && data.trim().length() > 0)
            out.println(prefix + "\t" + data);

        data = getCData(root);
        if(data != null && data.trim().length() > 0)
            out.println(prefix + "\t<![CDATA[" + data + "]]>");

        NodeList nodes = root.getChildNodes();
        for(int i = 0; i < nodes.getLength(); i++)
        {
            node = nodes.item(i);
            if(node.getNodeType() == Node.ELEMENT_NODE)
            {
                if(recurse)
                    printChildElements((Element)node, out, recurse, prefix
                                       + "\t");
                else
                    out.println(prefix + node.getNodeName());
            }
        }

        out.println(prefix + "</" + root.getNodeName() + ">");
    }

    /**
     * Returns the child element with the specified tagName for the specified
     * parent element.
     * @param parent The parent whose child we're looking for.
     * @param tagName the name of the child to find
     * @return The child with the specified name or null if no such child was
     *         found.
     * @throws NullPointerException if parent or tagName are null
     */
    public static Element findChild(Element parent, String tagName)
    {
        if(parent == null || tagName == null)
            throw new NullPointerException("Parent or tagname were null! "
                + "parent = " + parent + "; tagName = " + tagName);

        NodeList nodes = parent.getChildNodes();
        Node node;
        int len = nodes.getLength();
        for(int i = 0; i < len; i++)
        {
            node = nodes.item(i);
            if(node.getNodeType() == Node.ELEMENT_NODE
               && ((Element)node).getNodeName().equals(tagName))
                return (Element)node;
        }

        return null;
    }

    /**
     * Looks through all child elements of the specified root (recursively)
     * and returns the first element that corresponds to all parameters.
     *
     * @param root the Element where the search should begin
     * @param tagName the name of the node we're looking for
     * @param keyAttributeName the name of an attribute that the node has to
     * have
     * @param keyAttributeValue the value that attribute must have
     * @return the Element in the tree under root that matches the specified
     * paameters.
     * @throws NullPointerException if any of the arguments is null.
     */
    public static Element locateElement(Element root,
                                        String tagName,
                                        String keyAttributeName,
                                        String keyAttributeValue)
    {
        NodeList nodes = root.getChildNodes();
        Node node;
        int len = nodes.getLength();
        for(int i = 0; i < len; i++)
        {
            node = nodes.item(i);
            if(node.getNodeType() != Node.ELEMENT_NODE)
                continue;

            // is this the node we're looking for?
            if(node.getNodeName().equals(tagName))
            {
                String attr = ((Element)node).getAttribute(keyAttributeName);

                if(    attr!= null
                    && attr.equals(keyAttributeValue))
                    return (Element) node;
            }

            //look inside.
            Element child = locateElement( (Element) node, tagName
                          , keyAttributeName, keyAttributeValue);

            if (child != null)
                return child;

        }

        return null;
    }
}
