package org.wikiwebserver.core;

import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;


/**
 * A ConnectionListener monitors a ServerSocket, accepts incoming
 * connections and transfers them to a new ConcurrentHandler for serving
 * requests on the connection in a separate thread.
 * 
 * A new ConnectionListener requires a name, a ServerSocket to listen on,
 * a ConnectionHandler class name and a HandlerConfiguration class name.
 * An optional ClassLoader may be specified that is to be used for loading the
 * ConnectionHandler and HandlerConfiguration.
 * 
 * ConnectionListeners require a thread to run in the background.
 * 
 * Established connections should be serviced by calling the 
 * serviceConnections() method periodically.
 * 
 * @author Dr Michael Gardiner
 *
 */
public class ConnectionListener implements Runnable {

    /** The amount of time a connection thread can run before being dropped 
     * to a lower priority.  This has been modified to be reset upon serving
     * each new request for persistent connections. (HTTP 1.1) **/
    public static final long DEFAULT_BURST_PERIOD = 
    	ConfigManager.getInt("listener-default-burst-period");
    
    /** The amount of time a connection thread can run for before being stopped.
     * This is only currently used when running untrusted code **/
    public static final long DEFAULT_HANDLE_PERIOD = 
    	ConfigManager.getInt("listener-untrusted-handler-run-period"); 
    
    /** The maximum number of concurrent connections to WikiWebServer.  If this
     * limit is reached then WikiWebServer will not accept any new connections
     * until an active connection closes.
     * This limit is required to avoid out of memory errors. To increase the 
     * number of connections more memory should be given to the JVM or the 
     * thread stack size should be reduced. **/ 
    public static int DEFAULT_MAX_CONNECTIONS = 
    	ConfigManager.getInt("listener-max-concurrent-connections"); 

    private ThreadGroup connThreads;
    
    private final Collection<ConcurrentHandler> conns; 
    
    private final String name;    
    private final ServerSocket listener;
    private final String handlerClassName;  
    private final String configurationClassName;
    
    private ClassLoader classLoader;    
    // Time of last class loader replacement
    private volatile long classLoaderSetTime;       
    
    public ConnectionListener(String name, ServerSocket listener,
        String handlerClassName, String configurationClassName) 
        throws SocketException {
        
        this(name, listener, null, handlerClassName, configurationClassName);
    }
    
    public ConnectionListener(String name, 
    						  ServerSocket listener,
    						  ClassLoader classLoader, 
    						  String handlerClassName, 
    						  String configurationClasName) throws SocketException {
        
        this.name = name;
        this.listener = listener;
        this.listener.setReuseAddress(true);  
        this.handlerClassName = handlerClassName;
        this.configurationClassName = configurationClasName;
        
        if (classLoader != null) {
            this.classLoader = classLoader;    
        }
        else {
            this.classLoader = Thread.currentThread().getContextClassLoader();
            try {
                // Use the WikiClassLoader
                URL[] classPath = WikiWebServer.getClassUrls();
                this.classLoader = new WikiClassLoader(classPath, classLoader);
            } catch (MalformedURLException ex) {
                ex.printStackTrace();
            }
        }   
        
        this.conns = new Vector<ConcurrentHandler>(DEFAULT_MAX_CONNECTIONS);
        this.connThreads = new ThreadGroup("Connections to " + handlerClassName);
        this.connThreads.setMaxPriority(Thread.NORM_PRIORITY);        
    }
    
    public String getName() {
        return this.name;
    }
    
    public int getPort() {
        return this.listener.getLocalPort();
    }
    
    public SocketAddress getAddress() {
        return this.listener.getLocalSocketAddress();
    }    

    public void run() {
     
        while (!Thread.currentThread().isInterrupted()) {
            try {        
                
                // Create a new handler and thread ready for a new connection
                ConcurrentHandler handler = new ConcurrentHandler(
                    this.handlerClassName, this.configurationClassName);
                
                Thread connThread = new Thread(connThreads, handler);
                connThread.setDaemon(true);         
                
                // Wait if handling maximum connections
                while (getNumActiveConnections() >= DEFAULT_MAX_CONNECTIONS) {
                    Thread.sleep(1);
                }                 
                
                // Accept the connection from a client
                Socket sock = this.listener.accept();   
                handler.handle(sock);
                
                // Start the concurrent handler
                connThread.setContextClassLoader(classLoader);                 
                connThread.start();
                
                conns.add(handler);               
            }
            catch (Exception ex) {
                ex.printStackTrace();
            }
        }       
    }    
    
    public int getNumActiveConnections() {
        return connThreads.activeCount();
    }
    
    public void serviceConnections() {
        
        Set<ConcurrentHandler> snapshot;
        synchronized (conns) {
            snapshot = new HashSet<ConcurrentHandler>(conns);
        }
        
        for (ConcurrentHandler conn : snapshot) {
            
            try {
                // Remove completed connections
                if (!conn.isRunning()) conns.remove(conn);
                
                // If a new class loader is set, encourage connections to close
                if (classLoaderSetTime > conn.getStartTime()) {
                    conn.gracefulClose();
                }            
                
                // Interrupt connections open a long time
                else if (conn.getExecutionTime() > DEFAULT_HANDLE_PERIOD) {
                    conn.forceStop();
                }             
                
                // Slow down classes running a long time
                else if (conn.getExecutionTime() > DEFAULT_BURST_PERIOD) {
                    conn.setPriority(Thread.MIN_PRIORITY);
                }     
            }
            catch (Exception ex) {
                // Failed to set priority, interrupt or end a connection
                ex.printStackTrace();
            }
        }
    }
    
    public void setHandlerClassLoader(ClassLoader classLoader) {
        WareHouse.logInfo("Warning: New class loader applied to " + getName());
        this.classLoader = classLoader;
        this.classLoaderSetTime = System.currentTimeMillis();        
    }
}

