/**
 * JOnAS : Java(TM) OpenSource Application Server
 *
 * 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 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * Initial Developer : Guillaume Sauthier
 * --------------------------------------------------------------------------
 * $Id: XMLUtils.java 21202 2011-04-22 09:42:18Z benoitf $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.generators.genbase.utils;

import java.io.IOException;
import java.io.InputStream;

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

import org.ow2.jonas.deployment.client.JonasAppClientDTDs;
import org.ow2.jonas.deployment.client.JonasAppClientSchemas;
import org.ow2.jonas.deployment.common.lib.JEntityResolver;
import org.ow2.jonas.deployment.ear.EarDTDs;
import org.ow2.jonas.deployment.ear.EarSchemas;
import org.ow2.jonas.deployment.ejb.JonasEjbjarDTDs;
import org.ow2.jonas.deployment.ejb.JonasEjbjarSchemas;
import org.ow2.jonas.deployment.web.JonasWebAppDTDs;
import org.ow2.jonas.deployment.web.JonasWebAppSchemas;
import org.ow2.jonas.deployment.web.WebAppDTDs;
import org.ow2.jonas.deployment.web.WebAppSchemas;
import org.ow2.jonas.deployment.ws.JonasWsSchemas;
import org.ow2.jonas.deployment.ws.WsSchemas;
import org.ow2.jonas.generators.genbase.NoJ2EEWebservicesException;
import org.ow2.jonas.generators.genbase.archive.Archive;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;







/**
 * XML Utils Class. Holds methods for easier DOM parsing, XML modifications, ...
 * @author Guillaume Sauthier
 */
public class XMLUtils {

    /** DOM factory */
    private static DocumentBuilderFactory factory = null;

    /**
     * J2EE namespace
     */
    private static final String J2EE_NS = "http://java.sun.com/xml/ns/j2ee";

    /**
     * JONAS namespace
     */
    private static final String JONAS_NS = "http://www.objectweb.org/jonas/ns";

    /**
     * XML NS namespace
     */
    private static final String XMLNS_NS = "http://www.w3.org/2000/xmlns/";

    /**
     * XML Schema instance namespace
     */
    private static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance";

    /**
     * jonas-session tag name
     */
    private static final String SESSION_BEAN = "jonas-session";

    /**
     * jonas-entity tag name
     */
    private static final String ENTITY_BEAN = "jonas-entity";

    /**
     * jonas-message-driven tag name
     */
    private static final String MESSAGE_BEAN = "jonas-message-driven";

    /**
     * ejb-name tag name
     */
    private static final String EJB_NAME = "ejb-name";

    /**
     * servlet tag name
     */
    private static final String SERVLET = "servlet";

    /**
     * servlet-name tag name
     */
    private static final String SERVLET_NAME = "servlet-name";

    /**
     * jonas-service-ref tag name
     */
    private static final String JONAS_SERVICE_REF = "jonas-service-ref";

    /**
     * service-ref-name tag name
     */
    private static final String SR_NAME = "service-ref-name";

    /**
     * Empty private constructor for Utility Class
     */
    private XMLUtils() {
    }

    /**
     * Returns a new DocumentBuilderFactory.
     * @param validating set the parser validating or not
     * @return a new DocumentBuilderFactory.
     */
    private static DocumentBuilderFactory newFactory(final boolean validating) {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        factory.setValidating(validating);
        setFactoryValidation (factory, validating);

        return factory;
    }

    /**
     * Sets the validation for the factory
     *
     * @param factory the factory
     * @param validating to turn on or off validation
     */
    private static void setFactoryValidation (final DocumentBuilderFactory factory, final boolean validating) {
        factory.setValidating(validating);
        // ignore white space can only be set if parser is validating
        factory.setIgnoringElementContentWhitespace(validating);
        factory.setAttribute("http://apache.org/xml/features/validation/schema", new Boolean(validating));
    }


    /**
     * Creates a new Document from a given InputStream. Validate the file.
     *
     * @param is the InputStream to be parsed
     * @param name filename
     * @param isDTDsAllowed if false, throw exception on DTD Doctype
     *
     * @return the Document instance.
     *
     * @throws ParserConfigurationException ParserConfigurationException
     * @throws SAXException SAXException
     * @throws IOException IOException
     * @throws NoJ2EEWebservicesException NoJ2EEWebservicesException
     */
    public static Document newDocument(final InputStream is, final String name, final boolean isDTDsAllowed) throws ParserConfigurationException, SAXException, IOException, NoJ2EEWebservicesException {

        return newDocument (is, name, isDTDsAllowed, true);
    }

    /**
     * Creates a new Document from a given InputStream.
     *
     * @param is the InputStream to be parsed
     * @param name filename
     * @param isDTDsAllowed if false, throw exception on DTD Doctype
     * @param validate if the file is to be validated
     *
     * @return the Document instance.
     *
     * @throws ParserConfigurationException ParserConfigurationException
     * @throws SAXException SAXException
     * @throws IOException IOException
     * @throws NoJ2EEWebservicesException NoJ2EEWebservicesException
     */
    public static Document newDocument(final InputStream is, final String name, final boolean isDTDsAllowed, final boolean validate)
            throws NoJ2EEWebservicesException, ParserConfigurationException, SAXException, IOException {

        if (factory == null) {
            factory = newFactory(validate);
        }

        if (factory.isValidating() != validate) {
            setFactoryValidation(factory, validate);
        }

        DocumentBuilder builder = factory.newDocumentBuilder();

        // add entity resolver
        if (factory.isValidating()) {
            JEntityResolver jer = new JEntityResolver();
            // add Schemas
            jer.addSchemas(new WebAppSchemas());
            jer.addSchemas(new JonasWebAppSchemas());
            jer.addSchemas(new JonasEjbjarSchemas());
            jer.addSchemas(new JonasAppClientSchemas());
            jer.addSchemas(new EarSchemas());
            jer.addSchemas(new WsSchemas());
            jer.addSchemas(new JonasWsSchemas());

            // add DTDs
            jer.addDtds(new WebAppDTDs());
            jer.addDtds(new JonasWebAppDTDs());
            jer.addDtds(new JonasEjbjarDTDs());
            jer.addDtds(new JonasAppClientDTDs());
            jer.addDtds(new EarDTDs());

            builder.setEntityResolver(jer);
        }
        Document doc = builder.parse(is);
        // close InputStream when finished parsing
        is.close();
        if (doc.getDoctype() != null && !isDTDsAllowed) {
            // DTD case
            // -> Throw Exception
            throw new NoJ2EEWebservicesException(name + " use a DTD. Only XML Schema are accepted for J2EE 1.4 webservices");
        }
        return doc;
    }


    /**
     * Returns the <code>session</code>/<code>entity</code>/
     * <code>message-driven</code> XML Element with given name.
     * @param base <code>jonas-ejb-jar</code> Element.
     * @param bName the bean name to be found.
     * @return the <code>session</code>/<code>entity</code>/
     *         <code>message-driven</code> XML Element.
     */
    public static Element getBeanElement(final Element base, final String bName) {
        Element found = null;

        // try Session Bean
        NodeList sessions = base.getElementsByTagNameNS(JONAS_NS, SESSION_BEAN);

        for (int i = 0; (i < sessions.getLength()) && (found == null); i++) {
            Element session = (Element) sessions.item(i);
            NodeList names = session.getElementsByTagNameNS(JONAS_NS, EJB_NAME);

            // ejb-name mandatory and unique
            Element name = (Element) names.item(0);

            if (name.getFirstChild().getNodeValue().equals(bName)) {
                found = session;
            }
        }

        // try Entity Bean
        NodeList entities = base.getElementsByTagNameNS(JONAS_NS, ENTITY_BEAN);

        for (int i = 0; (i < entities.getLength()) && (found == null); i++) {
            Element entity = (Element) entities.item(i);
            NodeList names = entity.getElementsByTagNameNS(JONAS_NS, EJB_NAME);

            // ejb-name mandatory and unique
            Element name = (Element) names.item(0);

            if (name.getFirstChild().getNodeValue().equals(bName)) {
                found = entity;
            }
        }

        // try Message Driven Bean
        NodeList messages = base.getElementsByTagNameNS(JONAS_NS, MESSAGE_BEAN);

        for (int i = 0; (i < messages.getLength()) && (found == null); i++) {
            Element message = (Element) messages.item(i);
            NodeList names = message.getElementsByTagNameNS(JONAS_NS, EJB_NAME);

            // ejb-name mandatory and unique
            Element name = (Element) names.item(0);

            if (name.getFirstChild().getNodeValue().equals(bName)) {
                found = message;
            }
        }

        return found;
    }

    /**
     * Returns the matching <code>servlet</code> XML Element with given name.
     * @param base <code>web-app</code> Element.
     * @param sName the servlet name to be found.
     * @return the matching <code>servlet</code> XML Element.
     */
    public static Element getServletElement(final Element base, final String sName) {
        Element found = null;

        // Search servlet List
        NodeList servlets = base.getElementsByTagNameNS(J2EE_NS, SERVLET);

        for (int i = 0; (i < servlets.getLength()) && (found == null); i++) {
            Element servlet = (Element) servlets.item(i);
            NodeList names = servlet.getElementsByTagNameNS(J2EE_NS, SERVLET_NAME);

            // servlet-name mandatory and unique
            Element name = (Element) names.item(0);

            if (name.getFirstChild().getNodeValue().equals(sName)) {
                found = servlet;
            }
        }

        return found;
    }

    /**
     * Returns the <code>jonas-service-ref</code> XML Element with given name.
     * @param base <code>web-app</code>/ bean Element containing
     *        <code>jonas-service-ref</code> Element(s).
     * @param srName the service-ref name to be found.
     * @return the <code>jonas-service-ref</code> XML Element.
     */
    public static Element getJonasServiceRef(final Element base, final String srName) {
        Element found = null;

        // Search jonas-service-ref list
        if (base != null) {
            NodeList jsrs = base.getElementsByTagNameNS(JONAS_NS, JONAS_SERVICE_REF);

            for (int i = 0; (i < jsrs.getLength()) && (found == null); i++) {
                Element jsr = (Element) jsrs.item(i);

                NodeList names = jsr.getElementsByTagNameNS(JONAS_NS, SR_NAME);

                // service-ref-name mandatory and unique
                Element name = (Element) names.item(0);

                if (name != null) {
                    // found jonas:service-ref-name
                    if (name.getFirstChild().getNodeValue().equals(srName)) {
                        found = jsr;
                    }
                }
            }
        }

        return found;
    }

    /**
     * default application.xml contains a fake ejb module needed to be parsed
     * without error but not needed for a normal application. Than we remove it.
     * @param doc application.xml document
     */
    public static void cleanDummyApplication(final Document doc) {
        Element root = doc.getDocumentElement();
        NodeList nl = root.getElementsByTagNameNS(J2EE_NS, "module");
        root.removeChild(nl.item(0));
    }

    /**
     * Add an ejb module in an application Document
     * @param app application.xml Document
     * @param ejbjar EJBJar archive
     */
    public static void addEjb(final Document app, final Archive ejbjar) {
        Element module = app.createElementNS(J2EE_NS, "module");
        Element ejb = app.createElementNS(J2EE_NS, "ejb");
        Text ejbText = app.createTextNode(ejbjar.getRootFile().getName());
        ejb.appendChild(ejbText);
        module.appendChild(ejb);

        insertModule(app, module);
    }

    /**
     * Add a client module in an application Document
     * @param app application.xml Document
     * @param client Client archive
     */
    public static void addClient(final Document app, final Archive client) {
        Element module = app.createElementNS(J2EE_NS, "module");
        Element clt = app.createElementNS(J2EE_NS, "java");
        Text cltText = app.createTextNode(client.getRootFile().getName());
        clt.appendChild(cltText);
        module.appendChild(clt);

        insertModule(app, module);
    }

    /**
     * Add an web module in an application Document
     * @param app application.xml Document
     * @param webapp WebApp archive
     * @param ctx context-root
     */
    public static void addWebApp(final Document app, final Archive webapp, final String ctx) {
        Element module = app.createElementNS(J2EE_NS, "module");
        Element web = app.createElementNS(J2EE_NS, "web");
        Element webUri = app.createElementNS(J2EE_NS, "web-uri");
        Element context = app.createElementNS(J2EE_NS, "context-root");

        Text webText = app.createTextNode(webapp.getName());
        Text ctxText = app.createTextNode(ctx);
        webUri.appendChild(webText);
        context.appendChild(ctxText);

        web.appendChild(webUri);
        web.appendChild(context);

        module.appendChild(web);

        insertModule(app, module);
    }

    /**
     * Insert a module in application Document
     * @param app application.xml Document
     * @param module module Element
     */
    private static void insertModule(final Document app, final Element module) {
        Element application = app.getDocumentElement();
        Element first = findFirstSecurityRole(application);
        application.insertBefore(module, first);
    }

    /**
     * return the first found security-role Element (or null if not found)
     * @param app application.xml Document
     * @return the first found security-role Element (or null if not found)
     */
    private static Element findFirstSecurityRole(final Element app) {
        NodeList nl = app.getElementsByTagNameNS(J2EE_NS, "security-role");

        if (nl.getLength() == 0) {
            return null;
        } else {
            return (Element) nl.item(0);
        }
    }

    /**
     * Creates a new XML Document for an empty jonas-client.xml.
     * @return Returns a new XML Document for an empty jonas-client.xml.
     */
    public static Document newJonasClient() {

       Document root = createDocument();

       // jonas-client
       Element jonasClient = root.createElementNS(JONAS_NS, "jonas-client");

       // Automatically retrieve latest schema name
       String schema = JonasAppClientSchemas.getLastSchema();
       addNamespaces(jonasClient, schema);
       root.appendChild(jonasClient);

       return root;
   }

    /**
     * Creates a new XML Document for an empty application.xml.
     * @return Returns a new XML Document for an empty application.xml.
     */
    public static Document newApplication() {

       Document root = createDocument();

       // jonas-client
       Element application = root.createElementNS(J2EE_NS, "application");

       // Automatically retrieve latest schema name
       String schema = EarSchemas.getLastSchema();
       addNamespaces(application, schema);
       root.appendChild(application);

       return root;
   }

    /**
     * @return Returns an empty jonas-web-app Document
     */
    public static Document newJonasWeb() {

        Document root = createDocument();

        // jonas-web-app
        Element jonasWebApp = root.createElementNS(JONAS_NS, "jonas-web-app");

        // Automatically retrieve latest schema name
        int index = JonasWebAppSchemas.JONAS_WEBAPP_SCHEMAS.length - 1;
        String schema = JonasWebAppSchemas.JONAS_WEBAPP_SCHEMAS[index];
        addNamespaces(jonasWebApp, schema);
        root.appendChild(jonasWebApp);

        return root;
    }

    /**
     * @return Returns an empty Document.
     */
    private static Document createDocument() {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        DocumentBuilder builder;
        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
        return builder.newDocument();
    }

    /**
     * Add common namespaces to the given element and add schemaLocation
     * @param e the Element to be updated
     * @param schema the schema location value
     */
    private static void addNamespaces(final Element e, final String schema) {
        Document root = e.getOwnerDocument();
        // create xmlns="http://www.w3.org/XML/1998/namespace"
        Attr xmlns = root.createAttributeNS(XMLNS_NS, "xmlns");
        xmlns.setValue(JONAS_NS);

        // create xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        Attr xsi = root.createAttributeNS(XMLNS_NS, "xmlns:xsi");
        xsi.setValue(XSI_NS);

        // create xsi:schemaLocation="http://www.objectweb.org/jonas/ns
        //                            http://www.objectweb.org/jonas/ns/<schema>"
        Attr schemaLocation = root.createAttributeNS(XSI_NS, "xsi:schemaLocation");
        schemaLocation.setValue(JONAS_NS + " " + JONAS_NS + "/" + schema);

        e.setAttributeNodeNS(xmlns);
        e.setAttributeNodeNS(xsi);
        e.setAttributeNodeNS(schemaLocation);

    }

    /**
     * Check if the web module name is already declared in the application.xml Document
     * @param app the application.xml Doc
     * @param name web application name
     * @return Returns <code>true</code> if the module is already declared, <code>false</code> otherwise.
     */
    public static boolean isWebModuleAlreadyDeclared(final Document app, final String name) {

        // Corresponding Xpath expression : //j2ee:module/j2ee:web/j2ee:web-uri == name

        Element found = null;
        NodeList modules = app.getElementsByTagNameNS(J2EE_NS, "module");
        for (int i = 0; (i < modules.getLength()) && (found == null); i++) {

            // we check each module to see if it is a web module
            Element module = (Element) modules.item(i);
            NodeList webModules = module.getElementsByTagNameNS(J2EE_NS, "web");
            for (int j = 0; (j < webModules.getLength()) && (found == null); j++) {

                // We should only have 0..1 web module in a j2ee:module
                Element webModule = (Element) webModules.item(j);
                NodeList weburiModules = webModule.getElementsByTagNameNS(J2EE_NS, "web-uri");
                // web-uri is mandatory and is the first element under j2ee:web
                Element webUri = (Element) weburiModules.item(0);
                if (webUri.getFirstChild().getNodeValue().equals(name)) {
                    found = webUri;
                }
            }
        }
        // "true" if element was found
        return (found != null);
    }

}