/*
 * DataServer.java
 *
 * Created on 17 luty 2008, 08:47
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package socketserver;

/**
 *
 * @author
 */

import java.net.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;


public class DataServer extends JFrame
       implements ActionListener, Runnable
{

  // GUI setup attributes

  JMenuItem  fMenuClose   = null;
  JTextArea  fTextArea    = null;
  JTextField fTextField   = null;
  JButton    fStartButton = null;

  // Networking setup
  // Use a Vector to keep track of the DataWorker list.
  Vector fWorkerList = null;

  // Connect to clients
  ServerSocket fServerSocket = null;

  int fDataServerPort = 2222;// Default port number

  // Client counting and limit
  static int fClientCounter__ = 0;
  static int fMaxClients__    = 10;

  // Number of data values per set or "event"
  static int fNumDataVals__ = 6;

  // Flag for the server socket loop.
  boolean fKeepServing = true;

  /**  Open frame for the user interface. **/
  public static void main (String [] args) {
    // Can pass frame title in command line arguments
    String title="DataServer";
    if (args.length != 0) title = args[0];

    DataServer f = new DataServer (title);
    f.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
    f.setVisible (true);
  } // main

  /**
    *  Pass a title to the frame via the constructor
    *  argument. Build the GUI.
   **/
  DataServer (String title) {
    super (title);

    // Create the vector to list the DataWorker objects
    fWorkerList = new Vector ();

    // Now build the GUI.
    Container content_pane = getContentPane ();

    // Create a user interface.
    content_pane.setLayout ( new BorderLayout () );
    fTextArea = new JTextArea ("");
    JScrollPane area_scroll_pane = new JScrollPane (fTextArea);

    content_pane.add ( area_scroll_pane, "Center");

    // Create a panel with a textfield and two buttons
    fTextField = new JTextField ("2222");

    fStartButton = new JButton ("Start");
    fStartButton.addActionListener (this);


    JPanel panel = new JPanel (new GridLayout (1,2));
    JPanel button_panel = new JPanel ();
    panel.add (fTextField);
    button_panel.add (fStartButton);
    panel.add (button_panel);

    content_pane.add ( panel, "South");

    // Use the helper method makeMenuItem
    // for making the menu items and registering
    // their listener.
    JMenu m = new JMenu ("File");

    // Use File menu to hold Quit command.
    m.add (fMenuClose = makeMenuItem ("Quit"));

    JMenuBar mb = new JMenuBar ();
    mb.add (m);

    setJMenuBar (mb);
    setSize (400,400);
  } // ctor

  /** Process events from the frame menu and the chooser. **/
  public void actionPerformed (ActionEvent e) {
    boolean status = false;
    String command = e.getActionCommand ();

    if (command.equals ("Start") ) {
        try {
          fDataServerPort = Integer.parseInt (fTextField.getText ());
        }catch (NumberFormatException nfe){
          println ("Bad port number");
          return;
        }

        fStartButton.setEnabled (false);

        Thread thread = new Thread (this);
        thread.start ();

    } else if (command.equals ("Quit") ){
      fKeepServing = false;
      dispose ();
    }
  } // actionPerformed

  /** Create a ServerSocket and loop waiting for clients. **/
  public void run () {

    // The server_socket is used to make connections to
    // DataClients at this port number
    try {
        fServerSocket = new ServerSocket (fDataServerPort);
    }
    catch (IOException e) {
        println ("Error in server socket");
        return;
    }

    println ("Waiting for users...");

    // Loop here to grab clients
    while (fKeepServing) {

      try {
          // accept () blocks until a connection is made
          Socket socket = fServerSocket.accept ();

          // Do the setup this socket and then loop
          // back around to wait for the next DataClient.
          DataWorker worker = new DataWorker (this, socket);
          worker.start ();
      }
      catch (IOException ioe) {
          println ("IOException: <" + ioe + ">");
          break;
      } 
      catch  (Exception e)  {
          println ("Exception: <" + e + ">");
          break;
      }
    }
  } // run


  /**
    * When a DataWorker makes the connection, it checks to see if 
    * there is room on the server for it. 
    * We synchronize the method to avoid any problems with multiple 
    * clients interfering with each other.
   **/
  public synchronized boolean clientPermit () {
    if (fWorkerList.size () < fMaxClients__)
        return true;
    else
        return false;
  }

  /**
    * A DataWorker will set up the connection with the client. If it
    * decides that the conditions are OK, then it will invoke this 
    * method so that the parent server will add the worker to its 
    * list.
    * We synchronize the method to avoid any problems with multiple 
    * clients interfering with each other.
   **/
  public synchronized void clientConnected (DataWorker worker) {
    fWorkerList.add (worker);
    fClientCounter__++;
  }

  /**
    * When a client disconnects, the DataWorker object will
    * call back to this method to remove itself from the list
    * of workers. 
    * We synchronize the method to avoid any problems with multiple 
    * clients interfering with each other.
   **/
  public synchronized void clientDisconnected (String user, 
                                               DataWorker worker) {
    println ("Client: "+user+" disconneced");
    fWorkerList.remove (worker);
    fClientCounter__--;
  } 

  /**
    * This "helper method" makes a menu item and then
    * registers this object as a listener to it.
   **/
  private JMenuItem makeMenuItem (String name){
    JMenuItem m = new JMenuItem ( name );
    m.addActionListener ( this );
    return m;
  }

  /** Utility method to send messages to the text area. **/
  public void println (String str){
     fTextArea.append (str +"\n");
     repaint ();
  }
} // class DataServer 

/*
 * DataWorker.java
 *
 * Created on 17 luty 2008, 08:49
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package socketserver;

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

/**
  * DataServer uses an instance of this class to service
  * a DataClient connection. It waits for a data request
  *  and then obtains and sends the data.
  *
  * In actual use, the data could, for example be obtained from a
  * diskfile updated by a DAQ system. Here, though, we simulate data 
  * taking by generating an array of random numberfDataServer.
**/
public class DataWorker extends Thread
{

  // The parent server.
  DataServer fDataServer;

  // Name of the client
  String fUser;

  // Connection to the client.
  Socket fSocket;

  // I/O streams to the client
  InputStream  fNetInputStream;
  OutputStream fNetOutputStream;

  // Wrapped I/O streams

  BufferedReader   fNetInputReader;
  DataOutputStream fDataOutputStream;

  PrintWriter fPrintWriter;

  boolean fKeepRunning = true ;

  // Data production:
  // Smear each data channel with the following std.dev
  // for the Gaussian.
  double [] fStdDev = {10.0, 20.0, 30.0, 40.0, 50.0, 60.0};
  Random fRan;

  /**
    *  Pass reference to the owner DataServer and the
    *  index number for this DataSender instance.
   **/
  public DataWorker (DataServer s, Socket socket) {
    fDataServer = s;
    fSocket = socket;

    // Random number generator need for dummy data creation.
    fRan = new Random ();
  }

  /**  Send data to the client. **/
  public void run  () {

    // If setup fails, end thread processing.
    if  (!serviceSetup ()) return;

    fDataServer.println (
      "Client connection and login OK - Begin service...");

    // Lower tpriority to give main parent and
    // other threads some processor time.
    setPriority (MIN_PRIORITY);

    String client_last_msg = "";

    // Begin the loop for communicating with the client.
    while  (fKeepRunning){

      // Read a request from the DataClient
      String client_msg = readNetInputLine ();
      if (client_msg == null ) break;

      // Only print message if it changes. Avoids printing same
      // message for each data set.
      if  ( !client_msg.equals (client_last_msg))
          fDataServer.println ("Message from " + fUser + ": " 
                                + client_msg);
      client_last_msg = client_msg;

      // Could interpret the request and do something accordingly
      // but here we will just send a set of data values.


      // Send the number of data values.
      try {
        writeNetOutputInt (DataServer.fNumDataVals__);
      }
      catch  (IOException e) {
        break;
      }

      // Creat dummy data values and send them to the DataClient.
      for (int i=0; i< DataServer.fNumDataVals__; i++){

        // Select that range of Gaussian widths for the data
        // values for each channel of the data set. Add an offset
        // to get most negative values above zero.
        int i_std_dev = i%6;
        double dat = 3.0*fStdDev[i_std_dev] +
                     fStdDev[i_std_dev] * fRan.nextGaussian ();
        if  (dat < 0.0) dat = 0.0;

        // Pass only integer values;
        int idat =  (int) dat;
        try {
          writeNetOutputInt (idat);
        }
        catch  (IOException e) {
          break;
        }
      }
    }

    // Send message back to the text area in the frame.
    fDataServer.println (fUser + " has disconnected.");

    // Do any other tasks for ending the worker.
    signOff ();

  } // run

  /**
    * Set up the connection to the client. This requires obtaining the
    * IO streams, carrying out the login prototcol, and then starting
    * a DataWorker thread to tend to the client. 
    *
    * The bookkeeping code is a bit messy because we check both reads
    * and writes for errors in case the connection breaks down. 
    *
    * The reads catch their own IOExceptions and return a null, while
    * string writes use a PrintWriter that doesn't throw IOException. So
    * we use the checkError () method and throw it ourselvefDataServer. 
   **/
  public boolean serviceSetup ()  {

    fDataServer.println ("Client setup...");

    // First get the in/out streams from the socket to the client
    try{
      fNetInputStream  = fSocket.getInputStream ();
      fNetOutputStream = fSocket.getOutputStream ();
    }
    catch  (IOException e){
      fDataServer.println ("Unable to get input/output streams");
      return false;
    }

    // Create a PrintWriter class for sending text to the client.
    // The writeNetOutputLine method will use this class.
    try{

        fPrintWriter = new PrintWriter (
           new OutputStreamWriter (fNetOutputStream, "8859_1"), true );
    }
    catch (Exception e) {
        fDataServer.println ("Fails to open PrintWriter to client!");
        return false;
    }

    // Check if the server has room for this client.
    // If not, then send a message to this client to tell it 
    // the bad news.
    if  ( !fDataServer.clientPermit () ) {
      try{

        String msg= "Sorry, We've reached maximum of clients";
        writeNetOutputLine (msg);
        fDataServer.println (msg);
        return false;

      }
      catch  (IOException e){
        fDataServer.println ("Connection fails during login");
        return false;
      }
    }

    // Get a DataInputStream wrapper so we can use its 
    // readLine () methods.
    fNetInputReader  =
     new BufferedReader (new InputStreamReader (fNetInputStream));

    // Do a simple login protocol. Send a request for the users name.
    // Note a password check could be added here.
    try{
      writeNetOutputLine ( "Username: ");
    }
    catch (IOException e){
      fDataServer.println ("Connection fails during login");
      return false;
    }

    // Read the user name.
    fUser = readNetInputLine ();
    if  (fUser == null ) {
        fDataServer.println ("Connection fails during login");
        return false;
    }

     // Send a message that the login is OK.
    try{
      writeNetOutputLine ("Login successful");
      fDataServer.println ("Login successful for " + fUser);
    } catch  (IOException e){
      fDataServer.println ("Connection fails during login for " 
                            + fUser);
      return false;
    }
    fDataServer.println (fUser + " connected! ");
    fDataServer.println (fSocket.toString ());


    // The login is successful so now create a DataWorker to
    // service this client. Pass it an ID number
    fDataServer.clientConnected (this);

    // Get a data output stream for writing numerical data to the client
    fDataOutputStream = new DataOutputStream (fNetOutputStream);

    return true;
  } // serviceSetup

  /** Whenever this client disconnects tell the parent. **/
  public void signOff () {
    try{
        fSocket.close ();
    }catch  (Exception e){
        fDataServer.println ("Socket close exception for " + fUser);
    }
    fDataServer.clientDisconnected (fUser,this);
  } // signOff

  /** Utility method to read a whole text line.**/
  String readNetInputLine ()  {
    try {
      return fNetInputReader.readLine ();
    }
    catch  (IOException e){
      return null;
    }
  } // readNetInputLine

  /**
    * Output is wrapped with a PrintWriter, which doesn't throw
    * IOException. So we invoke the checkError() method and then
    * throw an exception if it detects an error.
   **/
  void writeNetOutputLine (String string)
                  throws IOException {

    fPrintWriter.println (string);
    if  ( fPrintWriter.checkError ()) throw  (new IOException ());

    fPrintWriter.flush ();
    if  ( fPrintWriter.checkError ()) throw  (new IOException ());
  } // writeNetOutputLine

  /** Utility to write integer values to the output stream. **/
  void writeNetOutputInt (int i)
      throws IOException {
    fDataOutputStream.writeInt (i);
    fDataOutputStream.flush ();
  } // writeNetOutputInt

  /** Utility to write float values to the output stream.**/
  void writeNetOutputFloat (float f)
      throws IOException {
    fDataOutputStream.writeFloat (f);
    fDataOutputStream.flush ();
  } // writeNetOutputFloat

} // class DataWorker