/**
 * 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$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.generators.wsgen.ddmodifier;

import org.objectweb.util.monolog.api.BasicLevel;
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;

/**
 * Modify a Deployment Desc for Endpoint. Wrapper around a web.xml DOM.
 *
 * @author Guillaume Sauthier
 */
public class WsEndpointDDModifier extends DeploymentDescModifier {

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

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

    /**
     * servlet-class Element name
     */
    private static final String SERVLET_CLASS = "servlet-class";

    /**
     * servlet-mapping Element name
     */
    private static final String SERVLET_MAPPING = "servlet-mapping";

    /**
     * url-pattern Element name
     */
    private static final String URL_MAPPING = "url-pattern";

    /**
     * init-param Element name
     */
    private static final String INIT_PARAM = "init-param";

    /**
     * param-name Element name
     */
    private static final String PARAM_NAME = "param-name";

    /**
     * param-value Element name
     */
    private static final String PARAM_VALUE = "param-value";

    /**
     * security-role-ref Element name
     */
    private static final String SECURITY_ROLE_REF = "security-role-ref";

    /**
     * url-pattern Element name
     */
    private static final String URL_PATTERN = "url-pattern";

    /**
     * security-constraint Element name
     */
    private static final String SECURITY_CONSTRAINT = "security-constraint";

    /**
     * login-config Element name
     */
    private static final String LOGIN_CONFIG = "login-config";

    /**
     * security-role Element name
     */
    private static final String SECURITY_ROLE = "security-role";

    /**
     * role-name Element name.
     */
    private static final String ROLE_NAME = "role-name";

    /**
     * Used to retrieve any Element childrens.
     */
    private static final String ANY_TAG_NAME = "*";

    /**
     * Creates a new WsEndpointDDModifier object.
     * @param web web.xml document
     */
    public WsEndpointDDModifier(Document web) {
        super(web.getDocumentElement(), web);
    }

    /**
     * Add a new <code>servlet</code> element in the web.xml.
     * @param name servlet name.
     * @param classname servlet fully qualified classname.
     */
    public void addServlet(String name, String classname) {
        Element servlet = newJ2EEElement(SERVLET);
        Element servletName = newJ2EEElement(SERVLET_NAME, name);
        Element servletClass = newJ2EEElement(SERVLET_CLASS, classname);

        servlet.appendChild(servletName);
        servlet.appendChild(servletClass);

        // add servlet in the webapp Element
        getElement().appendChild(servlet);

    }

    /**
     * Add a new security-constraint element into the web.xml
     *
     * @param securityConstraint A node containing the security-constraint setting
     */
    public void addEndpointSecurityConstraint(Node securityConstraint) {
        Element sConstraintElement = newJ2EEElement(SECURITY_CONSTRAINT);

        // securityConstraint comes from another document, therefore have to
        // import first
        Node newSecurityConstraint = getDocument().importNode(securityConstraint, true);

        while (newSecurityConstraint.hasChildNodes()) {
            sConstraintElement.appendChild(newSecurityConstraint.getFirstChild());
        }

        // add security-constraint to webapp
        getElement().appendChild(sConstraintElement);
    }

    /**
     * Add a new login-config element into the web.xml
     *
     * @param loginConfig An element constaining the login-config setting
     */
    public void addEndpointLoginConfig(Element loginConfig) {

        // A <web-app> MUST only have 1 <login-config> element
        // If the same login-config is already there, that's fine.
        // But if there is *another* login-config, we must throw an Exception

        // Try to get a reference on a login-config element
        NodeList configs = getElement().getElementsByTagNameNS(J2EE_NS, LOGIN_CONFIG);
        if (configs.getLength() == 0) {
            // no login-config, we can insert our own ...
            Element myLoginConfig = newJ2EEElement(LOGIN_CONFIG);

            // loginConfig comes from another document, therefore have to import
            // first
            Node importedLoginConfig = getDocument().importNode(loginConfig, true);

            while (importedLoginConfig.hasChildNodes()) {
                myLoginConfig.appendChild(importedLoginConfig.getFirstChild());
            }

            // add login-config to webapp
            getElement().appendChild(myLoginConfig);
        } else {
            // Ooops, found another login-config
            // Check if the login-config is the same than our own...

            // There can be only 1 login-config per web-app
            Element config = (Element) configs.item(0);

            // If nodes are not the same
            if (!areChildNodesIdentical(config, loginConfig)) {
                String msg = "Cannot insert the new login-config element (login-config cannot "
                             + "be defined twice, even with different values !) : " + loginConfig;
                throw new IllegalStateException(msg);
            }
            // OK, That's fine, we can let it 'as is'

        }
    }

    /**
     * @param first first Node to compare childs.
     * @param second second nodes to compare childs.
     * @return Returns <code>true</code> if nodes childs are identical.
     */
    private static boolean areChildNodesIdentical(final Element first, final Element second) {

        // Check childrens
        boolean identical = true;
        // by using getElementsByTagName, we avoid to check meaningless Text nodes
        NodeList firstChilds = first.getElementsByTagName(ANY_TAG_NAME);
        NodeList secondChilds = second.getElementsByTagName(ANY_TAG_NAME);

        if (firstChilds.getLength() != secondChilds.getLength()) {
            // one of the 2 nodes have more childrens, so they are not
            // identical.

            return false;
        } else if (firstChilds.getLength() == 0) {
            // no child *Element*
            // get the elements inner value
            String one = first.getFirstChild().getNodeValue();
            String two = second.getFirstChild().getNodeValue();

            // End recursion
            return one.equals(two);

        } else {
            // There is some children Elements
            // same number of childrens

            boolean childrensIdentical = true;
            for (int index = 0; index < firstChilds.getLength()
                    && childrensIdentical; index++) {
                // Compare childrens 1-to-1
                Element firstChild = (Element) firstChilds.item(index);
                Element secondChild = (Element) secondChilds.item(index);
                childrensIdentical = areElementIdentical(firstChild, secondChild);
            }
            identical &= childrensIdentical;
        }
        return identical;
    }

    /**
     * @param first first element to compare.
     * @param second second element to compare.
     * @return Returns <code>true</code> if both elements are identical.
     */
    private static boolean areElementIdentical(final Element first, final Element second) {

        // Check namespace URI
        if (!first.getNamespaceURI().equals(second.getNamespaceURI())) {
            return false;
        }

        // Check localname
        if (!first.getLocalName().equals(second.getLocalName())) {
            return false;
        }

        return areChildNodesIdentical(first, second);
    }

    /**
     * Add a new security-role element into the web.xml
     * @param securityRole A node containing the login-config setting
     */
    public void addSecurityRole(Node securityRole) {

        // Maybe the security-role element was already added.
        // Look in the Document to find an pre-existing security-role element
        // with the same name.

        Element foundSecurityRole = findSecurityRole((Element) securityRole);
        if (foundSecurityRole == null) {
            // no security-rople element, or none is matching
            // we can add our own ...

            Element mySecurityRole = newJ2EEElement(SECURITY_ROLE);

            // securityRole comes from another document, therefor have to import
            // first
            Node importedSecurityRole = getDocument().importNode(securityRole, true);

            while (importedSecurityRole.hasChildNodes()) {
                mySecurityRole.appendChild(importedSecurityRole.getFirstChild());
            }

            // add security-role to webapp
            getElement().appendChild(mySecurityRole);
        }
        // else, the right security-role is already there, nothing to do ...
    }

    /**
     * @param securityRole <code>security-role</code> element.
     * @return Returns a matching <code>security-role</code> element already
     *         in the Document.
     */
    private Element findSecurityRole(Element securityRole) {

        NodeList potentialSecurityRoles = getElement().getElementsByTagNameNS(J2EE_NS, SECURITY_ROLE);
        if (potentialSecurityRoles.getLength() == 0) {
            // no security-role elements defined
            // exit safely
            return null;
        }

        // Iterates over the potential security-role to find a matching
        // role-name
        Element found = null;
        String requiredRoleName = getRoleName(securityRole);
        // while nothing was found AND iteration not over
        for (int index = 0; (index < potentialSecurityRoles.getLength())
                && (found == null); index++) {
            Element potentialSR = (Element) potentialSecurityRoles.item(index);
            // get the potential security-role name
            String roleName = getRoleName(potentialSR);

            // compare, if equals, we're done
            if (requiredRoleName.equals(roleName)) {
                found = potentialSR;
            }
        }

        // found is still null if no matching security-role was found
        return found;
    }

    /**
     * @param securityRole <code>security-role</code> element in which the role name
     *                     will be extracted.
     * @return Returns the extracted <code>role-name</code> value.
     */
    private static String getRoleName(Element securityRole) {
        Node roleName = securityRole.getElementsByTagNameNS(J2EE_NS, ROLE_NAME).item(0);
        // j2ee:security-role/j2ee:role-name/#text
        return roleName.getFirstChild().getNodeValue().trim();
    }

    /**
     * Remove a <code>servlet</code> element from the web.xml.
     * @param name servlet name.
     */
    public void removeServlet(String name) {
        Element servlet = findServlet(name);

        if (servlet != null) {
            getElement().removeChild(servlet);
        }
    }

    /**
     * Remove a <code>servlet</code> element from the web.xml.
     * @param name servlet name.
     * @return security-role elements for this servlet if such elements exist
     */
    public NodeList removeServletWithSecurity(String name) {
        Element servlet = findServlet(name);
        NodeList elements = null;

        if (servlet != null) {

            elements = servlet.getElementsByTagNameNS(J2EE_NS, SECURITY_ROLE_REF);
            // if the list is empty, return null
            if (elements.getLength() == 0) {
                elements = null;
            }
            getElement().removeChild(servlet);
        }
        return elements;
    }

    /**
     * Add a new <code>servlet-mapping</code> element in the web.xml.
     * @param name servlet name.
     * @param mapping <code>url-mapping</code> value
     */
    public void addServletMapping(String name, String mapping) {
        Element servletMapping = newJ2EEElement(SERVLET_MAPPING);
        Element servletName = newJ2EEElement(SERVLET_NAME, name);
        Element urlMapping = newJ2EEElement(URL_MAPPING, mapping);

        servletMapping.appendChild(servletName);
        servletMapping.appendChild(urlMapping);

        // add servletMapping in the webapp Element
        getElement().appendChild(servletMapping);
    }

    /**
     * Add a new <code>init-param</code> element in the web.xml.
     * @param servletName the servlet name where init-param will be added.
     * @param pName parameter name
     * @param pValue parameter value
     */
    public void addServletParam(String servletName, String pName, String pValue) {
        Element ip = newJ2EEElement(INIT_PARAM);
        Element pn = newJ2EEElement(PARAM_NAME, pName);
        Element pv = newJ2EEElement(PARAM_VALUE, pValue);

        ip.appendChild(pn);
        ip.appendChild(pv);

        Element servlet = findServlet(servletName);
        servlet.appendChild(ip);

    }

    /**
     * Add a new <code>security-role-ref</code> element in the web.xml.
     * @param servletName the servlet name where security-role-ref will be
     *        added.
     * @param securityRoleRefs security-role elements for a servlet
     */
    public void addServletSecurityRoleRefs(String servletName, NodeList securityRoleRefs) {
        // find the servlet
        Element servlet = findServlet(servletName);
        // add all security-role-ref
        for (int i = 0; i < securityRoleRefs.getLength(); i++) {
            Node securityRoleRefItem = securityRoleRefs.item(i);
            servlet.appendChild(securityRoleRefItem);
        }
    }

    /**
     * search webapp element for servlet named with the given name.
     * @param name the searched servlet name
     * @return the found element or null if element is not found (should'nt
     *         occurs).
     */
    private Element findServlet(String name) {
        NodeList nl = getElement().getElementsByTagNameNS(J2EE_NS, SERVLET);
        Element servlet = null;

        for (int i = 0; (i < nl.getLength()) && (servlet == null); i++) {
            Element e = (Element) nl.item(i);

            NodeList names = e.getElementsByTagNameNS(J2EE_NS, SERVLET_NAME);

            // test servlet/servlet-name/#text-node.value
            if (names.item(0).getFirstChild().getNodeValue().equals(name)) {
                servlet = e;
            }
        }

        return servlet;
    }

    /**
     * Remove servlet-mapping tag associated to a given servlet-name
     * @param sName servlet-name
     * @return url-pattern element's value in the servelet-mapping
     */
    public String removeServletMapping(String sName) {
        NodeList nl = getElement().getElementsByTagNameNS(J2EE_NS, SERVLET_MAPPING);
        Element mapping = null;
        String urlPatternValue = null;

        for (int i = 0; (i < nl.getLength()) && (mapping == null); i++) {
            Element e = (Element) nl.item(i);

            NodeList names = e.getElementsByTagNameNS(J2EE_NS, SERVLET_NAME);

            // test servlet-mapping/servlet-name/#text-node.value
            if (names.item(0).getFirstChild().getNodeValue().equals(sName)) {
                mapping = e;
            }
        }

        if (mapping != null) {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG, "mapping element found : " + mapping);
            }
            // pick up the url-pattern before removing
            NodeList urlPatterns = mapping.getElementsByTagNameNS(J2EE_NS, URL_PATTERN);
            urlPatternValue = urlPatterns.item(0).getFirstChild().getNodeValue();

            getElement().removeChild(mapping);
        }

        return urlPatternValue;
    }

    /**
     * Update the security-constraint element having url-pattern equal with
     * oldPattern by replacing this old pattern with the newUrlPattern
     * @param oldUrlPatter url-pattern to be replaced
     * @param newUrlPatterValue url-pattern to replace with
     */
    public void updateSecurityConstraint(String oldUrlPatter, String newUrlPatterValue) {

        NodeList nl = getElement().getElementsByTagNameNS(J2EE_NS, SECURITY_CONSTRAINT);

        // loop over security constraints
        for (int i = 0; i < nl.getLength(); i++) {
            Element e = (Element) nl.item(i);

            // look at nested url-pattern Element(s)
            NodeList urlPatternCollection = e.getElementsByTagNameNS(J2EE_NS, URL_PATTERN);
            for (int j = 0; j < urlPatternCollection.getLength(); j++) {

                Element urlPatternElement = (Element) urlPatternCollection.item(j);
                Text urlPatternText = (Text) urlPatternElement.getFirstChild();

                // if found urlPattern Element, replace its value
                if (urlPatternText.getNodeValue().equals(oldUrlPatter)) {
                    urlPatternText.setNodeValue(newUrlPatterValue);
                }
            }
        }
    }

    /**
     * DOCUMENT ME!
     * @param name DOCUMENT ME!
     * @param home DOCUMENT ME!
     * @param remote DOCUMENT ME!
     * @param link DOCUMENT ME!
     */
    public void addEjbRef(String name, String home, String remote, String link) {

        Element ejbRef = newJ2EEElement("ejb-ref");
        Element ejbRefName = newJ2EEElement("ejb-ref-name", name);
        Element ejbRefType = newJ2EEElement("ejb-ref-type", "Session");
        Element ejbHome = newJ2EEElement("home", home);
        Element ejbRemote = newJ2EEElement("remote", remote);
        Element ejbLink = newJ2EEElement("ejb-link", link);

        ejbRef.appendChild(ejbRefName);
        ejbRef.appendChild(ejbRefType);
        ejbRef.appendChild(ejbHome);
        ejbRef.appendChild(ejbRemote);
        ejbRef.appendChild(ejbLink);

        getElement().appendChild(ejbRef);

    }

    /**
     * DOCUMENT ME!
     * @param name DOCUMENT ME!
     * @param home DOCUMENT ME!
     * @param remote DOCUMENT ME!
     * @param link DOCUMENT ME!
     */
    public void addEjbLocalRef(String name, String home, String remote, String link) {

        Element ejbRef = newJ2EEElement("ejb-local-ref");
        Element ejbRefName = newJ2EEElement("ejb-ref-name", name);
        Element ejbRefType = newJ2EEElement("ejb-ref-type", "Session");
        Element ejbHome = newJ2EEElement("local-home", home);
        Element ejbRemote = newJ2EEElement("local", remote);
        Element ejbLink = newJ2EEElement("ejb-link", link);

        ejbRef.appendChild(ejbRefName);
        ejbRef.appendChild(ejbRefType);
        ejbRef.appendChild(ejbHome);
        ejbRef.appendChild(ejbRemote);
        ejbRef.appendChild(ejbLink);

        getElement().appendChild(ejbRef);

    }
}
