First time here? Please, start at the beginning.

uChat Source

Same as in Part 2.1, except with the complicated parts still in (still no comments there, sorry):
//lots of imports here

import java.util.*; // name-IP address map
import java.net.*;  // several Socket classes and InetAddress
import java.io.*;   // IOException and a couple *Writers

import java.awt.*;       // GUI elements
import java.awt.event.*; // GUI event handlers

import java.util.regex.*; // regular expression library, used to prevent a minor exploit

// changed the class/file name, class names should usually start with a capital
// subclass java.awt.Frame (a window) so we can be displayed on-screen
// implement WindowListener and ActionListener (from java.awt.event) so we can handle our own events
// implement java.lang.Runnable so we can listen for incoming messages in a seperate thread
//	(I've reciently realized multi-threading wasn't actually necessary...)
public class uChat extends Frame implements WindowListener, ActionListener, Runnable {

// NOTE: within source code, I tend to use "us", "we", "our", etc. to refer to the class/program and it's actions. Don't ask why, I don't know.

 // network-related vars
 InetAddress addrs;   // broadcast IP address, declared below
 int port = 12121;    // communication port number
 DatagramSocket sock; // multi-purpose UDP socket

 // GUI elements
 TextField name;  // top text field
 TextArea disp;   // main text area
 TextField input; // bottom text field
 Button send;     // obvious 
 Button connect;  // not really "connecting" but close enough

 Map lookupTable; // keeps track of which user(name) is at which IP address

// constructor, throws any Exception because I was too lazy to list each one that could happen
// NOT A BEST PRACTICE
public uChat() throws Exception {
 super("uChat"); // call superclass constructor: Frame(String s); sets window title

 // retrieve broadcast IP address, method may (but shouldn't with a numerical address) throw UnknownHostException
 addrs = InetAddress.getByName("255.255.255.255");

 // initialze Socket, and bind to the port
 sock = new DatagramSocket(port);

 // initialize the lookup table as a HashMap
 lookupTable = new HashMap();

 // set up GUI elements
 disp = new TextArea("",0,0,TextArea.SCROLLBARS_VERTICAL_ONLY); // no starting text, default height and width, scrollbar display policy
 input = new TextField();
 name = new TextField();
 send = new Button("Send");    // buttons need
 connect = new Button("Join"); // labels

 // prevent editing the message display box
 disp.setEditable(false);

 //GUI layout design can be pretty hairy

 // a java.awt.Panel is a simple container
 // constructor accept java.awt.LayoutManager as an arg, BorderLayout is a subtype
 Panel pnl1 = new Panel(new BorderLayout());

 // add an anonymous label, followed by our name field and connect button
 pnl1.add(new Label("Name:"), BorderLayout.WEST);
 pnl1.add(name, BorderLayout.CENTER);
 pnl1.add(connect, BorderLayout.EAST);

 // the lower Panel, with the input field and send button
 Panel pnl2 = new Panel(new BorderLayout());
 pnl2.add(input,BorderLayout.CENTER);
 pnl2.add(send,BorderLayout.EAST);

 // set the our layout and add the panels and display area
 setLayout(new BorderLayout());
 add(pnl1, BorderLayout.NORTH);
 add(disp, BorderLayout.CENTER);
 add(pnl2, BorderLayout.SOUTH);

 // set up a new thread and pass us as the Runnable
 Thread t = new Thread(this);
 // start the thread
 t.start();

 // add us as ActionListener for the main GUI elements
 send.addActionListener(this);
 connect.addActionListener(this);
 input.addActionListener(this);
 name.addActionListener(this);

 // add us as a WindowListener for ourself
 addWindowListener(this);

 // display the main window
 setVisible(true);
} // end of constructor

// override Frame.setVisible(boolean b) to add some functionallity
public void setVisible(boolean b) {
 super.setVisible(b); // call Frame.setVisible() to actually show/hide the window
 if(b) { // if we're being shown...
  pack(); // ... shrink/grow the window so that each element gets it's 'prefferred size"
  Dimension scrn = Toolkit.getDefaultToolkit().getScreenSize(); // get the screen's size (in pixels) and...
  // ... calculater a point exactly our size up and left from the bottom corner of the screen and move us there
  setLocation((scrn.width-this.getSize().width)/1,(scrn.height-this.getSize().height)/1);
  // the result of this is causing the window to appear in the lower-right corner of the screen
 }
} // end of setVisible

public static void main(String arg[]) { // this is actually a stub,
 // call the constructor (from inside a try block), but don't keep a reference to the created object
 // (it'll dislay itself, so we don't need to)
 try { new uChat(); }
 // catch any Exception that pops up and pass it to the handle(Excpetion) method (it's way below)
 catch(Exception e) { handle(e); }
} // end of main

// a beast of a method here, this is from the ActionListener interface
// it's called any time an ActionEvent occurs, like clicking a button or pressing enter in a text field
// it's pretty convoluted. I'll devote an entire post (or more) to explaining it later.
public void actionPerformed(ActionEvent evt) {
 if(name.getText().trim().equals("")) {return;}
 byte[] data= null;
 try {
  if(evt.getSource() == input || evt.getSource() == send) {
   if(input.getText().trim().equals("")) {return;}
   if(!connect.getLabel().equals("Leave")) {return;}
   { // a little pre-processing to prevent old-skool hax (exploiting text wrapping)
     // replaces every instance of 3+ space chars with 2 space chars
    input.setText(
	Pattern.compile(" {3,}")
	.matcher(input.getText().trim())
	.replaceAll("  ")			); //end of setText()
   }
   data = "SAY:".concat(input.getText()).getBytes("ISO-8859-1");
  }
  else if(evt.getSource() == connect || evt.getSource() == name) {
   if(connect.getLabel() == "Join") {
    name.setEditable(false);
    data = "JOIN".concat(name.getText()).getBytes("ISO-8859-1");
    connect.setLabel("Leave");
    input.requestFocus();
   }
   else if (connect.getLabel() == "Leave") {
    data = "LEAV".concat(name.getText()).getBytes("ISO-8859-1");
    connect.setLabel("Join");
    name.setEditable(true);    
   }
   else { return; /* shouldn't happen*/ }
  }
  else { return; /* shouldn't happen*/ }

  DatagramPacket out = new DatagramPacket(data, data.length,addrs,port);
  sock.send(out);
 } catch(Exception e) { handle(e); }
 input.setText("");
}

// a "flag" to tell the listener thread to stop
boolean die = false;

// the run() method is from the java.lang.Runnable interface
// this code will run in a seperate thread
// it listens for incoming UDP packets, interprets them, and updated the display text area accordingly
// it's fairly complicated to, and will get it's own post later
public void run() {
 while(!die) {
  byte[] data = new byte[1024];
  DatagramPacket in = new DatagramPacket(data, data.length);
  try {
   sock.receive(in);

   String tmpIN = new String(data, "ISO-8859-1");

   if(tmpIN.startsWith("SAY:")) {
    String who = lookupTable.get(in.getAddress());
    disp.append(who+": "+new String(data, 4, in.getLength()-4, "ISO-8859-1")+"\n");
    toFront();
    Toolkit.getDefaultToolkit().beep();
   }
   else if(tmpIN.startsWith("JOIN")) {
    InetAddress tmp = in.getAddress();
    String tmpS = new String(data, 4,in.getLength()-4, "ISO-8859-1");
    lookupTable.put(tmp, tmpS);
    disp.append(tmpS+'('+tmp.getHostAddress()+')'+" has joined\n");

    byte[] send = "HERE".concat(name.getText()).getBytes("ISO-8859-1");
    DatagramPacket outbound = new DatagramPacket(send, send.length, tmp, port);
    sock.send(outbound);
   }
   else if(tmpIN.startsWith("HERE")) {
    InetAddress tmp = in.getAddress();
    String tmpS = new String(data, 4,in.getLength()-4, "ISO-8859-1");
    lookupTable.put(tmp, tmpS);

    if(!tmpS.equals(name.getText())) { //needed to suppress "you are here"
     disp.append(tmpS+'('+tmp.getHostAddress()+')'+" is here\n");
    }
   }
   else if(tmpIN.startsWith("LEAV")) {
    InetAddress tmp = in.getAddress();
    String tmpS = new String(data, 4,in.getLength()-4, "ISO-8859-1");
    lookupTable.remove(tmp);
    disp.append(tmpS+'('+tmp.getHostAddress()+')'+" has left\n");
   }
   else { /* unknown, silently ignore */ }
  } catch(Exception e) { handle(e); }
 }
 try {
  sock.close();
 } catch(Exception e) { handle(e); }
}

// from the WindowListener interface, this method is called when the user asks the window to close by clicking it's exit button
// it does a little clean-up: announcing to other uChaters that we're leaving
public void windowClosing(WindowEvent evt) {
 setVisible(false); // hide the window

 // change the flag, telling the listener thread to stop
 die = true;

 // if we're connected
 if(connect.getLabel() == "Join") { //only send LEAV if we're JOINed
  try {
   //build and send packet announcing that we're leaving
   byte[] data = "LEAV".concat(name.getText()).getBytes("ISO-8859-1");
   DatagramPacket out = new DatagramPacket(data, data.length,addrs,port);
   sock.send(out);
  } catch(Exception e) { handle(e); } // if an exception occurs, pass it to handle(Exception)
 }
 // this frees all resources associated with this window, allowing the event-processing threads to stop
 dispose();

 // this method explicitly tells the JMV to exit, but isn't needed as all threads should be ready to stop
// System.exit(0);
}

// these are also part of the WindowListner interface, but aren't used by this program
public void windowOpened(WindowEvent evt) { }
public void windowClosed(WindowEvent evt) { }
public void windowIconified(WindowEvent evt) { }
public void windowDeiconified(WindowEvent evt) { }
public void windowActivated(WindowEvent evt) { }
public void windowDeactivated(WindowEvent evt) { }

// all purpose Exception handler.
// NOT REMOTELY A BEST PRACTICE but sufficient for this small program
// static, so it can be accessed from main()
public static void handle(Exception e) {
 // set up a pair of writers to convert the Excpetion's stack trace to a string
 StringWriter sb = new StringWriter();
 PrintWriter out = new PrintWriter(sb);

 // dump the stack trace to the above writer and System.out (aka STDOUT, aka the terminal window)
 e.printStackTrace(out);
 e.printStackTrace(System.out);

 // make a new text area, default sizes, with the stack trace (via the writers) as its content
 TextArea disp = new TextArea(sb.getBuffer().toString());

 // simple frame, set its layout
 Frame f0 = new Frame("Oops");
 f0.setLayout(new BorderLayout());

 // anonymous inner class as event listener for the frame, more in another post
 f0.addWindowListener(new WindowAdapter() {
  public void windowClosing(WindowEvent evt) { ((Window)evt.getSource()).dispose(); }
 });

 // add a label, then add the text area from above
 f0.add(new Label("Oops, something went wrong. uC may or may not still work."), BorderLayout.NORTH);
 f0.add(disp, BorderLayout.CENTER);

 // pack() and show the frame
 f0.pack();
 f0.setVisible(true);
 // if you crash uChat (hint: start another instance while one's running), you'll see this method
 // in action. it's not a pretty error message, and would tell your average user absolutely nothing.
 // to be honest, this was 75% to shut the compiler up about "uncaught exceptions", 25% for debugging
}

}

No comments:

Post a Comment