/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.cxf.transport.http_jetty;

import java.io.IOException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.cxf.Bus;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.configuration.jsse.TLSServerParameters;
import org.apache.cxf.transport.HttpUriMapper;
import org.apache.cxf.transport.https_jetty.JettySslConnectorFactory;
import org.mortbay.jetty.AbstractConnector;
import org.mortbay.jetty.Handler;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.ContextHandler;
import org.mortbay.jetty.handler.ContextHandlerCollection;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.thread.BoundedThreadPool;


/**
 * This class is the Jetty HTTP Server Engine that is configured to
 * work off of a designated port. The port will be enabled for 
 * "http" or "https" depending upon its successful configuration.
 */
public class JettyHTTPServerEngine
    implements ServerEngine {
    private static final long serialVersionUID = 1L;
    
    private static final Logger LOG =
        LogUtils.getL7dLogger(JettyHTTPServerEngine.class);
   

    /**
     * The bus.
     */
    private final Bus bus;
    
    /**
     * This is the Jetty HTTP Server Engine Factory. This factory caches some 
     * engines based on port numbers.
     */
    private final JettyHTTPServerEngineFactory factory;
    
    
    /**
     * This is the network port for which this engine is allocated.
     */
    private final int port;
    
    /**
     * This field holds the protocol for which this engine is 
     * enabled, i.e. "http" or "https".
     */
    private String protocol;
    
    private int servantCount;
    private Server server;
    private AbstractConnector connector;
    private JettyConnectorFactory connectorFactory;
    private ContextHandlerCollection contexts;
    
    /**
     * This field holds the TLS ServerParameters that are programatically
     * configured. The tlsServerParamers (due to JAXB) holds the struct
     * placed by SpringConfig.
     */
    private TLSServerParameters tlsProgrammaticServerParameters;
    
    /**
     * This field hold the threading parameters for this particular engine.
     */
    private ThreadingParameters threadingParameters;
    
    /**
     * This boolean signfies that SpringConfig is over. finalizeConfig
     * has been called.
     */
    private boolean configFinalized;
    
    /**
     * This constructor is solely called by the JettyHTTPServerEngineFactory.
     */
    JettyHTTPServerEngine(
        JettyHTTPServerEngineFactory fac, 
        Bus bus,
        int port) {
        this.bus     = bus;
        this.factory = fac;
        this.port    = port;
    }
    
    /**
     * The bus.
     */
    public Bus getBus() {
        return bus;
    }
    
    /**
     * Returns the protocol "http" or "https" for which this engine
     * was configured.
     */
    public String getProtocol() {
        return protocol;
    }
    
    /**
     * Returns the port number for which this server engine was configured.
     * @return
     */
    public int getPort() {
        return port;
    }
    
    /**
     * This method will shut down the server engine and
     * remove it from the factory's cache. 
     */
    public void shutdown() {
        factory.destroyForPort(port);
    }
    
    /**
     * Register a servant.
     * 
     * @param url the URL associated with the servant
     * @param handler notified on incoming HTTP requests
     */
    public synchronized void addServant(URL url, JettyHTTPHandler handler) {
        if (server == null) {
            server = new Server();
            
            connector = connectorFactory.createConnector(port);
            //REVISITION for setup the connector's threadPool
            /*
            if (getListener().isSetMaxIdleTimeMs()) {
                listener.setMaxIdleTimeMs(getListener().getMaxIdleTimeMs().intValue());
            }
            if (getListener().isSetLowResourcePersistTimeMs()) {
                int lowResourcePersistTime = 
                    getListener().getLowResourcePersistTimeMs().intValue();
                listener.setLowResourcePersistTimeMs(lowResourcePersistTime);
            }*/

            server.addConnector(connector);
            contexts = new ContextHandlerCollection();
            server.addHandler(contexts);
            try {
                server.start();
                if (connector.getThreadPool() instanceof BoundedThreadPool
                    && isSetThreadingParameters()) {
                    BoundedThreadPool pool = (BoundedThreadPool)connector.getThreadPool();
                    if (getThreadingParameters().isSetMinThreads()) {
                        pool.setMinThreads(getThreadingParameters().getMinThreads());
                    }
                    if (getThreadingParameters().isSetMaxThreads()) {
                        pool.setMaxThreads(getThreadingParameters().getMaxThreads());
                    }
                }
            } catch (Exception e) {
                LOG.log(Level.SEVERE, e.getMessage(), e);
                //problem starting server
                try {                    
                    server.stop();
                    server.destroy();
                } catch (Exception ex) {
                    LOG.log(Level.SEVERE, ex.getMessage(), ex);
                }    
            }
        }
        
        String contextName = HttpUriMapper.getContextName(url.getPath());
        final String smap = HttpUriMapper.getResourceBase(url.getPath());
        ContextHandler context = new ContextHandler();
        context.setContextPath(contextName);
        context.setHandler(handler);
        contexts.addHandler(context);
        if (contexts.isStarted()) {           
            try {                
                context.start();
            } catch (Exception ex) {
                LOG.log(Level.WARNING, ex.getMessage(), ex);
            }
        }
        
        handler.setName(smap);        
        ++servantCount;
    }
    
    /**
     * Remove a previously registered servant.
     * 
     * @param url the URL the servant was registered against.
     */
    public synchronized void removeServant(URL url) {        
        
        String contextName = HttpUriMapper.getContextName(url.getPath());
        //final String smap = HttpUriMapper.getResourceBase(url.getPath());

        boolean found = false;
        // REVISIT:After a stop(), the server is null, and therefore this 
        // operation shouldn't find a handler, but then below, why do we
        // print a message to Std Error?
        if (server != null) {
            for (Handler handler : contexts.getChildHandlersByClass(ContextHandler.class)) {
                ContextHandler contextHandler = null;
                if (handler instanceof ContextHandler) {
                    contextHandler = (ContextHandler) handler;
                    if (contextName.equals(contextHandler.getContextPath())) {
                        try {
                            contexts.removeHandler(handler);
                            handler.stop();
                            handler.destroy();
                        } catch (Exception e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        /*for (Handler httpHandler
                            : contextHandler.getChildHandlersByClass(JettyHTTPHandler.class)) {
                            if (smap.equals(((JettyHTTPHandler)httpHandler).getName())) {
                                contexts.removeHandler(httpHandler);
                                try {
                                    handler.stop();                               
                                    handler.destroy();
                                } catch (Exception e) {
                                    // TODO Auto-generated catch block
                                    e.printStackTrace();
                                }    
                            }
                        
                        }*/
                    }
                    found = true;
                }
            }
        }
        if (!found) {
            LOG.warning("Not able to remove the handler");
        }
        
        --servantCount;
        /* Bug in Jetty, we cannot do this.  If we restart later, data goes off
         * someplace unknown
        if (servantCount == 0) {
            server.removeListener(listener);
        }
        */
    }

    /**
     * Get a registered servant.
     * 
     * @param url the associated URL
     * @return the HttpHandler if registered
     */
    public synchronized Handler getServant(URL url)  {
        String contextName = HttpUriMapper.getContextName(url.getPath());
        //final String smap = HttpUriMapper.getResourceBase(url.getPath());
        
        Handler ret = null;
        // After a stop(), the server is null, and therefore this 
        // operation should return null.
        if (server != null) {           
            for (Handler handler : server.getChildHandlersByClass(ContextHandler.class)) {
                ContextHandler contextHandler = null;
                if (handler instanceof ContextHandler) {
                    contextHandler = (ContextHandler) handler;
                    if (contextName.equals(contextHandler.getContextPath())) {           
                        ret = contextHandler.getHandler();
                        break;
                    }
                }
            }    
        }
        return ret;
    }

    protected void retrieveListenerFactory() {
        if (tlsProgrammaticServerParameters != null) {
            connectorFactory = 
                getHTTPSConnectorFactory(tlsProgrammaticServerParameters);
            protocol = "https";
        } else {
            connectorFactory = getHTTPConnectorFactory();
            protocol = "http";
        }
        LOG.fine("Configured port " + port + " for \"" + protocol + "\".");
    }

    /**
     * This method creates a connector factory. If there are TLS parameters
     * then it creates a TLS enabled one.
     */
    protected JettyConnectorFactory getHTTPConnectorFactory() {
        return new JettyConnectorFactory() {                     
            public AbstractConnector createConnector(int porto) {
                SelectChannelConnector result = 
                    new SelectChannelConnector();
                
                // Regardless the port has to equal the one
                // we are configured for.
                assert porto == port;
                
                //SocketConnector result = new SocketConnector();
                result.setPort(porto);
                return result;
            }
        };
    }
    
    /**
     * This method creates a connector factory enabled with the JSSE
     */
    protected JettyConnectorFactory getHTTPSConnectorFactory(
            TLSServerParameters tlsParams
    ) {
        return new JettySslConnectorFactory(tlsParams);
    }
    
    /**
     * This method is called after configure on this object.
     */
    protected void finalizeConfig() 
        throws GeneralSecurityException,
               IOException {
        retrieveListenerFactory();
        this.configFinalized = true;
    }
    
    /**
     * This method is called by the ServerEngine Factory to destroy the 
     * listener.
     *
     */
    protected void stop() throws Exception {
        if (server != null) {
            connector.close();
            server.stop();
            server.destroy();
            server   = null;
        }
    }
    
    /**
     * This method is used to programmatically set the TLSServerParameters.
     * This method may only be called by the factory.
     */
    protected void setProgrammaticTlsServerParameters(TLSServerParameters params) {
        tlsProgrammaticServerParameters = params;
        if (this.configFinalized) {
            this.retrieveListenerFactory();
        }
    }
    
    /**
     * This method returns the programmatically set TLSServerParameters, not
     * the TLSServerParametersType, which is the JAXB generated type used 
     * in SpringConfiguration.
     * @return
     */
    protected TLSServerParameters getProgrammaticTlsServerParameters() {
        return tlsProgrammaticServerParameters;
    } 

    /**
     * This method sets the threading parameters for this particular 
     * server engine.
     * This method may only be called by the factory.
     */
    public void setThreadingParameters(ThreadingParameters params) {
        threadingParameters = params;
    }
    
    /**
     * This method returns whether the threading parameters are set.
     */
    public boolean isSetThreadingParameters() {
        return threadingParameters != null;
    }
    
    /**
     * This method returns the threading parameters that have been set.
     * This method may return null, if the threading parameters have not
     * been set.
     */
    public ThreadingParameters getThreadingParameters() {
        return threadingParameters;
    }
    
}
