
/*
   ConnectionBroker is a network server program that "brokers"
   connections between pairs of users on the network.  It
   keeps a list of clients who have registered themselves as
   available for connection.  Anyone who connects to the 
   server must give one of the following commands (indicated
   by the first charcter that is transmitted to the server):
   
       REGISTER:  The connecting user is added to the list
                  of available clients.  Any remaining
                  characters on the line after the REGISTER
                  command are stored as info for the client.
                  The client is assigned a unique ID number.
                  The client waits for a partner to be
                  connected.  Note that a client can only
                  have one partner.  The client should not
                  send any data, except for a close command,
                  until it has a partner.  When it gets a
                  partner, it will be notified by a message
                  that begins with the CONNECTED character.
                  (Any data that is sent before connection is
                  ignored.)
                  
       CONNECT:   The remaining data on the line after the
                  CONNECT command must be an ID number.  A connection
                  is made between the client who sent the CONNECT
                  command and the waiting client with the specified 
                  ID number, if any.  The server will then
                  relay data between the two clients until one
                  side or the other closes the connection.
                  Note that a message begining with the CLOSE
                  character is interpreted as closing the
                  connection.  If the connection cannot be made,
                  then the server replies with a line begining
                  with the NOT_AVAILABLE character.  If the
                  connection is made, then a line beginning
                  with the CONNECTED character is sent to
                  both partners in the connection.
                  
        SEND_CLIENT_LIST:  The server responds by sending info
                  about all the waiting clients.  Each line
                  begins with the CLIENT_INFO character, followed
                  by the client's ID, a space, and the client's info.
                  After all the info has been sent, a line beginning
                  with the END_CLIENT_INFO character is sent to mark
                  the end of the transmission.
                  
   The port on which the server listens can be specified as a
   command-line parameter.  If none is specified, then the
   DEFAULT_PORT is used.
   
   Note that since the ConnectionBroker relays data between the
   two partners in a connection, the partners can be applets 
   on Web pages downloaded from the computer where the server
   is running.
*/


import java.io.*;
import java.net.*;
import java.util.Vector;

public class ConnectionBroker {

   /* Characters that can occur as the first char on a 
      transmitted or received line of data.  See the comment
      at the top of this file for more info on how these
      are used. */

   static final char REGISTER = '[';          // Characters sent as commands
   static final char CONNECT = '=';           //    by a client to server.
   static final char SEND_CLIENT_LIST = ':';

   static final char CLOSE = ']';  // Sent by a client to close a connection.
   
   static final char NOT_AVAILABLE = '!';     // Characters sent by the
   static final char CONNECTED = '.';         //    server to the a client,
   static final char CLIENT_INFO = '>';       //    with info for the
   static final char END_CLIENT_INFO = '<';   //    client.

   /* The server listens on the DEFAULT_PORT if none is specified
      on the command line. */

   static final int DEFAULT_PORT = 3030;
   
   private static Vector clientList = new Vector();  // Available clients.
   private static int nextClientID = 1;  // Keeps track of the next
                                         //   available number to use
                                         //   to use as a unique ID for
                                         //   a client.
                                         

   public static void main(String[] args) {
           // The main() routine creates a listening socket and
           // listens for requests.  When a request is received,
           // a thread is created to service it.
      int port;  // Port on which server listens.
      ServerSocket listener;
      Socket client;
      if (args.length == 0)
         port = DEFAULT_PORT;
      else {
         try {
            port = Integer.parseInt(args[0]);
         }
         catch (NumberFormatException e) {
            System.out.println(args[0] + " is not a legal port number.");
            return;
         }
      }
      try {
         listener = new ServerSocket(port);
      }
      catch (IOException e) {
         System.out.println("Can't start server.");
         System.out.println(e.toString());
         return;
      }
      System.out.println("Listneing on port " + listener.getLocalPort());
      try {
         while (true) {
            client = listener.accept();
            new ClientThread(client);
         }
      }
      catch (Exception e) {
         System.out.println("Server shut down unexpectedly.");
         System.out.println(e.toString());
         System.exit(1);
      }
   }
   
   
   /* The four following routines manipulate the list of
      availalble clients who are waiting for a connection.
      All manipulation of the clientList is synchronized
      on the list object to avoid race conditions. */

   static void addClient(Client client) {
        // Adds a new client to the clientList vector.
      synchronized(clientList) {
         client.ID = nextClientID++;
         if (client.info.length() == 0)
            client.info = "Anonymous" + client.ID;
         clientList.addElement(client);
      }
      System.out.println("Added client " + client.ID + " " + client.info);
   }
   
   static void removeClient(Client client) {
        // Removes the client from the clientList, if present.
      synchronized(clientList) {
         clientList.removeElement(client);
      }
      System.out.println("Removed client " + client.ID);
   }
   
   static Client getClient(int ID) {
        // Removes client from the clientList vector, if it
        // contains a client of the given ID.  If so, the 
        // removed client is returned.  Otherwise, null is returned.
      synchronized(clientList) {
         for (int i = 0; i < clientList.size(); i++) {
            Client c = (Client)clientList.elementAt(i);
            if (c.ID == ID) {
               clientList.removeElementAt(i);
               System.out.println("Removed client " + c.ID);
               c.ID = 0;  // Since this client is no longer waiting!
               return c;
            }
         }
         return null;
      }
   }
   
   static Client[] getClients() {
        // Returns an array of all the clients in the
        // clientList.  If there are none, null is returned.
      synchronized(clientList) {
         if (clientList.size() == 0)
            return null;
         Client[] clients = new Client[ clientList.size() ];
         for (int i = 0; i < clientList.size(); i++)
            clients[i] = (Client)clientList.elementAt(i);
         return clients;
      }
   }
   


   private static class Client {
         // Contains the data for a client.  For clients who are
         // waiting for a connection, the ID will be > 0.  For
         // clients who request a connection, the ID will be 0.

      int ID;  // Unique identifier for a waiting client.
      String info;  // The info for a waiting client.
      Socket connection;  // Connection between server and this client.
      Reader incoming;    // For reading data from the connection.
      PrintWriter outgoing;   // For sending data over the connection.
      volatile Client partner;   // If non-null, data is being relayed
                                 //   between this client and its partner.

      private StringBuffer line = new StringBuffer();  // For reading data.
      private boolean checkLineFeed;  // A kludge to handle the differerent
                                      //   possible end-of-line markers.
                                      //   If a \r character is seen, this
                                      //   is set to true as a signal to
                                      //   ignore the following \n, if any.

      String getln() throws IOException {
             // Read and return one line of data from the connection.
         if (incoming == null)  // (This is not expected.)
            throw new IOException("Not connected.");
         int ch;
         ch = incoming.read();
         if (ch == -1)
            throw new IOException("Attempt to read past end-of-stream.");
         if (ch == '\n' && checkLineFeed)
            ch = incoming.read();
         line.setLength(0);
         while (ch != -1 && ch != '\n' && ch != '\r') {
            line.append((char)ch);
            ch = incoming.read();
         }
         checkLineFeed = (ch == '\r');
         return line.toString();
      }
      
      void send(String message) throws IOException {
            // Send the message over the connection.
         if (outgoing == null)  // (This is not expected.)
            throw new IOException("Not connected.");
         outgoing.println(message);
         outgoing.flush();
         if (outgoing.checkError())
            throw new IOException("Error while sending data.");
         }
   
   } // end nested class Client
   


   private static class ClientThread extends Thread {
         // A thread that handles one connection to the server.
         // The constructor starts the thread.
         
      Client client;  // Represents the connected client.
      
      ClientThread(Socket connection) {
            // Constructor.  Create the Client object and
            // start the thread to handle the connection.
         client = new Client();
         client.connection = connection;
         start();
      }

      public void run() {
            // The method that is run by this thread.  Process
            // the connection, catching any errors.  in the end,
            // make sure that the connection is closed and that
            // the client has been removed from the client list.
         try {
             processClient();
         }
         catch (Exception e) {
         }
         finally {
            try {
               client.connection.close();
            }
            catch (Exception e) {
            }
            if (client.ID > 0)
               removeClient(client);
         }
      }

      void processClient() throws IOException { 
           // Do all processing for this client.  Create streams
           // for communication.  Read the first line from the
           // incoming stream, which must be one of three possible
           // commands.  Carry out the command, or throw an
           // exception if it is not a legal command.
         client.incoming = 
             new InputStreamReader(client.connection.getInputStream());
         client.outgoing = 
             new PrintWriter(client.connection.getOutputStream());
         String message = client.getln();
         if (message.length() == 0) {
            throw new IOException(
                   "Unexpected input to 'ConnectionBroker' program.");
         }
         char cmd = message.charAt(0);   // This is the command.
         message = message.substring(1); // This might be data for command,
         if (cmd == REGISTER) {
               // Client wants to register and wait for a connection.
               // Add client to the clientList.  Then start reading
               // data until a close command is received.  If this
               // client has a partner, the incoming data is relayed
               // to the partner.  A well-behaved client will not 
               // send data until it has a partner (except if it
               // wants to close before getting a partner).
            client.info = message;
            addClient(client);
            while (true) {
               message = client.getln();
               if (client.partner != null)
                  client.partner.send(message);
               if (message.length() > 0 && message.charAt(0) == CLOSE) {
                  System.out.println("Closing down relay for client "
                                                           + client.ID);
                  break;
               }
            }
         }
         else if (cmd == CONNECT) {
               // Client wants to connect to another waiting client.
               // The data for the command is the ID of the waiting
               // client.  Try to form the partnership.  Then read
               // incoming data and relay it to the partner until
               // a CLOSE command is seen.
            int partnerID;
            try {
               partnerID = Integer.parseInt(message);
            }
            catch (NumberFormatException e) {
               client.send(NOT_AVAILABLE + "Client ID is not an integer.");
               throw new IOException("Illegal connect request.");
            }
            Client partner = getClient(partnerID);
            if (partner == null) {
               client.send(NOT_AVAILABLE + "Unknown client ID.");
               throw new IOException("Requested connection not found.");
            }
            client.partner = partner;      // Set up partnership.
            partner.partner = client;
            partner.send("" + CONNECTED);  // Notify both partners.
            client.send("" + CONNECTED);
            while (true) {
               message = client.getln();
               partner.send(message);
               if (message.length() > 0 && message.charAt(0) == CLOSE)
                  return;
            }
         }
         else if (cmd == SEND_CLIENT_LIST) {
                // Client wants a list of available waiting clients.
                // Send it and exit.
            Client[] clients = getClients();
            if (clients != null) {
               for (int i =0; i < clients.length; i++)
                  client.send("" + CLIENT_INFO + clients[i].ID + " " + clients[i].info);
            }
            client.send("" + END_CLIENT_INFO);
         }
         else {
               // The command is not one of the three legal commands.
            throw new IOException("Unexpected input to 'ConnectionBroker' program.");
         }
      } // end processClient()

   } // end class ClientThread



} // end class ConnectionRelay
