/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009-2011 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 (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.
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id: HttpServiceImpl.java 21674 2011-08-24 10:01:51Z benoitf $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.web.tomcat7.osgi.httpservice;

import java.io.File;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.Servlet;
import javax.servlet.ServletException;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.startup.ContextConfig;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.ow2.jonas.lib.management.javaee.J2eeObjectName;
import org.ow2.jonas.web.JWebContainerServiceException;
import org.ow2.jonas.web.base.osgi.httpservice.DefaultHttpContextImpl;
import org.ow2.jonas.web.base.osgi.httpservice.JOnASHttpService;
import org.ow2.jonas.web.base.osgi.httpservice.OSGIResourcesServlet;
import org.ow2.jonas.web.tomcat7.Tomcat7Service;
import org.ow2.jonas.web.tomcat7.versioning.ContextFinder;

/**
 * Implementation of {@link HttpService} that uses {@link Tomcat7Service}
 * catalina web container to publish servlets. The Http Service allows other
 * bundles in the OSGi environment to dynamically register resources and
 * servlets into the URI namespace of Http Service. A bundle may later
 * unregister its resources or servlets.
 * @author Guillaume Porcher
 */
public class HttpServiceImpl implements JOnASHttpService {

    /**
     * The bundle using the service.
     */
    private final Bundle callerBundle;

    /**
     * The {@link Tomcat7Service} used by this service.
     */
    private final Tomcat7Service tomcatService;

    /**
     * The name of the working directory.
     */
    private final String workDir;

    /**
     * List of the registered wrappers.
     */
    private List<OSGIWrapper> wrapperList = null;

    /**
     * Default constructor.
     * @param bundle The bundle using the service.
     * @param tomcat7Service The {@link Tomcat7Service} to use.
     * @param workDir The name of the working directory.
     */
    public HttpServiceImpl(final Bundle bundle, final Tomcat7Service tomcat7Service, final String workDir) {
        this.callerBundle = bundle;
        this.tomcatService = tomcat7Service;
        this.workDir = workDir;
        this.wrapperList = new LinkedList<OSGIWrapper>();
    }

    /**
     * Creates a default <code>HttpContext</code> for registering servlets or
     * resources with the HttpService, a new <code>HttpContext</code> object is
     * created each time this method is called. <p> The behavior of the methods
     * on the default <code>HttpContext</code> is defined as follows: <ul>
     * <li><code>getMimeType</code>- Does not define any customized MIME types
     * for the Content-Type header in the response, and always returns
     * <code>null</code>. <li><code>handleSecurity</code>- Performs no
     * authentication on the request. This method always returns
     * <code>true</code>. <li><code>getResource</code>- Assumes the named
     * resource is in the context bundle; this method calls the context bundle's
     * <code>Bundle.getResource</code> method, and returns the appropriate URL
     * to access the resource. On a Java runtime environment that supports
     * permissions, the Http Service needs to be granted
     * <code>org.osgi.framework.AdminPermission[*,RESOURCE]</code>. </ul>
     * @return a default <code>HttpContext</code> object, which is an instance
     *         of {@link DefaultHttpContextImpl}.
     */
    public HttpContext createDefaultHttpContext() {
        return new DefaultHttpContextImpl(this.callerBundle);
    }

    /**
     * Registers resources into the URI namespace. <p> The alias is the name in
     * the URI namespace of the Http Service at which the registration will be
     * mapped. An alias must begin with slash ('/') and must not end with slash
     * ('/'), with the exception that an alias of the form &quot;/&quot; is used
     * to denote the root alias. The name parameter must also not end with slash
     * ('/'). See the specification text for details on how HTTP requests are
     * mapped to servlet and resource registrations. <p> For example, suppose
     * the resource name /tmp is registered to the alias /files. A request for
     * /files/foo.txt will map to the resource name /tmp/foo.txt. <pre>
     * httpservice.registerResources(&quot;/files&quot;, &quot;/tmp&quot;,
     * context); </pre> The Http Service will call the <code>HttpContext</code>
     * argument to map resource names to URLs and MIME types and to handle
     * security for requests. If the <code>HttpContext</code> argument is
     * <code>null</code>, a default <code>HttpContext</code> is used (see
     * {@link #createDefaultHttpContext}).
     * @param alias name in the URI namespace at which the resources are
     *        registered
     * @param name the base name of the resources that will be registered
     * @param httpContext the <code>HttpContext</code> object for the registered
     *        resources, or <code>null</code> if a default
     *        <code>HttpContext</code> is to be created and used.
     * @throws NamespaceException if the registration fails because the alias is
     *         already in use.
     * @throws java.lang.IllegalArgumentException if any of the parameters are
     *         invalid
     */
    public void registerResources(final String alias, final String name, final HttpContext httpContext)
            throws NamespaceException, IllegalArgumentException {
        HttpContext context = null;
        // if context is null, use default context
        if (httpContext == null) {
            context = this.createDefaultHttpContext();
        } else {
            context = httpContext;
        }

        final Servlet servlet = new OSGIResourcesServlet(context, name);
        try {
            this.registerServlet(alias, servlet, null, context);
        } catch (final ServletException e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }

    /**
     * Registers a servlet into the URI namespace. <p> The alias is the name in
     * the URI namespace of the Http Service at which the registration will be
     * mapped. <p> An alias must begin with slash ('/') and must not end with
     * slash ('/'), with the exception that an alias of the form &quot;/&quot;
     * is used to denote the root alias. See the specification text for details
     * on how HTTP requests are mapped to servlet and resource registrations.
     * <p> The Http Service will call the servlet's <code>init</code> method
     * before returning. <pre>
     * httpService.registerServlet(&quot;/myservlet&quot;, servlet, initparams,
     * context); </pre> <p> Servlets registered with the same
     * <code>HttpContext</code> object will share the same
     * <code>ServletContext</code>. The Http Service will call the
     * <code>context</code> argument to support the <code>ServletContext</code>
     * methods <code>getResource</code>,<code>getResourceAsStream</code> and
     * <code>getMimeType</code>, and to handle security for requests. If the
     * <code>context</code> argument is <code>null</code>, a default
     * <code>HttpContext</code> object is used (see
     * {@link #createDefaultHttpContext}).
     * @param alias name in the URI namespace at which the servlet is registered
     * @param servlet the servlet object to register
     * @param initParams initialization arguments for the servlet or
     *        <code>null</code> if there are none. This argument is used by the
     *        servlet's <code>ServletConfig</code> object.
     * @param context the <code>HttpContext</code> object for the registered
     *        servlet, or <code>null</code> if a default
     *        <code>HttpContext</code> is to be created and used.
     * @throws NamespaceException if the registration fails because the alias is
     *         already in use.
     * @throws javax.servlet.ServletException if the servlet's <code>init</code>
     *         method throws an exception, or the given servlet object has
     *         already been registered at a different alias.
     * @throws java.lang.IllegalArgumentException if any of the arguments are
     *         invalid
     */
    public void registerServlet(final String alias, final Servlet servlet, final Dictionary initParams,
            HttpContext context) throws ServletException, NamespaceException, IllegalArgumentException {
        // check alias
        if (!this.checkAlias(alias)) {
            throw new IllegalArgumentException(alias + " is not a valid alias.");
        }

        // check if servlet is already registered
        synchronized (wrapperList) {
            for (OSGIWrapper osgiWrapper : wrapperList) {
                if (osgiWrapper.getServlet() == servlet) {
                    throw new ServletException("servlet already registered");
                }
            }
        }

        String versionID = null;
        String userURI = null;

        // Parse context in order to find the context root of the given context
        String parsedContext [] = parseContext(alias);
        String contextRoot = parsedContext[0];
        String servletPath = parsedContext[1];

        if (this.tomcatService.isVersioningEnabled()) {
            versionID = this.getVersionId();
            if (!contextRoot.contains(versionID)) {
                userURI = contextRoot;
                contextRoot += versionID;
            } else {
                userURI = contextRoot.replace(versionID, "");
            }
        }


        // register fake webapp context in host
        final Host host = this.tomcatService.findHost("");

        Container hostContainer = host.findChild(contextRoot);
        OSGIContext osgiContext = null;

        // No host found
        if (hostContainer == null) {
            // if context is null, use default context
            if (context == null) {
                context = this.createDefaultHttpContext();
            }
            // create fake webapp for registering the servlet
            osgiContext = new OSGIContext(context);
            osgiContext.setPath(contextRoot);
            osgiContext.setName(contextRoot);
            osgiContext.setTomcatService(tomcatService);

            // JSR 77
            osgiContext.setJ2EEServer(tomcatService.getJonasServerName());
            osgiContext.setServer(J2eeObjectName.J2EEServer(tomcatService.getDomainName(), tomcatService.getJonasServerName()).toString());

            osgiContext.setDocBase(new File(this.workDir).getAbsolutePath());

            // Empty Injection Map
            osgiContext.setInjectionMap(new HashMap<String, Map<String, String>>());


            final ContextConfig config = new ContextConfig();
            ((Lifecycle) osgiContext).addLifecycleListener(config);

            host.addChild(osgiContext);

            try {
                if (this.tomcatService.isVersioningEnabled() && userURI != null) {
                    final String policy = this.tomcatService.getVersioningService().getDefaultDeploymentPolicy();
                    ContextFinder.bindContextRoot(this.callerBundle.getSymbolicName(), userURI, osgiContext, policy);
                }

                ContextFinder.addNonVersionedContext(contextRoot);
            } catch (final Exception e) {
                throw new JWebContainerServiceException("Failed binding versioned web context", e);
            }
        } else if (hostContainer instanceof OSGIContext) {
            osgiContext = (OSGIContext) hostContainer;
        } else {
            throw new NamespaceException(contextRoot + " already registered.");
        }

        // Check servlet path is unique
        if (osgiContext.findChild(servletPath) != null) {
            throw new NamespaceException(servletPath + " already registered in the context " + contextRoot + ".");
        }

        // create wrapper for the servlet
        final OSGIWrapper osgiWrapper = new OSGIWrapper(servlet, context);
        osgiWrapper.setName(servletPath);
        osgiWrapper.setParentClassLoader(servlet.getClass().getClassLoader());
        if (initParams != null) {
            final Enumeration elements = initParams.keys();
            while (elements.hasMoreElements()) {
                final String name = (String) elements.nextElement();
                osgiWrapper.addInitParameter(name, (String) initParams.get(name));
            }
        }
        // initialize the servlet in the wrapper context
        servlet.init(osgiWrapper);

        // Add to the servlet list
        synchronized (wrapperList) {
            wrapperList.add(osgiWrapper);

            // register servlet in fake webapp
            osgiContext.addChild(osgiWrapper);

            // redirect all request to the servlet
            osgiContext.addServletMapping(servletPath + "/*", osgiWrapper.getName(), true);
        }
    }

    /**
     * Unregisters a previous registration done by <code>registerServlet</code>
     * or <code>registerResources</code> methods. <p> After this call, the
     * registered alias in the URI name-space will no longer be available. If
     * the registration was for a servlet, the Http Service must call the
     * <code>destroy</code> method of the servlet before returning. <p> If the
     * bundle which performed the registration is stopped or otherwise "unget"s
     * the Http Service without calling {@link #unregister} then Http Service
     * must automatically unregister the registration. However, if the
     * registration was for a servlet, the <code>destroy</code> method of the
     * servlet will not be called in this case since the bundle may be stopped.
     * {@link #unregister} must be explicitly called to cause the
     * <code>destroy</code> method of the servlet to be called. This can be done
     * in the <code>BundleActivator.stop</code> method of the bundle registering
     * the servlet.
     * @param alias name in the URI name-space of the registration to unregister
     * @throws java.lang.IllegalArgumentException if there is no registration
     *         for the alias or the calling bundle was not the bundle which
     *         registered the alias.
     */
    public void unregister(final String alias) throws IllegalArgumentException {
        // check alias
        if (!this.checkAlias(alias)) {
            throw new IllegalArgumentException(alias + " is not a valid alias.");
        }

        String versionID = null;
        if (this.tomcatService.isVersioningEnabled()) {
            versionID = this.getVersionId();
        }

        // Extract context
        String parsedContext[] = parseContext(alias);
        String contextRoot = parsedContext[0];
        String servletPath = parsedContext[1];

        if (versionID != null) {
            if (!contextRoot.contains(versionID)) {
                contextRoot += versionID;
            }
        }

        // get context associated with alias
        final Host host = this.tomcatService.findHost("");
        final Context ctx = this.findContext(host, contextRoot);
        if (ctx == null) {
            throw new IllegalArgumentException(contextRoot + " is not registered.");
        }

     // check if is an osgi context
        if (ctx instanceof OSGIContext) {
            OSGIContext osgiContext = (OSGIContext) ctx;

            final OSGIWrapper osgiWrapper = (OSGIWrapper) osgiContext.findChild(servletPath);
            if (osgiWrapper == null) {
                throw new IllegalArgumentException(servletPath + " is not registered in " + contextRoot + ".");
            }
            removeWrapper(osgiWrapper, true);
        } else {
            throw new IllegalArgumentException(contextRoot + " is not registered by this bundle " + this.callerBundle + ".");
        }
    }

    /**
     * Remove a wrapper from its context.
     * @param osgiWrapper the wrapper to remove.
     * @param removeFromList true if needs to be removed from the global list
     */
    private void removeWrapper(final OSGIWrapper osgiWrapper, final boolean removeFromList) {
        OSGIContext osgiContext = (OSGIContext) osgiWrapper.getParent();

        synchronized (wrapperList) {
            // Remove from the servlet list
            if (removeFromList) {
                wrapperList.remove(osgiWrapper);
            }

            // Remove redirection
            osgiContext.removeServletMapping(osgiWrapper.getName() + "/*");

            osgiContext.removeChild(osgiWrapper);
        }

        if (osgiContext.getChildren().length == 0) {
            Host host = (Host) osgiContext.getParent();

            host.removeChild(osgiContext);
            if (this.tomcatService.isVersioningEnabled()) {
                ContextFinder.unbindContextRoot(osgiContext.getPath());
            }

            ContextFinder.removeNonVersionedContext(osgiContext.getPath());
        }
    }

    /**
     * Unregister all servlets registered by the bundle while using the service.
     */
    public void stop() {
        synchronized (wrapperList) {
            Iterator<OSGIWrapper> itWrapper = wrapperList.iterator();
            while (itWrapper.hasNext()) {
                OSGIWrapper osgiWrapper = itWrapper.next();
                // Avoid concurrent modification exception
                itWrapper.remove();
                // Do not remove it from the list in the submethod in order to avoid concurrent access
                removeWrapper(osgiWrapper, false);
            }
        }
    }

    /**
     * Look in catalina Host for the Context associated with the given alias.
     * @param host The catalina host.
     * @param alias The alias.
     * @return The {@link Context} matching the given alias in catalina host.
     */
    private Context findContext(final Host host, final String alias) {
        for (final Container container : host.findChildren()) {
            if (container instanceof Context) {
                if (alias.equals(((Context) container).getPath())) {
                    return (Context) container;
                }
            }
        }
        return null;
    }

    /**
     * Split the context and the servlet path form the alias.
     * @param alias the alias to parse.
     * @return An array containing the context and the servlet path.
     */
    private String[] parseContext(final String alias) {
        int contextIndex = alias.indexOf('/', 1);
        if (contextIndex >= 0) {
            return new String[] {alias.substring(0, contextIndex), alias.substring(contextIndex)};
        } else {
            return new String[] {alias, ""};
        }
    }

    /**
     * Check if alias if a valid osgi HttpService alias. An alias must begin
     * with slash ('/') and must not end with slash ('/'), with the exception
     * that an alias of the form "/" is used to denote the root alias.
     * @param alias The alias to check.
     * @return <code>true</code> if <code>alias</code> is valid,
     *         <code>false</code> otherwise.
     */
    private boolean checkAlias(final String alias) {
        if (alias == null) {
            return false;
        }
        if (alias.equals("/")) {
            return true;
        }
        if (alias.startsWith("/") && !alias.endsWith("/")) {
            return true;
        }
        return false;
    }

    /**
     * Returns version of the context when versioning is enabled.
     * @return version of the context when versioning is enabled.
     */
    private String getVersionId() {
        final String SNAPSHOT_WITH_DOT = ".SNAPSHOT";
        final String SNAPSHOT_WITH_HYPHEN = "-SNAPSHOT";

        String bundleVersion = (String) this.callerBundle.getHeaders().get(Constants.BUNDLE_VERSION);
        if (bundleVersion.endsWith(SNAPSHOT_WITH_DOT)) {
            bundleVersion = bundleVersion.substring(0, bundleVersion.length() - SNAPSHOT_WITH_DOT.length())
                + SNAPSHOT_WITH_HYPHEN;
        }

        return "-" + bundleVersion;
    }
}
