/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2004, TOPP - www.openplans.org
 *    
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.validation.xml;

import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


/**
 * ReaderUtils purpose.
 * 
 * <p>
 * This class is intended to be used as a library of XML relevant operation for
 * the XMLConfigReader class.
 * </p>
 * 
 * <p></p>
 *
 * @author dzwiers, Refractions Research, Inc.
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/validation/src/main/java/org/geotools/validation/xml/ReaderUtils.java $
 * @version $Id: ReaderUtils.java 27862 2007-11-12 19:51:19Z desruisseaux $
 *
 * @see XMLConfigReader
 */
class ReaderUtils {
    /** Used internally to create log information to detect errors. */
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(
            "org.vfny.geoserver.global");

    /**
     * ReaderUtils constructor.
     * 
     * <p>
     * Static class, this should never be called.
     * </p>
     */
    private ReaderUtils() {
    }

    /**
     * loadConfig purpose.
     * 
     * <p>
     * Parses the specified file into a DOM tree.
     * </p>
     *
     * @param configFile The file to parse int a DOM tree.
     *
     * @return the resulting DOM tree
     *
     * @throws ParserConfigurationException DOCUMENT ME!
     * @throws ParserConfigurationException
     * @throws SAXException DOCUMENT ME!
     */
    public static Element loadConfig(Reader configFile)
        throws IOException, ParserConfigurationException, SAXException {
        LOGGER.finest("loading configuration file " + configFile);

        InputSource in = new InputSource(configFile);
        DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
        dfactory.setNamespaceAware(true);

        // TODO turn on validation
        dfactory.setValidating(false);
        dfactory.setIgnoringComments(true);
        dfactory.setCoalescing(true);
        dfactory.setIgnoringElementContentWhitespace(true);

        Document serviceDoc = dfactory.newDocumentBuilder().parse(in);
        Element configElem = serviceDoc.getDocumentElement();

        return configElem;
    }

    /**
     * initFile purpose.
     * 
     * <p>
     * Checks to ensure the file is valid. Returns the file passed in to allow
     * this to wrap file creations.
     * </p>
     *
     * @param f A file Handle to test.
     * @param isDir true when the File passed in is expected to be a directory,
     *        false when the handle is expected to be a file.
     *
     * @return the File handle passed in
     *
     * @throws IOException When the file does not exist or is not the type
     *         specified.
     */
    public static File initFile(File f, boolean isDir)
        throws IOException {
        if (!f.exists()) {
            throw new IOException(
                "Path specified does not have a valid file.\n" + f + "\n\n");
        }

        if (isDir && !f.isDirectory()) {
            throw new IOException(
                "Path specified does not have a valid file.\n" + f + "\n\n");
        }

        if (!isDir && !f.isFile()) {
            throw new IOException(
                "Path specified does not have a valid file.\n" + f + "\n\n");
        }

        LOGGER.fine("File is valid: " + f);

        return f;
    }

    /**
     * getChildElement purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child element of
     * the specified name.  An exception occurs when the node is required and
     * not found.
     * </p>
     *
     * @param root The root element to look for children in.
     * @param name The name of the child element to look for.
     * @param mandatory true when an exception should be thrown if the child
     *        element does not exist.
     *
     * @return The child element found, null if not found.
     *
     * @throws SAXException When a child element is required and not found.
     */
    public static Element getChildElement(Element root, String name,
        boolean mandatory) throws SAXException {
        Node child = root.getFirstChild();

        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                if (name.equals(child.getNodeName())) {
                    return (Element) child;
                }
            }

            child = child.getNextSibling();
        }

        if (mandatory && (child == null)) {
            throw new SAXException(root.getNodeName()
                + " does not contains a child element named " + name);
        }

        return null;
    }

    /**
     * getChildElement purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child element of
     * the specified name.
     * </p>
     *
     * @param root The root element to look for children in.
     * @param name The name of the child element to look for.
     *
     * @return The child element found, null if not found.
     *
     * @see getChildElement(Element,String,boolean)
     */
    public static Element getChildElement(Element root, String name) {
        try {
            return getChildElement(root, name, false);
        } catch (SAXException e) {
            //will never be here.
            return null;
        }
    }

    /**
     * getIntAttribute purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child integer
     * attribute of the specified name.  An exception occurs when the node is
     * required and not found.
     * </p>
     *
     * @param elem The root element to look for children in.
     * @param attName The name of the attribute to look for.
     * @param mandatory true when an exception should be thrown if the
     *        attribute element does not exist.
     * @param defaultValue a default value to return incase the attribute was
     *        not found. mutually exclusive with the ConfigurationException
     *        thrown.
     *
     * @return The int value if the attribute was found, the default otherwise.
     *
     * @throws SAXException When a attribute element is required and not found.
     */
    public static int getIntAttribute(Element elem, String attName,
        boolean mandatory, int defaultValue) throws SAXException {
        String attValue = getAttribute(elem, attName, mandatory);

        if (!mandatory && (attValue == null)) {
            return defaultValue;
        }

        try {
            return Integer.parseInt(attValue);
        } catch (Exception ex) {
            if (mandatory) {
                throw new SAXException(attName + " attribute of element "
                    + elem.getNodeName() + " must be an integer, but it's '"
                    + attValue + "'");
            } else {
                return defaultValue;
            }
        }
    }

    /**
     * getIntAttribute purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child integer
     * attribute of the specified name.  An exception occurs when the node is
     * required and not found.
     * </p>
     *
     * @param elem The root element to look for children in.
     * @param attName The name of the attribute to look for.
     * @param mandatory true when an exception should be thrown if the
     *        attribute element does not exist.
     *
     * @return The value if the attribute was found, the null otherwise.
     *
     * @throws SAXException When a child attribute is required and not found.
     */
    public static String getAttribute(Element elem, String attName,
        boolean mandatory) throws SAXException {
        Attr att = elem.getAttributeNode(attName);

        String value = null;

        if (att != null) {
            value = att.getValue();
        }

        if (mandatory) {
            if (att == null) {
                throw new SAXException("element " + elem.getNodeName()
                    + " does not contains an attribute named " + attName);
            } else if ("".equals(value)) {
                throw new SAXException("attribute " + attName + "in element "
                    + elem.getNodeName() + " is empty");
            }
        }

        return value;
    }

    /**
     * getBooleanAttribute purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child integer
     * attribute of the specified name.  An exception occurs when the node is
     * required and not found.
     * </p>
     *
     * @param elem The root element to look for children in.
     * @param attName The name of the attribute to look for.
     * @param mandatory true when an exception should be thrown if the
     *        attribute element does not exist.
     *
     * @return The value if the attribute was found, the false otherwise.
     *
     * @throws SAXException When a child attribute is required and not found.
     */
    public static boolean getBooleanAttribute(Element elem, String attName,
        boolean mandatory) throws SAXException {
        String value = getAttribute(elem, attName, mandatory);

        return Boolean.valueOf(value).booleanValue();
    }

    /**
     * getChildText purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child text value
     * of the specified element name.
     * </p>
     *
     * @param root The root element to look for children in.
     * @param childName The name of the attribute to look for.
     *
     * @return The value if the child was found, the null otherwise.
     */
    public static String getChildText(Element root, String childName) {
        try {
            return getChildText(root, childName, false);
        } catch (SAXException ex) {
            return null;
        }
    }

    /**
     * getChildText purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child text value
     * of the specified element name.  An exception occurs when the node is
     * required and not found.
     * </p>
     *
     * @param root The root element to look for children in.
     * @param childName The name of the attribute to look for.
     * @param mandatory true when an exception should be thrown if the text
     *        does not exist.
     *
     * @return The value if the child was found, the null otherwise.
     *
     * @throws SAXException When a child attribute is required and not found.
     */
    public static String getChildText(Element root, String childName,
        boolean mandatory) throws SAXException {
        Element elem = getChildElement(root, childName, mandatory);

        if (elem != null) {
            return getElementText(elem, mandatory);
        } else {
            if (mandatory) {
                String msg = "Mandatory child " + childName + "not found in "
                    + " element: " + root;

                throw new SAXException(msg);
            }

            return null;
        }
    }

    /**
     * getChildText purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the text value of the
     * specified element name.
     * </p>
     *
     * @param elem The root element to look for children in.
     *
     * @return The value if the text was found, the null otherwise.
     */
    public static String getElementText(Element elem) {
        try {
            return getElementText(elem, false);
        } catch (SAXException ex) {
            return null;
        }
    }

    /**
     * getChildText purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the text value of the
     * specified element name.  An exception occurs when the node is required
     * and not found.
     * </p>
     *
     * @param elem The root element to look for children in.
     * @param mandatory true when an exception should be thrown if the text
     *        does not exist.
     *
     * @return The value if the text was found, the null otherwise.
     *
     * @throws SAXException When text is required and not found.
     */
    public static String getElementText(Element elem, boolean mandatory)
        throws SAXException {
        String value = null;

        LOGGER.finest("getting element text for " + elem);

        if (elem != null) {
            Node child;

            NodeList childs = elem.getChildNodes();

            int nChilds = childs.getLength();

            for (int i = 0; i < nChilds; i++) {
                child = childs.item(i);

                if (child.getNodeType() == Node.TEXT_NODE) {
                    value = child.getNodeValue();

                    if (mandatory && "".equals(value.trim())) {
                        throw new SAXException(elem.getNodeName()
                            + " text is empty");
                    }

                    break;
                }
            }

            if (mandatory && (value == null)) {
                throw new SAXException(elem.getNodeName()
                    + " element does not contains text");
            }
        } else {
            throw new SAXException("Argument element can't be null");
        }

        return value;
    }

    /**
     * getKeyWords purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns a list of keywords that
     * were found.
     * </p>
     *
     * @param keywordsElem The root element to look for children in.
     *
     * @return The list of keywords that were found.
     */
    public static String[] getKeyWords(Element keywordsElem) {
        NodeList klist = keywordsElem.getElementsByTagName("keyword");
        int kCount = klist.getLength();
        List keywords = new ArrayList(kCount);
        String kword;
        Element kelem;

        for (int i = 0; i < kCount; i++) {
            kelem = (Element) klist.item(i);
            kword = getElementText(kelem);

            if (kword != null) {
                keywords.add(kword);
            }
        }

        Object[] s = (Object[]) keywords.toArray();

        if (s == null) {
            return new String[0];
        }

        String[] ss = new String[s.length];

        for (int i = 0; i < ss.length; i++)
            ss[i] = s[i].toString();

        return ss;
    }

    /**
     * getFirstChildElement purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the element which
     * represents the first child.
     * </p>
     *
     * @param root The root element to look for children in.
     *
     * @return The element if a child was found, the null otherwise.
     */
    public static Element getFirstChildElement(Element root) {
        Node child = root.getFirstChild();

        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                return (Element) child;
            }

            child = child.getNextSibling();
        }

        return null;
    }

    /**
     * getDoubleAttribute purpose.
     * 
     * <p>
     * Used to help with XML manipulations. Returns the first child integer
     * attribute of the specified name.  An exception occurs when the node is
     * required and not found.
     * </p>
     *
     * @param elem The root element to look for children in.
     * @param attName The name of the attribute to look for.
     * @param mandatory true when an exception should be thrown if the
     *        attribute element does not exist.
     *
     * @return The double value if the attribute was found, the NaN otherwise.
     *
     * @throws SAXException When a attribute element is required and not found.
     */
    public static double getDoubleAttribute(Element elem, String attName,
        boolean mandatory) throws SAXException {
        String value = getAttribute(elem, attName, mandatory);

        double d = Double.NaN;

        if (value != null) {
            try {
                d = Double.parseDouble(value);
            } catch (NumberFormatException ex) {
                throw new SAXException("Illegal attribute value for " + attName
                    + " in element " + elem.getNodeName()
                    + ". Expected double, but was " + value);
            }
        }

        return d;
    }
}
