/**
 * 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 (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 20516 2010-10-29 14:34:27Z 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.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;

    /**
     * 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;
    }

    /**
     * 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.");
        }
        String versionID = null;
        String userURI = null;
        String contextRoot = alias;
        if (this.tomcatService.isVersioningEnabled()) {
            versionID = this.getVersionId();
            if (!contextRoot.contains(versionID)) {
                userURI = contextRoot;
                contextRoot += versionID;
            } else {
                userURI = contextRoot.replace(versionID, "");
            }
        }

        // if context is null, use default context
        if (context == null) {
            context = this.createDefaultHttpContext();
        }

        // create fake webapp for registering the servlet
        final OSGIContext osgiContext = new OSGIContext(context, this.callerBundle);
        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);

        // create wrapper for the servlet
        final OSGIWrapper osgiWrapper = new OSGIWrapper(servlet, context);
        osgiWrapper.setName(servlet.getClass().getName());
        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);

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

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

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

        if (this.findContext(host, contextRoot) != null) {
            throw new NamespaceException(contextRoot + " already registered.");
        }

        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);
        }

    }

    /**
     * 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();
        }
        String contextRoot = alias;
        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 this s an osgi context, registered by the same bundle
        if (ctx instanceof OSGIContext && ((OSGIContext) ctx).getCallerBundle().equals(this.callerBundle)) {
            host.removeChild(ctx);
        } else {
            throw new IllegalArgumentException(contextRoot + " is not registered by this bundle " + this.callerBundle
                    + ".");
        }

        if (this.tomcatService.isVersioningEnabled()) {
            ContextFinder.unbindContextRoot(contextRoot);
        }

        ContextFinder.removeNonVersionedContext(contextRoot);
    }

    /**
     * Unregister all servlets registered by the bundle while using the service.
     */
    public void stop() {
        final Host host = this.tomcatService.findHost("");
        for (final Container container : host.findChildren()) {
            if (container instanceof OSGIContext) {
                if (this.callerBundle.equals(((OSGIContext) container).getCallerBundle())) {
                    host.removeChild(container);
                }
            }
        }
    }

    /**
     * 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;
    }

    /**
     * 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;
    }
}
