
package uk.co.wingpath.io;

import java.io.*;
import java.util.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import uk.co.wingpath.util.*;
import uk.co.wingpath.event.*;

/**
* This class provides a simple general-purpose multi-threaded TCP socket server.
* <p>Each connection is handled by a {@link Runnable} that is passed to an
* {@link ExecutorService}, with the effect that each connection is managed
* by its own thread.
*/
public class SocketServer
    implements Runnable
{
    private final int port;
    private final ServiceFactory serviceFactory;
    private final ServerSocket listenSocket;
    private final Reporter reporter;
    private final LinkedList<Service> services;
    private final AtomicBoolean terminated;
    private final Thread thread;

    /**
    * Constructs a {@code SocketServer}.
    * <p>A server socket is opened using the specified {@code host} and
    * {@code port}. The {@code run} method will start a service task
    * for each connection accepted on the socket using a dedicated thread and
    * the supplied {@code service} and {@code reporter}.
    * @param host name of interface on which to listen for connections.
    * May be {@code null} if any interface may be used.
    * @param port port on which to listen for connections.
    * @param serviceFactory the service to be provided on the connection.
    * @param reporter to report exceptions thrown by the service
    * or by the {@code accept} method of the server socket.
    * @throws IOException if the socket could not be opened.
    */
    public SocketServer (String host, int port, ServiceFactory serviceFactory,
            Reporter reporter)
        throws IOException
    {
        if (reporter == null)
            throw new NullPointerException ("reporter must not be null");
        this.reporter = reporter;
        services = new LinkedList<Service> ();
        terminated = new AtomicBoolean (false);
        if (host == null)
            host = "";
        try
        {
            SocketAddress addr = host.equals ("") ?
                new InetSocketAddress (port) :
                new InetSocketAddress (host, port);
            listenSocket = new ServerSocket ();
            listenSocket.setReuseAddress (true);
            listenSocket.setSoTimeout (200);
            listenSocket.bind (addr);
            this.port = port;
            this.serviceFactory = serviceFactory;
        }
        catch (BindException e)
        {
            if (host.equals (""))
            {
                throw new HIOException ("I113", "Can't listen on port " + port);
            }
            else
            {
                throw new HIOException ("I114",
                    "Can't listen on interface '" + host + "' port " + port);
            }
        }
        catch (UnknownHostException e)
        {
            throw new HIOException ("I115", "Unknown host: " + host);
        }

        assert reporter.debug ("Socket server starting: %s port %d",
            host, port);
        thread = new Thread (this,
            "Socket server " + host + ":" + port);
        thread.setDaemon (true);
        thread.start ();
    }

    /**
    * Removes any completed services from the services lists.
    * This is done simply to stop the list growing indefinitely.
    */
    private void updateServices ()
    {
        synchronized (services)
        {
            Iterator<Service> it = services.iterator ();

            while (it.hasNext ())
            {
                Service s = it.next ();
                if (!s.isAlive ())
                    it.remove ();
            }
        }
    }

    /**
    * Shuts down the server.
    */
    public void shutdown ()
    {
        if (terminated.getAndSet (true))
            return;
        assert reporter.debug ("SocketServer shutting down");

        try
        {
            listenSocket.close ();
        }
        catch (IOException e)
        {
        }

        synchronized (services)
        {
            Iterator<Service> it = services.iterator ();

            while (it.hasNext ())
            {
                Service s = it.next ();
                s.terminate ();
            }

            services.clear ();
        }
    }

    /**
    * Runs the server.
    * <p>This method should normally be called in its own thread.
    * <p>The method repeatedly waits for and accepts connections.
    * It starts a service task for each connection using a dedicated thread
    * and the supplied {@code service} and {@code reporter}.
    * <p>The method returns only if its thread is interrupted or an
    * IOException is thrown by the {@code accept} method of the server socket.
    */
    public void run ()
    {
        Event.checkIsNotEventDispatchThread ();
        try
        {
            while (!terminated.get ())
            {
                if (Thread.interrupted ())
                    break;

                try
                {
                    Socket client = listenSocket.accept ();
                    SocketConnection connection =
                        new SocketConnection (client, reporter);
                    Service service = serviceFactory.createService (connection);
                    synchronized (services)
                    {
                        services.add (service);
                    }
                    reporter.info (null, "Accepted connection from %s:%d",
                        client.getInetAddress ().getHostAddress (),
                        client.getPort ());
                }
                catch (SocketTimeoutException e)
                {
                    // accept timed out.
                }
                catch (IOException e)
                {
                    if (terminated.get ())
                        break;

                    // I/O error in accept.
                    // Sun don't document why this is thrown, but it could
                    // be because the client closed the connection
                    // while it was being accepted, or it could be that we
                    // have too many sockets open.
                    // Quote from Elliote Rusty Harold in O'Reilly's
                    // "Java Network Programming":
                    // "Exceptions thrown by accept() ... should not shut
                    // down the server."
                    // Report it anyway - it might be of interest to the user.
                    reporter.error (null, Exceptions.getMessage (e));
                }

                updateServices ();
            }
        }
        finally
        {
            shutdown ();
        }
    }
}


