/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009 Bull S.A.S.
 * Contact: jonas-team@ow2.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 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.web.tomcat6.ws;

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Set;
import java.io.File;

import org.ow2.jonas.ws.jaxws.ejb.IWebDeployer;
import org.ow2.jonas.ws.jaxws.ejb.IEJBWebserviceEndpoint;
import org.ow2.jonas.ws.jaxws.ejb.ISecurityConstraint;
import org.ow2.jonas.ws.jaxws.ejb.context.IContextNamingStrategy;
import org.ow2.jonas.ws.jaxws.ejb.context.ContextNamingStrategyException;
import org.ow2.jonas.ws.jaxws.ejb.context.IContextNamingInfo;
import org.ow2.jonas.ws.jaxws.IWebservicesContainer;
import org.ow2.jonas.ws.jaxws.IWebservicesModule;
import org.ow2.jonas.ws.jaxws.WSException;
import org.ow2.jonas.ws.jaxws.PortMetaData;
import org.ow2.jonas.web.tomcat6.Tomcat6Service;
import org.ow2.jonas.web.tomcat6.JOnASStandardContext;
import org.ow2.jonas.web.tomcat6.security.Realm;
import org.ow2.jonas.web.tomcat6.ws.strategy.EjbJarContextNamingStrategy;
import org.ow2.jonas.web.tomcat6.ws.security.SecureWebDeploymentDescBuilder;
import org.ow2.jonas.web.base.lib.PermissionManager;
import org.ow2.jonas.deployment.web.WebContainerDeploymentDesc;
import org.ow2.jonas.lib.security.PermissionManagerException;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.Host;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.Context;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.deploy.SecurityCollection;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.startup.ContextConfig;

/**
 * The WebservicesWebDeployer is responsible of deploying/creating/removing
 * web contexts for EJB based endpoints.
 *
 * @author Guillaume Sauthier
 */
public class WebservicesWebDeployer implements IWebDeployer {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(WebservicesWebDeployer.class);

    /**
     * Our support: Tomcat.
     */
    private Tomcat6Service tomcatService;

    /**
     * The work directory where contexts bases will be stored.
     */
    private String workDirectory;

    /**
     * Construct a new {@link WebservicesWebDeployer} manager based on Tomcat.
     * @param tomcatService Tomcat instance
     */
    public WebservicesWebDeployer(final Tomcat6Service tomcatService) {
        this.tomcatService = tomcatService;
    }

    /**
     * @param workDirectory the workDirectory to set
     */
    public void setWorkDirectory(final String workDirectory) {
        this.workDirectory = workDirectory;
    }

    /**
     * Deploy the given module, with all its endpoints in the web
     * container, creating appropriate contexts when required.
     *
     * @param module the webservices module to deploy
     * @throws org.ow2.jonas.ws.jaxws.WSException
     *          if deployment went wrong.
     */
    public void deploy(final IWebservicesModule<? extends IWebservicesContainer<? extends IEJBWebserviceEndpoint>> module) throws WSException {

        logger.debug("Deploying WsModule {0}", module.getName());
        Map<WebContext, List<IEJBWebserviceEndpoint>> endpoints = getWebContextPartitions(module);

        for (Map.Entry<WebContext, List<IEJBWebserviceEndpoint>> entry : endpoints.entrySet()) {

            deployEndpoints(entry.getKey(), entry.getValue());
        }

    }

    /**
     * Deploy a context and the associated endpoints.
     * @param webContext context to be created
     * @param endpoints endpoints to be deployed
     */
    protected void deployEndpoints(final WebContext webContext,
                                   final List<IEJBWebserviceEndpoint> endpoints) throws WSException {
        logger.debug("Deploying Web Context {0}", webContext.getContextName());

        // Create the context
        JOnASStandardContext context = createWebservicesContext(webContext.getContextName());

        // Get the default Host
        Host host = getDefaultHost();

        // Configure the realm to be used if required
        configureSecurityRealm(host, context, webContext, endpoints);

        // Add required servlets inside the Context
        for (IEJBWebserviceEndpoint endpoint : endpoints) {

            // 1 Servlet per endpoint
            configureWebservicesServlet(context, endpoint);

            // Compute a default URL that can be used to access the endpoint locally
            PortMetaData pmd = endpoint.getPortMetaData();
            String url = URLUtils.getEndpointURL(pmd, host);
            pmd.setEndpointURL(url);

        }

        // Add it into the Host (ie start it)
        host.addChild(context);
    }

    /**
     * Configure the security of the given StandardContext.
     * @param host Context's parent, used to get a default Realm
     * @param context Context to be configured
     * @param webContext WebContext description (provides realm/jaasEntry name)
     * @param endpoints list of endpoints (possibly secured)
     * @throws WSException
     */
    private void configureSecurityRealm(final Host host,
                                        final JOnASStandardContext context,
                                        final WebContext webContext,
                                        final List<IEJBWebserviceEndpoint> endpoints) throws WSException {

        // Get a realm (if possible)
        Realm realm = null;
        if (webContext.getRealmName() != null) {
            // JACC realm
            realm = tomcatService.createJOnASRealm(webContext.getRealmName(), true);
        } else if (webContext.getJaasEntry() != null) {
            // JAAS realm
            realm = tomcatService.createJOnASRealm(webContext.getJaasEntry(), false);
        } else {
            // Plug on existing realm if any
            org.apache.catalina.Realm parentRealm = host.getRealm();
            if ((parentRealm != null) && (parentRealm instanceof Realm)) {

                try {
                    realm = (Realm) ((Realm) parentRealm).clone();
                } catch (CloneNotSupportedException cnse) {
                    String err = "Cannot clone the realm used by the existing context or its parent realm";
                    throw new WSException(err, cnse);
                }
            }
        }

        if (realm != null) {
            // A PermissionManager needs a contextId
            String contextId = host.getName() + context.getPath();

            // Creates a permission manager
            WebContainerDeploymentDesc dd = createWebDeploymentDescriptor(endpoints);
            PermissionManager permissionManager = createPermissionManager(contextId, dd);

            // Assign it to the Realm
            realm.setPermissionManager(permissionManager);
            context.setWebDeploymentDescriptor(dd);

            // Setup realm<->context association
            realm.setContext(context);
            context.setRealm(realm);
        }
    }

    /**
     * Creates a PermissionManager for the given Web DD.
     * @param contextId ID of the secured context
     * @param dd web deployment descriptor containing security stuff
     * @return a ready to use PermissionManager
     * @throws WSException if there is an error during the PermissionManager creation
     */
    private PermissionManager createPermissionManager(final String contextId,
                                                      final WebContainerDeploymentDesc dd) throws WSException {

        // Use the descriptor to creates a PermissionManager
        PermissionManager manager = null;
        try {
            manager = new PermissionManager(dd, contextId, true);
            manager.translateServletDeploymentDescriptor();
            manager.commit();
        } catch (PermissionManagerException e) {
            throw new WSException("Cannot construct PermissionManager", e);
        }

        return manager;
    }

    /**
     * Creates a Web deployment descriptor structure from the list of (possibly secured) endpoints.
     * @param endpoints list of endpoints
     * @return a security initialized Web DD
     * @throws WSException if there is an error during the DD generation.
     */
    private WebContainerDeploymentDesc createWebDeploymentDescriptor(final List<IEJBWebserviceEndpoint> endpoints) throws WSException {

        // Build a Web DeploymentDescriptor
        SecureWebDeploymentDescBuilder builder = new SecureWebDeploymentDescBuilder();
        List<ISecurityConstraint> constraints = new ArrayList<ISecurityConstraint>();
        for (IEJBWebserviceEndpoint endpoint : endpoints) {
            ISecurityConstraint sc = endpoint.getSecurityConstraint();
            if (sc != null) {
                constraints.add(sc);
            }
        }
        return builder.createJOnASWebDescriptor(constraints);
    }

    /**
     * Undeploy the given module, with all its endpoints from the
     * web container. An implementation should catch most of the
     * Exceptions and try to cleanupas much as possible.
     *
     * @param module the webservices module to undeploy
     */
    public void undeploy(final IWebservicesModule<? extends IWebservicesContainer<? extends IEJBWebserviceEndpoint>> module) {

        logger.debug("Undeploying WsModule {0}", module.getName());

        // Get Web context used by the endpoints of the module
        List<String> contextsToBeUndeployed = getAssociatedWebContexts(module);

        // Stop all the used Contexts
        for (String contextName : contextsToBeUndeployed) {
            // Find the Context
            Host host = getDefaultHost();

            if (host != null) {
                Context context = (Context) host.findChild(contextName);

                if (context != null) {

                    logger.debug("Undeploying Web Context {0}", contextName);

                    // remove the docbase directory
                    // add by youchao
                    String path = context.getDocBase();
                    File docBase = new File(path);
                    if(docBase.isDirectory()) {
                        docBase.delete();
                    }

                    // Remove and Stop the Context
                    host.removeChild(context);

                    // if there is an associated Realm (now stopped)
                    // get the PermissionManager from it, and delete it
                    org.apache.catalina.Realm realm = context.getRealm();
                    if ((realm != null) && (realm instanceof Realm)) {
                        Realm ctxRealm = (Realm) realm;
                        PermissionManager manager = ctxRealm.getPermissionManager();
                        if (manager != null) {
                            try {
                                manager.delete();
                            } catch (PermissionManagerException pme) {
                                logger.debug("Cannot delete PermissionManager of Context ''{0}''", contextName, pme);
                            }
                        }
                    }


                }
            }
        }

    }

    /**
     * Get the web contexts list associated with the WS module's endpoints.
     * Theses contexts will be undeployed.
     * @param module WS hosting module
     * @return list of undeployable web contexts
     */
    protected List<String> getAssociatedWebContexts(final IWebservicesModule<? extends IWebservicesContainer<? extends IEJBWebserviceEndpoint>> module) {
        List<String> contexts = new ArrayList<String>();

        // Iterates on the deployed contexts to retrive where they were deployed
        for (IWebservicesContainer<? extends IEJBWebserviceEndpoint> container : module.getContainers()) {
            for (IEJBWebserviceEndpoint endpoint : container.getEndpoints()) {
                Map<String, Object> infos = endpoint.getDeploymentInfos();
                String name = (String) infos.get(IEJBWebserviceEndpoint.KEY_CONTEXT_NAME);
                contexts.add(name);
            }
        }

        return contexts;
    }

    /**
     * Create and configure the Servlet to serve the endpoint:
     * <ol>
     *   <li>Create the servlet wrapping the endpoint</li>
     *   <li>Add it to the Context</li>
     *   <li>Set the servlet-mapping</li>
     * </ol>
     * @param context configured context
     * @param endpoint endpoint to be wrapped
     */
    private void configureWebservicesServlet(final StandardContext context,
                                             final IEJBWebserviceEndpoint endpoint) {

        // create the Servlet
        StandardWrapper wrapper = wrapWebserviceEndpoint(endpoint);

        // Configure security constraints (if required)
        configureSecurityConstraints(context, endpoint);

        logger.debug("Added Wrapper/Servlet: {0}", wrapper);

        // Add the Servlet
        context.addChild(wrapper);

        // Add the servlet-mapping
        String pattern = endpoint.getPortMetaData().getUrlPattern();
        context.addServletMapping(pattern, wrapper.getServletName());

    }

    /**
     * Configure security constraints (if required) for the given Context.
     * @param context context to be configured
     * @param endpoint endpoints that may be secured
     */
    private void configureSecurityConstraints(final StandardContext context,
                                              final IEJBWebserviceEndpoint endpoint) {

        // Do something only if there are some security references in the endpoint description
        // otherwise exit
        if (endpoint.getSecurityConstraint() == null) {
            return;
        }

        ISecurityConstraint security = endpoint.getSecurityConstraint();

        // Use endpoint-address as the secured url-pattern
        SecurityConstraint constraint = new SecurityConstraint();
        // user-data-constraint/transport-guarantee defines
        constraint.setUserConstraint(security.getTransportGuarantee());
        // auth-constraint/(role-name)* references the roles defined on the bean
        for (String role : security.getRoleNames()) {
            constraint.addAuthRole(role);
        }
        SecurityCollection collection = new SecurityCollection();
        collection.addPattern(security.getUrlPattern());
        //collection.addPattern("/*");
        // Use defined http-methods (or use all if none defined)
        if (security.getHttpMethods() != null) {
            for (String method : security.getHttpMethods()) {
                collection.addMethod(method);
            }
        }
        constraint.addCollection(collection);
        context.addConstraint(constraint);

        // security-roles
        for (String role : security.getRoleNames()) {
            context.addSecurityRole(role);
        }


        // Manage LoginConfig
        LoginConfig loginConfig = new LoginConfig();
        String method = security.getAuthMethod();
        if (method == null) {
            method = "BASIC";
        } else if ("FORM".equals(method) || "NONE".equals(method)) {
            // This is a non sense to set FORM or NONE on endpoints
            logger.warn("This is an error to set auth-method to ''{0}'', changing it to ''BASIC''", method);
            method = "BASIC";
        }
        loginConfig.setAuthMethod(method);
        // The security realm name is not really used in Tomcat ...
        loginConfig.setRealmName(security.getRealmName());
        context.setLoginConfig(loginConfig);
    }

    /**
     * Group the endpoints of the given module keyed on the web context they will belong to.
     * @param module ws module to be explored
     * @return a Map keyed by context-name
     * @throws WSException if there is a problem regarding the Context (duplicate name, incompatible realm, ...)
     */
    private Map<WebContext, List<IEJBWebserviceEndpoint>> getWebContextPartitions(final IWebservicesModule<? extends IWebservicesContainer<? extends IEJBWebserviceEndpoint>> module) throws WSException {

        // Prepare the Map
        Map<WebContext, List<IEJBWebserviceEndpoint>> endpoints;
        endpoints = new HashMap<WebContext, List<IEJBWebserviceEndpoint>>();

        // Get the default Host
        Host host = getDefaultHost();

        // Iterates on all the endpoints contained in module/container
        for (IWebservicesContainer<? extends IEJBWebserviceEndpoint> container : module.getContainers()) {
            for (IEJBWebserviceEndpoint endpoint : container.getEndpoints()) {

                // Compute a context-root
                String contextName = getContextRoot(endpoint);
                String realmName = null;
                String jaasEntry = null;
                if (endpoint.getSecurityConstraint() != null) {
                    realmName = endpoint.getSecurityConstraint().getRealmName();
                    // TODO add support for JAAS entry
                    //jaasEntry = endpoint.getSecurityConstraint().getJaasEntry();
                }
                WebContext key = new WebContext(contextName, realmName, jaasEntry);

                // Check if the context already exists
                if (host.findChild(contextName) != null) {
                    // The context-root already exists in the Host
                    throw new WSException("Context '" + contextName + "' already deployed in Host '" + host + "'");
                }

                if (endpoints.containsKey(key)) {
                    // Shared context (compatible security realms)
                    List<IEJBWebserviceEndpoint> list = endpoints.get(key);
                    list.add(endpoint);
                } else if ((findWebContext(endpoints.keySet(), contextName)) == null){
                    // No-one uses this context for now
                    List<IEJBWebserviceEndpoint> list = new ArrayList<IEJBWebserviceEndpoint>();
                    list.add(endpoint);
                    endpoints.put(key, list);
                } else {
                    // context name already used, but with an incompatible security scheme
                    WebContext ctx = findWebContext(endpoints.keySet(), contextName);
                    List<IEJBWebserviceEndpoint> endpointsGroup = endpoints.get(ctx);
                    StringBuilder sb = new StringBuilder();
                    sb.append("Context '");
                    sb.append(contextName);
                    sb.append("' for Bean WSEndpoint '");
                    sb.append(endpoint.getContextNamingInfo().getBeanName());
                    sb.append("' is incompatible in regards to security with a context used by beans [");
                    for (IEJBWebserviceEndpoint ep : endpointsGroup) {
                        sb.append(ep.getContextNamingInfo().getBeanName());
                        sb.append(" ");
                    }
                    sb.append("]. ");
                    sb.append("Specify the <session>/<context-root> element in the META-INF/easybeans.xml ");
                    sb.append("or set compatibles <session>/<realm-name> (or <session>/<jaas-entry>)");

                    throw new WSException(sb.toString());
                }
            }
        }

        return endpoints;
    }

    /**
     * Find the WebContext with the given name in the list of provided WebContexts.
     * @param contexts searched list
     * @param name key
     * @return the name matching WebContext, null if not found.
     */
    private WebContext findWebContext(final Set<WebContext> contexts,
                                      final String name) {
        for(WebContext webContext : contexts) {
            if (name.equals(webContext.getContextName())) {
                return webContext;
            }
        }

        return null;
    }

    /**
     * @return the default Host of tomcat
     */
    private Host getDefaultHost() {
        return tomcatService.findHost(null);
    }

    /**
     * Creates an empty (no servlets registered) StandardContext setup for serving webservices endpoints.
     * @param contextName name of the context
     * @return a configured Context
     */
    protected JOnASStandardContext createWebservicesContext(final String contextName) {

        // Create the context instance
        JOnASStandardContext context = new JOnASStandardContext(false, true, false);
        context.setPath(contextName);

        context.setTomcatService(tomcatService);

        // I need here a valid File in the work directory of JOnAS
        File docBase = new File(workDirectory, contextName);
        if (!docBase.exists()) {
            docBase.mkdirs();
        } // TODO remove the directory ?
        context.setDocBase(docBase.getPath());

        // This listener is here to tell tomcat that the Context is
        // OK for startup, even if no ContextConfig was set.
        context.addLifecycleListener(new WebservicesContextLifecycleListener());

        // This is required to set the right Authenticator in the Pipeline
        context.addLifecycleListener(new ContextConfig());

        return context;
    }

    /**
     * Compute a context root for a given endpoint
     * @param endpoint endpoint that will be deployed on the computed context
     * @return a possible web context name
     * @throws WSException if context computation failed
     */
    private String getContextRoot(final IEJBWebserviceEndpoint endpoint) throws WSException {

        String name = endpoint.getPortMetaData().getContextRoot();
        if (name == null) {
            // Get the naming strategy
            // TODO should be configurable
            IContextNamingStrategy strategy = new EjbJarContextNamingStrategy();

            try {
                name = strategy.getContextName(endpoint.getContextNamingInfo());
            } catch (ContextNamingStrategyException e) {
                throw new WSException("Cannot define a web context name for endpoint " + endpoint, e);
            }
        }

        // Fix the name if required
        if (!name.startsWith("/")) {
            name = "/" + name;
        }
        // Store it
        // TODO get ride of the PMD.contextRoot property
        endpoint.getPortMetaData().setContextRoot(name);
        endpoint.getDeploymentInfos().put(IEJBWebserviceEndpoint.KEY_CONTEXT_NAME, name);

        return name;

    }

    /**
     * Wrap the WS endpoint in a StandardWrapper instance that represents the Servlet.
     * @param endpoint endpoint to be wrapped
     * @return a Servlet representation
     */
    protected StandardWrapper wrapWebserviceEndpoint(final IEJBWebserviceEndpoint endpoint) {
        IContextNamingInfo info = endpoint.getContextNamingInfo();
        // Name the servlet after the bean's name
        StandardWrapper wrapper = new WebServiceEndpointStandardWrapper(endpoint, info.getBeanName());
        wrapper.setParentClassLoader(this.getClass().getClassLoader());
        return wrapper;
    }

    /**
     * A specialized LifecyleListener for WS Contexts.
     */
    private static class WebservicesContextLifecycleListener implements LifecycleListener {

        public void lifecycleEvent(LifecycleEvent lifecycleEvent) {

            if (Lifecycle.START_EVENT.equals(lifecycleEvent.getType())) {

                // Tell tomcat that the context is configured
                Context ctx = (Context) lifecycleEvent.getLifecycle();
                ctx.setConfigured(true);
            } else if (Lifecycle.AFTER_START_EVENT.equals(lifecycleEvent.getType())) {

                // Cleanup registered application listeners (such as JSF, ...)
                // This avoid some un-interesting traces
                Context ctx = (Context) lifecycleEvent.getLifecycle();
                String[] listeners = ctx.findApplicationListeners();
                for (String listener : listeners) {
                    ctx.removeApplicationListener(listener);
                }
            }
        }
    }

    /**
     * Data holder.
     */
    private static class WebContext {

        /**
         * Name of the context.
         */
        private String contextName;

        /**
         * Realm/resource name (for JACC realm).
         */
        private String realmName;

        /**
         * JAAS entry name (for JAAS realm).
         */
        private String jaasEntry;

        /**
         * Construct a WebContext.
         * @param name
         * @param realmName
         * @param jaasEntry
         */
        public WebContext(final String name,
                          final String realmName,
                          final String jaasEntry) {
            this.contextName = name;
            this.realmName = realmName;
            this.jaasEntry = jaasEntry;
        }

        public String getContextName() {
            return contextName;
        }

        public String getRealmName() {
            return realmName;
        }

        public String getJaasEntry() {
            return jaasEntry;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            WebContext that = (WebContext) o;

            if (contextName != null ? !contextName.equals(that.contextName) : that.contextName != null) return false;
            if (jaasEntry != null ? !jaasEntry.equals(that.jaasEntry) : that.jaasEntry != null) return false;
            if (realmName != null ? !realmName.equals(that.realmName) : that.realmName != null) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = contextName != null ? contextName.hashCode() : 0;
            result = 31 * result + (realmName != null ? realmName.hashCode() : 0);
            result = 31 * result + (jaasEntry != null ? jaasEntry.hashCode() : 0);
            return result;
        }
    }
}
