Comp202: Principles of Object-Oriented Programming II
Fall 2007 -- Project 4:  Chat Program    


Under construction!

package chat

Individual implementations should be placed in a different package with a name corresponding to its creator's NetID name.

 

 

 

General Chat Program Specifications:

  1. Auto-connect:  when a client connects to your program, it must automatically create the connection back to that client
  2. Inter-operability:  Must work with everyone else's chat program
  3. Group chat:  Must be able to handle more than two people in a chat conversation.
  4. Private chat:  Must be able to hold conversations where no more people are allowed to join in.
  5. Multiple simultaneous conversations:  Must be able to have more than one conversations at the same time, both public and private

 

Milestone 1

All of the above functionality in the General Chat Program Specifications, except

  1. Private chat
  2. Leaving a group chat

 

Milestone 2:

All of the functionality in the General Chat Program Specifications, with the following modifications:

  1. All communications through the IChat object must use a single method utilizing an extended visitor
  2. Leaving a chat group must be implemented such that all parties in the group are notified that the one user is leaving.
  3. Private chat is optional
  4. Must be able to gracefully handle any request from a remote client through the extended visitor interface.   For instance, if private chat is not supported but a request for private chat is received, then it should not crash the system or stop it from functioning normally.

 

Comparing Deserialized Objects

In a networked application, we are often serializing objects and sending around the globe.   Sometimes we need to compare those objects against each other.  But the problem is that the equals() method, inherited from Object, by default, uses a direct comparison of the hashcode of the object, which in turn is derived from its memory location.  This means that two deserialized instances of the same original object will return false if compared using equals()!

What we must do is to override the equals() method and the hashCode() method of the original object so they will return true if deserialized instances of the same original object are compared.  (The Java standard is that if two objects are equal, then they must return the same hash code.)

Technique #1:  Objects are equal if all their fields return values that are equal

Here is some example code for an IPerson implementation where there are two available fields, the name and IP address:

  /**
   * IPersons holding the same values will be equal
   * regardless of implementation
   */
  public boolean equals(Object o) {
    if(null != o) {
      if(o instanceof IPerson) {
        return ((IPerson)o).getName().equals(getName()) 
          && ((IPerson)o).getIPAddress().equals(getIPAddress());
      }
    }
    return false;
  }
  
  /**
   * IPersons holding the same values will have the
   * same hashcode, regardless of implementation
   */
  public int hashCode() {
    return (getName()+getIPAddress()).hashCode();
  }
  

NOTE: The above code utilizes the fact that the String class in java overrides the default hashCode method inherited from Object to return a hash code that is determined purely and completely by the letters in the String and the order in which they are in the String .    Therefore different instances of the same string value will always be equal and have the same hash code.

Pros:

Cons:

 

Technique #2: Include a Universally Unique Identifier (UUID)

A Universally Unique Identifier (also called a Globally Unique Identifier or GUID in other systems) is a 128-bit value (4x size of an int) that is guaranteed (to within a very small probability) of being unique no matter who creates it or when.   This enables us to give an instance a unique identifying value that will be the same even after it has been transmitted across the net.   Most large enterprise systems use this technique because it scales well.

  /**
   * Universally Unique IDentifier, used to create
   * an ID value that is unique across different machines.
   */
  private java.util.UUID uuid = java.util.UUID.randomUUID();
  
  /**
   * Insures that unmarshalled copies of this object will 
   * still be equal to the original.
   * Two rooms with the same values may still not be equal.
   */
  public boolean equals(Object o){
    if(null != o) {
      if( o instanceof Room) {
        return uuid.equals(((Room)o).uuid);
      }
    }  
    return false;
  }
     
  /**
   * Insures that unmarshalled copies still have the 
   * same hashcode as the original
   * Two rooms with the same values may still 
   * not have the same hashcode.
   */
  public int hashCode() {
    return uuid.hashCode();
  }

 

Pros:

Cons:

 


Representing Sets and Dictionaries

java.util.Set<E> 

Set<E> is an interface that represents a set of objects  (See http://java.sun.com/javase/6/docs/api/java/util/Set.html)   It is a form of restricted access container in that it allows only a small set of methods for interacting with the object contained within it.  Here are some of the more frequently used methods:

Note that the comparison operations needed for the above methods uses the equals method of the objects being stored.   Therefore it is very important that the equals method work properly for any objects stored in a set.   This becomes particularly true when those objects are remote or serialized/deserialized objects.

To process all the elements in the set, use the for-each syntax of the for-loop:

Set<MyClass> s;

for(MyClass x : s) {
   // process an element called "x".
}
This is essentially a mapping process.

Note that you cannot modify the set while the loop is running.  Therefore, the following code is illegal:
Set<MyClass> s;
MyClass other;

for(MyClass x : s) {
  // some code involving x 
  s.remove(x);  // This mutates the list while the for-loop is processing it.
  // some more code involving x
}

Instead, you should write the following:

Set<MyClass> s;
MyClass other;

if(s.contains(other) {
  // some code involving other
  s.remove(other);
  // some more code involving other
}

Since Set<E> is an interface, you may use any implementation you choose.   A convenient and fast implementation is java.util.HashSet<E>.   This need only appear once in your code, when you first initialize the empty set:

Set<MyClass> aSet = new HashSet<MyClass>();

 

java.util.Map<K,V>

Map<K,V> (See http://java.sun.com/javase/6/docs/api/java/util/Map.html) is an interface that represents a "dictionary" that maps keys of type KK to a value of type V.   A dictionary allows one to store values that are associated with a key and then to later retrieve those values just by supplying the key value.   This is very useful in many situations, such as relating a set of chat objects (the value) with a given room (the key).

The of the more frequently used methods of of Map<K,V> are:

Since Map<K,V> is an interface, you may use any implementation you choose.   A convenient and fast implementation is java.util.HashMap<E>.   This need only appear once in your code, when you first initialize the empty set:

MaP<MyKey, MyValue> aSet = new HashMap<MyKey, MyValue>();


Comparing Remote Objects

The problem with Remote objects, where a stub gets sent from the server to the client, is that unlike serialized objects, an entire, fully functional copy of the original instance does NOT get deserialized and instantiated on the client.  

The deserialized stub object forwards all methods that throw RemoteException to the skeleton object on the server, which then forwards the method call to the original instantiation of the object.   Since the stub is an Object instance, it does locally retain all those abilities inherited from Object.

In particular, the equals() and hashCode() methods, which are inherited from Object are problematic.   They cannot be overridden to be remote calls because that would entail adding the ability to throw the RemoteException exception, but the rules of Java forbid overriding a method to throw an exception.  By default, as inherited from Object, equals() compares the hashCode of the instances being compared.  But by default, the hashCode() method simply returns a number based upon the memory location of that object.

Thus if one were to use equals() to compare any two remote objects, the result would always be false because two stubs are fundamentally two different instances stored at different memory locations, even if they were originally generated from the same instance on the server. 

The problem here is that our stub, our "proxy" in design pattern-speak, is too dumb.  We need to send a more intelligent proxy.   The solution is to use the Decorator Design Pattern, which wraps the original dumb stub and provides more functionality, i.e. in the equals() and hashCode() methods.

The trick is to separate the Remote interface from the interface that defines the remote behavior of the stub/proxy (IChat below).  That is, when sending the stub, don't send the stub made by exporting the Remote object, but rather, send a non-remote instance of our own, custom proxy class.

UIML diagram of smart proxy class

In the above example, we have a class Chat, that would normally implement the IChat interface, and via that interface, inherit from the Remote interface.   Instead, above, Chat implements the IChat_Impl interface, with combines the IChat and Remote interfaces.   And yes, IChat_Impl adds no new functionality.   It's only purpose is to create the direct, in-line hierarchy from Remote to Chat that includes IChat as required for the proper functioning of the stub exporting mechanism of UnicastRemoteObject.exportObject()

 

Below is the code for the smart stub itself.   All the calls that throw RemoteException are simply extensions of those found on the dumb stub, so hence, they simply forward the method call to the original dumb stub.   The equals() and hashCode() methods use the UUID method from above.   In theory those methods could use any method they believed was right, including dispatching the call across to be processed back on the server.  Here, all equality processing takes place locally on the client, to maximize speed and to reduce the overall network traffic.

package swong.model;
import java.rmi.*;
import chat.*;

/**
 * A "smart proxy" that wraps the normal "dumb" stub of a Chat 
 * instance.   All remote operations are simply forwarded to the 
 * composed stub.  The equals() and hashCode() methods are overridden
 * to make this instance of the smart proxy unique, no matter 
 * how many times it has been serialized and sent over to another 
 * machine.  Any deserialization of this stub will compare true
 * with any other deserialization of this stub.
 */
class Chat_Proxy implements IChat {
  /**
   * Needed for cross-version compatibility of deserialization
   */
  private static final long serialVersionUID = 200710262L;
  
  /**
   * The composed Chat stub object
   */
  private IChat c_impl_stub;
  
  /**
   * Constructor for the class
   * @param c_impl_stub The regular, "dumb" stub of a Chat object.
   */
  Chat_Proxy(IChat c_impl_stub) {
    this.c_impl_stub = c_impl_stub;
  }
  
  /**
   * Simply delegates to the c_impl_stub object
   */
  public void append(String s) throws RemoteException {
    System.out.println("Chat_Proxy.append: "+s);
    c_impl_stub.append(s);
  }

  /**
   * Simply delegates to the c_impl_stub object
   */
  public String joinRoom(IChat me) throws RemoteException {
    return c_impl_stub.joinRoom(me);
  }

  /**
   * Simply delegates to the c_impl_stub object
   */
  public IPerson getPerson() throws RemoteException {
    return c_impl_stub.getPerson();
  }
  
  /**
   * Unique id value that identifies this instance of the proxy
   * object
   */
  private java.util.UUID uuid = java.util.UUID.randomUUID();
  
  /**
   * Compares the UUID of this instance with the UUID of the other
   * instance.
   * @param o the other object to compare with
   * @return true if both objects are IChat_Proxy instances 
   * and both have the same uuid value.
   */
  public boolean equals(Object o){
    if( o instanceof Chat_Proxy) {
      return uuid.equals(((Chat_Proxy)o).uuid);
    }
    else {   
      return false;
    }
  }
  
  /**
   * Returns the hashCode of the UUID
   */
  public int hashCode() {
    return uuid.hashCode();
  }
}
 

Here is an example of a factory method that is employed to encapsualate the creation of the smart proxy.  Since the return value of the factory method is an IChat-compliant stub instance, the return value can be used wherever the dumb stub was used originally.   One only needs to replace the creation of the original Chat instance and its dumb stub wherever they is found with a call to this factory method.

  /**
   * Create a "smart stub" of a Chat object that overrides 
   * equals() and hashCode() such that desrialized versions 
   * of the same stub will always compare to be equal.
   */
  public IChat makeChatStub(Chat c_impl) {
    try {
      // Make the dumb stub 
      IChat c_impl_stub = (IChat) UnicastRemoteObject.exportObject(c_impl, getNextPort());      
      // Make the smart stub
      IChat c = new Chat_Proxy(c_impl_stub);  
      
      IChat c = new Chat_Proxy(c_impl_stub);  // Make the smart stub
      view.append("ChatModel.makeChatStub: c = "+c+"\n");
      return c; // return the smart stub
    }
    catch(RemoteException e) {
      view.append("Exception creating chat stub: "+e+"\n");
      // uses another similar routine that 
      // makes a null object instance of IChat
      return makeNullChatStub(); 
    }
  }
 

The above code assumes that there is a null object instance of IChat that gives some well-defined behavior for use when a valid instance of IChat is required but unattainable.

More information on using smart proxies can be found at http://www.javaworld.com/javaworld/jw-11-2000/jw-1110-smartproxy.html?page=3

 

 


Last Revised Thursday, 03-Jun-2010 09:52:31 CDT

2007 Stephen Wong and Dung Nguyen