/**
 * 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.base.proxy;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.ow2.jonas.service.ServiceException;
import org.ow2.jonas.web.JWebContainerServiceException;
import org.ow2.jonas.web.base.BaseWebContainerService;
import org.ow2.util.archive.api.ArchiveException;
import org.ow2.util.archive.api.IArchive;
import org.ow2.util.ee.deploy.api.deployable.WARDeployable;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.url.URLUtils;
import org.ow2.util.xml.DocumentParser;
import org.ow2.util.xml.DocumentParserException;
import org.ow2.util.xml.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * This class provides proxy mechanism.<br>
 * This is used to intercept requests and then start the web container or deploy the associated war file.
 * @author Florent Benoit
 */
public class HttpOnDemandProxy implements Runnable {

    /**
     * Thread count (to be written in the Thread name).
     */
    private static int counter = 0;

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

    /**
     * Web container service.
     */
    private BaseWebContainerService webContainerService;

    /**
     * Port number.
     */
    private int httpPortNumber = 0;

    /**
     * Redirect Port number.
     */
    private int redirectPortNumber = 0;

    /**
     * Server socket (for handling clients).
     */
    private ServerSocket serverSocket = null;

    /**
     * Infinite loop for waiting clients ?
     */
    private boolean listeningClients = true;


    /**
     * Map between URL context and info about this context.
     */
    private Map<String, ContextInfo> contextMapping = null;


    /**
     * Default constructor.
     */
    public HttpOnDemandProxy() {
        this.contextMapping = new HashMap<String, ContextInfo>();
    }


    /**
     * Start the thread for this service.
     */
    public void run() {
        // infinite loop until that the service is stopped
        while (this.listeningClients) {

            // Accepting connections...
            Socket socket;
            try {
                socket = serverSocket.accept();
            } catch (IOException e) {
                // Exception but if we're no longer listening, just exit
                if (!listeningClients) {
                    break;
                }
                logger.error("Unable to accept connections", e);
                continue;
            }

            // invalid client
            if (socket == null) {
                continue;
            }

            // Handle the request
            HttpSocketHandler handler = null;
            try {
                handler = new HttpSocketHandler(this, socket);
            } catch (HttpOnDemandProxyException e) {
                logger.error("Unable to handle connection", e);
            }

            // Socket present, we can handle the connection
            if (handler != null) {
                // Handle the request in a separate thread
                Thread thread = new Thread(handler);
                thread.setName(handler.getClass().getName() + "-" + (counter++));
                thread.start();
            }
        }
    }

    /**
     * Start this component.
     * @throws HttpOnDemandProxyException if start-up failed
     */
    public void enable() throws HttpOnDemandProxyException {

        // Create server socket
        try {
            this.serverSocket = new ServerSocket(httpPortNumber);
        } catch (IOException e) {
            throw new ServiceException("Cannot use the given port number '" + httpPortNumber + "'.", e);
        }

        // Reuse address
        try {
            serverSocket.setReuseAddress(true);
        } catch (SocketException e) {
            throw new ServiceException("Cannot set setReuseAddress mode", e);
        }

        // Start thread
        new Thread(this).start();

        // debug info
        logger.debug("Listening on ''{0}'' port number", httpPortNumber);
    }

    /**
     * Stop the component
     * @throws HttpOnDemandProxyException service stopping failed
     */
    public void disable() throws HttpOnDemandProxyException {

        // We're no longer accepting clients
        this.listeningClients = false;

        // Stop the server socket
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                logger.error("Unable to close server socket", e);
            }
        }

    }

    /**
     * @return the http port number used by the proxy.
     */
    public int getHttpPortNumber() {
        return httpPortNumber;
    }

    /**
     * Sets the proxy port number.
     * @param httpPortNumber the proxy port number
     */
    public void setHttpPortNumber(final int httpPortNumber) {
        this.httpPortNumber = httpPortNumber;
    }

    /**
     * @return the server socket
     */
    public ServerSocket getServerSocket() {
        return serverSocket;
    }

    /**
     * Sets the web container service that is linked to this component.
     * @param webContainerService the service instance
     */
    public void setWebContainerService(final BaseWebContainerService webContainerService) {
        this.webContainerService = webContainerService;
    }

    /**
     * @return the web container service instance.
     */
    public BaseWebContainerService getWebContainerService() {
        return webContainerService;
    }

    /**
     * Starts the web container.
     * @throws JWebContainerServiceException if container is not started
     */
    public void startWebContainer() throws JWebContainerServiceException {
        webContainerService.startInternalWebContainer();
    }

    /**
     * @return the redirect port number (internal web container port)
     */
    public int getRedirectPortNumber() {
        return redirectPortNumber;
    }

    /**
     * Sets the redirect port number (internal web container port)
     * @param redirectPortNumber given port number
     */
    public void setRedirectPortNumber(final int redirectPortNumber) {
        this.redirectPortNumber = redirectPortNumber;
    }

    /**
     * Checks if the context is available (can be started or not)
     * @param context  the name of the context
     * @return true if context is available
     */
    public boolean isAvailableContext(final String context) {
        return contextMapping.get(context) != null;
    }

    /**
     * @param context the given context used to get info
     * @return data on the given context (or null if not found)
     */
    public ContextInfo getContextInfo(final String context) {
        return contextMapping.get(context);
    }


    /**
     * Checks if the context is ready to be called by clients
     * @param context the given context name
     * @return true if it is deployed
     */
    public boolean isContextDeployed(final String context) {
        ContextInfo contextInfo = contextMapping.get(context);

        // true if not null and deployed
        return contextMapping.get(context) != null && webContainerService.isWarLoaded(contextInfo.getWarFile());
    }

    /**
     * Called by the deployer if this proxy is enabled. It will store the data for the given war file.
     * @param warFile the given file to deploy
     * @throws HttpOnDemandProxyException if the file cannot be registered
     */
    public void addWar(final WARDeployable war) throws HttpOnDemandProxyException {

        // Get the archive
        IArchive archive = war.getArchive();

        // On-Demand Feature enabled in the jonas-web.xml file ?
        boolean onDemandEnabled = false;

        // Value of the context
        String contextRoot = null;
        URL webxmlURL = null;
        try {
            webxmlURL = archive.getResource("WEB-INF/jonas-web.xml");
            if (webxmlURL != null) {
                // There is an entry
                // analyze the stream
                InputStream is;
                try {
                    is = webxmlURL.openStream();
                } catch (IOException e) {
                    throw new HttpOnDemandProxyException("Cannot analyze the given war file", e);
                }
                // Get document
                Document document;
                try {
                    document = DocumentParser.getDocument(is, false, null);
                } catch (DocumentParserException e) {
                    throw new HttpOnDemandProxyException("Cannot analyze the given war file", e);
                }

                // Root element = <jonas-web-app>
                Element jonasWebAppElement = document.getDocumentElement();

                // get context-root if defined
                contextRoot = XMLUtils.getStringValueElement(jonasWebAppElement, "context-root");

                // get on-demand if defined
                onDemandEnabled = Boolean.parseBoolean(XMLUtils.getStringValueElement(jonasWebAppElement, "on-demand"));

                // Handle special case of root context
                if ("/".equals(contextRoot)) {
                    contextRoot = "";
                }
            }
        } catch (ArchiveException e) {
            throw new HttpOnDemandProxyException("Cannot get resource from war file", e);
        } finally {
            archive.close();
        }

        // This archive has not enabled the on-demand feature in the jonas-web.xml file
        // Deploy now the archive without onDemand mode
        if (!onDemandEnabled) {
            logger.debug("The war file ''{0}'' has not the on-demand flag enabled. WEB-INF/jonas-web.xml path is ''{1}''", archive.getName(), webxmlURL);
            try {
                webContainerService.registerWar(war);
            } catch (JWebContainerServiceException e) {
                throw new HttpOnDemandProxyException("Unable to deploy the war file '" + war + "'.", e);
            }
            // this is a redirect, so stop the execution here
            return;
        }


        // Compute context name by using the filename if not found in jonas-web.xml file
        if (contextRoot == null) {
            contextRoot = archive.getName();
            if (contextRoot.endsWith(".war")) {
                contextRoot = contextRoot.substring(0, contextRoot.length() - 4);
            }
        }

        // ContextInfo present ?
        ContextInfo existingContextInfo = contextMapping.get(contextRoot);
        if (existingContextInfo != null) {
            throw new HttpOnDemandProxyException("Cannot register the war File '" + archive.getName()
                    + "' with context '" + contextRoot + "' as it is already in use for the war File '"
                    + existingContextInfo.getWarFile() + "'");
        }

        // Build data
        ContextInfo contextInfo = new ContextInfo();
        contextInfo.setWarFile(getAbsoluteFilename(war));



        // Add the mapping <context> --> context Info
        contextMapping.put(contextRoot, contextInfo);

        // info
        logger.info("The war file ''{0}'' is available on demand at the context /{1}", archive.getName(), contextRoot);
    }

    /**
     * Convert a Deployable into an absolute filename.
     * @param deployable Deployable to convert
     * @return the absolute path of the deployable
     * @throws HttpOnDemandProxyException if URL cannot be extracted from the archive
     */
    private String getAbsoluteFilename(WARDeployable deployable) throws HttpOnDemandProxyException {

        URL url = null;
        try {
            url = deployable.getArchive().getURL();
        } catch (ArchiveException e) {
            throw new HttpOnDemandProxyException("Cannot extract URL from " + deployable, e);
        }
        File file = URLUtils.urlToFile(url);
        return file.getAbsolutePath();

    }

    /**
     * Un-register the given war file
     * @param war the file to undeploy
     */
    public void removeWar(final WARDeployable war) throws HttpOnDemandProxyException {

        String warFile = getAbsoluteFilename(war);

        // Remove the associated context if found
        Set<Map.Entry<String, ContextInfo>> entries = contextMapping.entrySet();
        String context = null;

        // For each entry, search a matching context (same war file)
        if (entries != null) {
            for (Map.Entry<String, ContextInfo> entry : entries) {
                if (warFile.equals(entry.getValue().getWarFile())) {
                    context = entry.getKey();
                    break;
                }
            }
        }

        // Context is found, remove info on this context
        if (context != null) {
            contextMapping.remove(context);
        }

        // War loaded by the container, remove it
        if (webContainerService.isWarLoaded(warFile)) {
            webContainerService.unRegisterWar(war);
        }
        logger.debug("Unregistering war file ''{0}''", warFile);
    }


}
