
package uk.co.wingpath.io;

import java.io.*;
import java.net.*;
import java.util.concurrent.*;
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 Runnable that is passed to an
* Executor, with the effect that each connection is managed by its own
* thread.
*/
public class SocketServer
    implements Runnable
{
    private final int port;
    private final Service service;
    private final ServerSocket listenSocket;
    private final Executor executor;
    private final Reporter reporter;

    /**
    * 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 the
    * supplied {@code service}, {@code executor} 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 service the service to be provided on the connection.
    * @param executor executor to provide service threads.
    * The executor must have enough threads available to use one thread for
    * each open 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, Service service,
            Executor executor, Reporter reporter)
        throws IOException
    {
        this.executor = executor;
        this.reporter = reporter;
        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.service = service;
        }
        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);
        }
    }

    /**
    * 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 the
    * supplied {@code service}, {@code executor} 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
        {
            for (;;)
            {
                if (Thread.interrupted ())
                    break;

                try
                {
                    Socket client = listenSocket.accept ();
                    ServiceTask task = new ServiceTask (
                        new SocketConnection (client, reporter),
                        service,
                        reporter
                    );
                    executor.execute (task);
                    if (reporter != null)
                    {
                        reporter.info (null, "Accepted connection from " +
                            client.getInetAddress ().getHostAddress () + ":" +
                            client.getPort ());
                    }
                }
                catch (SocketTimeoutException e)
                {
                    // accept timed out.
                }
                catch (IOException e)
                {
                    // I/O error in accept.
                    // Sun don't document why this is thrown, but it's
                    // probably because the client closed the connection
                    // while it was being accepted.
                    // 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.
                    if (reporter != null)
                        reporter.error (null, Exceptions.getMessage (e));
                }
            }
        }
        finally
        {
            try
            {
                listenSocket.close ();
            }
            catch (IOException e)
            {
            }
        }
    }
}


