Add two facilities for collaboration to an otherwise humdrum applet
I’m sure most of you have seen magnetic poetry, that refrigerator-magnet word game whereby you arrange individual word magnets into poetic musings. You may have even unearthed an applet version of this game during your online adventures. The applet presents a selection of words in the form of electromagnetic tiles that you drag around with your mouse.
Although the poetry game is quite fun, the applet actually represents outdated, mid-90s technology. To bring the applet up to date we’re going to add a collaborative touch. In fact, this month we’ll add two facilities for collaboration.
Note, though, that our creation requires the most robust of Java virtual machines. Our tests indicate the poetry applet works for the following platforms:
- AppletViewer on Solaris
- HotJava on Solaris
- Microsoft Internet Explorer 4.0 on Solaris
- Microsoft Internet Explorer 4.0 on Windows 95/NT
- Netscape Communicator 4.04 (with JDK 1.1 support) on Solaris
- Netscape Communicator 4.04 (with JDK 1.1 support) Windows 95/NT
Click here to view the applet.
Offline collaboration
You’ve whiled away your day at work, channeling all your energy into a masterful outpouring of poetic expression. Now, how do you share this feat with the world? You can write it down and print it out, or — with our New TechnologyTM — you can submit your work to the poetry servlet!
The poetry servlet is a simple database of submitted poems; you can either submit your own poem or browse through the poems submitted by other creative geniuses.
Online collaboration
Simply being able to read what someone else has written often is not enough. Sometimes two heads work better than one. For this multiple-poet situation, we want realtime collaboration so that many people can come together and produce something better than any one of them could have done alone. Again, our New TechnologyTM can help!
Using the distributed list classes that we introduced with the earlier whiteboard articles, we can add online collaboration to the poetry applet. By storing the state of the poetry board in a distributed list, we can truly share the poetic experience.
The collaborative poetry framework
Our collaborative poetry application naturally is divided into client-side and server-side components. Let’s take a look at what each of these entails.
Client side
The client side of the poetry application features one main class, PoetryBoard
, which implements the click-and-drag poetry board — the basic user interface through which poetry is created.
Every tile on this board is represented by the Tile
class. Tile
is a serializable stateholder that stores a word plus its color and location, and provides the actual tile drawing code. We’re making the class serializable because we need to use the object streams for communication purposes.
Finally, the actual poetry applet is implemented with the PoetryApplet
class. This creates and controls a PoetryBoard
, which provides client-side networking with the poetry servlets, and the user interface facility for engaging online collaboration, and loading and saving poems.
Server side
The server side of the poetry application consists of two servlets. The first, PoetryServlet
, is the poem database, which stores submitted poems in serialized form in a persistent hashtable. This servlet provides three main services, as follows, it:
- Obtains a list of the titles of submitted poems
- Downloads a named poem
- Allows a poem to be uploaded into persistent storage
The other realtime aspect of the server side is implemented with the ServletListServlet
class from January’s column,”Networking our whiteboard with servlets.” This is a distributed list class that allows clients to collaborate in realtime by using an abstract list class; details of the networking are hidden from the clients behind a simple Vector
-like data structure.
If you haven’t already read the columns pertaining to the distributed list, I suggest you do so now (see the “Previous Step by Step columns” portion of our Resources for links to these columns).
How it works
The initial poetry screen presents you with a number of tiles that you can drag around and arrange into poems.
Selecting the Shared checkbox enables collaboration mode. When you enable this option, the poetry applet connects to a central server and displays a shared poem. You can modify the poem as before, but now you will see changes that other users make in realtime — just as they will see yours. Disabling this option returns the applet to non-networked mode with a local copy of the shared poem.
Clicking the Load button displays a list of “published” poem titles. You can download any poem you want and change it as you like.
Finally, Save allows you to save your poem. Make sure you provide your poem with a fitting title before you submit it to the eyes of the world.
The implementation
Due to space limitations, we will only discuss the code in outline. To get into the real details you’ll need to look at the raw source code, which you can download from the
section.
Class Tile
This class is a simple serializable storage box for one word of a poem.
class Tile implements Cloneable, Serializable {
static final int PAD_X = 3, PAD_Y = 2;
}
Our Tile
class should be both Cloneable and Serializable, which allows us to use the clone()
method to obtain a shallow duplicate of a Tile
, and the object streams to serialize it.
String word;
int x, y;
Color background, foreground;
transient Rectangle bounds;
transient int width, height, ascent;
The basic state of a tile is the word itself, its location, and its foreground and background colors. In addition, however, we will include the bounds of the tile (its on-screen size) and its font ascent (the value used for drawing it). We mark these extra fields as transient
to specify that they will not be transmitted by the object streams. Instead, every client will manually fill in these fields when a tile is downloaded.
This detail is quite important to note. When dealing with issues such as the exact size of a font, you cannot rely on the value for one user being valid for another. Exact details of the size of a font will vary from computer to computer. In some places, the word “poetry” might be 30 pixels wide, whereas in others it might be 40. For this reason, we cannot serialize the width or height of a tile and expect the values to be universally useful. Instead, we just serialize the bare minimum of necessary information and allow clients to fill in the rest with appropriate, local values.
Tile (String word, Color bg, Color fg) { ... }
Our constructor fills in the basic specified tile information.
Rectangle getBounds (Component parent) { ... }
The getBounds()
method returns the bounds of this tile. If the bounds have not already been calculated, they are computed based on the font of the specified parent component. In this way we can ensure that the correct values will be filled in for every client.
Tile getTranslated (int deltaX, int deltaY) { ... }
The getTranslated()
method returns a clone of the tile, translated by the specified offset. Because the distributed list requires that all elements stored in it be immutable (it cannot easily determine if an object has been changed internally), we must replace a tile rather than modifying it in-place: Whenever an element is changed, we must replace it in the list with a completely new element.
void paint (Graphics g, boolean selected) { ... }
The paint()
method draws the tile at its current location in the specified graphics context, g
. The selected
variable indicates if the tile is currently selected and should highlight itself.
Class PoetryBoard
The PoetryBoard
class, a simple non-networked 1.1 AWT component, implements the actual drag-and-drop poetry user interface. Although we don’t use any new features of JDK 1.1, it is simpler for our entire framework to use version 1.1: The new event model is easier to work with than that of JDK 1.0.2, and it is much more convenient to be able to develop within a pure 1.1 framework then to attempt backwards compatibility with what is essentially an obsolete version of Java.
public class PoetryBoard extends Canvas implements UpdateListener {
static final Color TILE_BG = new Color (0x009999),
TILE_FG = new Color (0x663300);
}
Here we extend Canvas
and implement UpdateListener
. In a graphic-intensive application like ours, it is more efficient to use Canvas
than it would be to use the lightweight Component
superclass. Because lightweight components are all treated as transparent, when one component calls repaint()
all overlaid components are repainted, up to the parent native component; however, when Canvas
calls repaint()
, only the one native component will be repainted. Our distributed list notifies us via the UpdateListener
class of asynchronous changes.
ObservableList tiles;
Tile selectedTile, newTile;
The current contents of the poetry board are stored in an ObservableList
. This is a generic Vector
-like data structure that supports either non-networked operations or networking through sockets, RMI, servlets, and even CORBA. When a tile is being dragged, we store the original tile in selectedTile
and the new, dragged tile in newTile
.
public PoetryBoard () { ... }
This constructor sets up the basic state of the poetry board and enables mouse events.
public void setTiles (ObservableList newTiles) {
tiles.removeUpdateListener (this);
selectedTile = null;
tiles = newTiles;
tiles.addUpdateListener (this);
repaint ();
}
We call the setTiles()
method with a new ObservableList
of tiles to use. We deregister for receiving update events from the old list and then register with this new list.
public void paint (Graphics g) {
Rectangle clip = g.getClipBounds ();
if (clip == null)
clip = new Rectangle (getSize ());
Enumeration elements = tiles.elements ();
...
while (elements.hasMoreElements ()) {
Tile tile = (Tile) elements.nextElement ();
if ((tile != selectedTile) && clip.intersects (tile.getBounds (this)))
tile.paint (g, false);
}
if ((newTile != null) && clip.intersects (newTile.getBounds (this)))
newTile.paint (g, true);
}
The paint()
method draws the poetry board. We iterate through the list of tiles, calling the paint()
method of each. For efficiency, we check that the graphics-clipping rectangle holds a tile before actually drawing it. If a tile is being dragged, we draw it in its new position at the very end.
Note that when we are determining the bounds of a tile, we pass this
as a parameter. If the tile has not yet determined its bounds, it will do so now, based upon our current font settings.
protected void processMouseEvent (MouseEvent mouseEvent) {
...
// locate newly selected tile
while (elements.hasMoreElements ()) {
Tile tile = (Tile) elements.nextElement ();
if (tile.getBounds (this).contains (x, y))
selectedTile = tile;
}
...
// replace a tile; networking is transparent
tiles.replaceElementAtEnd (selectedTile, newTile);
...
}
The processMouseEvent()
method is called when the user clicks or releases the mouse. On a click, we locate the newly selected tile and start the drag process; on a release, we actually move the tile from its old position to its new position in tiles
. If the datastructure is networked, the change will be transmitted to all other clients.
protected void processMouseMotionEvent (MouseEvent mouseEvent) {
...
// move tile and repaint
Tile movedTile = newTile.translate (x - oldX, y - oldY);
Rectangle area = newTile.getBounds ().union (movedTile.getBounds (this));
newTile = movedTile;
repaint (30, area.x, area.y, area.width, area.height);
...
}
The processMouseMotionEvent()
method is called when the user drags the mouse. We update the new tile position and then repaint the union of the tile’s old and new positions.
public void updateOccurred (UpdateEvent event) {
selectedTile = newTile = null;
repaint ();
...
}
The updateOccurred()
method is called when a direct or asynchronous change is made to our tiles list. We clear the tile currently being dragged and then call repaint()
to reflect any changes.
Class PoetryApplet
The PoetryApplet
class is the main user interface of the networked poetry applet. We create the various user-interface components and register to receive events appropriately, using a CardLayout
to flip among the various screens.
public class PoetryApplet extends Applet implements ActionListener, ItemListener { ... }
Our class is an Applet
that implements ActionListener
and ItemListener
to receive the appropriate user-interface events.
ObservableList localTiles;
ServletList servletTiles;
The current non-networked word list is stored in the localTiles
variable. If collaboration is enabled, servletTiles
holds a networked word list.
public void init () {
...
// download a word-list
localTiles = new ObservableList ();
URL url = new URL (getCodeBase (), "words.txt");
BufferedReader reader = new BufferedReader (
new InputStreamReader (url.openStream (), "latin1"));
String word;
while ((word = reader.readLine ()) != null) {
Tile tile = new Tile (word, PoetryBoard.TILE_BG, PoetryBoard.TILE_FG);
... compute random position
localTiles.addElement (tile.getTranslated (rndX, rndY));
}
in.close ();
poetry.setTiles (localTiles);
...
}
In the init()
method, we set up the user interface and configure the PoetryBoard
with a non-networked list containing words downloaded from a standard configuration file.
public void start () { ... }
When the user is in collaboration mode, start()
reconnects the networked list, if necessary.
public void stop () { ... }
Likewise, when the user is in collaboration mode, stop()
temporarily disconnects the networked list.
public void itemStateChanged (ItemEvent event) { ... }
The itemStateChanged()
method is called when the user toggles collaboration mode; we simply call one of the following two methods, startShared()
or startShared()
, as appropriate.
void startShared () {
...
// start networked mode
servletTiles = new ServletList (new URL (getCodeBase (), "/servlet/ServletListServlet"));
poetry.setTiles (servletTiles);
...
}
When the user engages collaboration mode, we simply replace the current word-list with a servlet-networked distributed list that contains a poem currently under construction. We use the ServletList
class from January, connecting to a ServletListServlet
on the server.
void stopShared () {
...
// stop networked mode; duplicate the poem
servletTiles.stop ();
localTiles = new ObservableList ();
Enumeration tiles = servletTiles.elements ();
while (tiles.hasMoreElements ())
localTiles.addElement (tiles.nextElement ());
poetry.setTiles (localTiles);
...
}
When the user disables collaboration mode, we create a new non-networked list containing the current contents of the collaborative poem. Using this mechanism, a user can disconnect from a collaborative session and finish a poem solo.
public void actionPerformed (ActionEvent event) { ... }
When the user clicks on any of the user-interface buttons or otherwise engages an action, actionPerformed()
is called, which in turns calls one of the following methods.
void load () {
...
// download the poem titles
URL url = new URL (getCodeBase (), "/servlet/PoetryServlet");
BufferedReader reader = new BufferedReader (
new InputStreamReader (url.openStream (), "latin1"));
String title;
while ((tile = reader.readLine ()) != null)
...
}
The load()
method downloads all of the currently submitted poem titles. We use the same code we used to download the initial word list; however, this time we are accessing the PoetryServlet
on the server.
void loadPoem (String title) {
...
// download a poem
String encodedTile = URLEncoder.encode (title);
URL url = new URL (getCodeBase (),
"/servlet/PoetryServlet?title=" + encodedTitle);
ObjectInputStream in = new ObjectInputStream (url.openStream ());
localTiles = (ObservableList) in.readObject ();
in.close ();
poetry.setTiles (localTiles);
...
}
The loadPoem()
method downloads a poem from the PoetryServlet
. We use the URLEncoder
class to encode the title as appropriate for an HTTP get
request and then call on the servlet to download the poem. We use an ObjectInputStream
to download the serialized poem and then simply assign it to the PoetryBoard
.
void savePoem (String title) {
...
// upload a poem
String encodedTile = URLEncoder.encode (title);
URL url = new URL (getCodeBase (),
"/servlet/PoetryServlet/" + encodedTitle);
URLConnection conn = url.openConnection ();
conn.setDoOutput (true);
ObjectOutputStream out = new ObjectOutputStream (
conn.getOutputStream ());
out.writeObject (localTiles);
out.close ();
conn.getInputStream ().close ();
...
}
The savePoem()
method uploads a poem to the PoetryServlet
. We construct a URL that references the servlet, with the poetry title appended to the servlet name (for example, http://servlet/PoetryServlet/This+is+a+title%21
). We then open a URLConnection
to post the poem, call setDoOutput()
to enable HTTP posts, and write the current poem using an ObjectOutputStream
. Finally, we close the output stream and call getInputStream()
to perform the HTTP post.
Class PoetryServlet
The PoetryServlet
class is the main servlet for our poetry application. It simply provides a storage mechanism for user’s poems.
public class PoetryServlet extends HttpServlet {
}
Our servlet is a standard HTTP-accessible servlet, so we extend HttpServlet
from the Java Servlet Development Kit.
Hashtable poems;
The current list of poems is stored in the poems hashtable. We store each poem as a byte-array containing the serialized word list.
public void init (ServletConfig c) throws ServletException {
super.init (c);
String fileName = getInitParameter ("saveFile");
if (fileName != null) {
try {
ObjectInputStream in = new ObjectInputStream (
new FileInputStream (fileName));
poems = (Hashtable) in.readObject ();
in.close ();
log ("Poems loaded from " + fileName);
} catch (FileNotFoundException ex) {
poems = new Hashtable ();
log ("New poem database created.");
} catch (Exception ex) {
poems = new Hashtable ();
log ("Error loading poems: " + ex);
}
} else {
poems = new Hashtable ();
log ("Poems not loaded.");
}
}
Once our servlet is initialized, and before any requests are made, the init()
method is called. We deserialize a stored list of poems from a persistent storage file, if it exists, or create a new, empty list of poems if it does not. We use the getInitParameter()
method to get the saveFile
initialization parameter; the Web server administrator should specify this with a servlet configuration tool when installing the servlet.
public void destroy () {
String fileName = getInitParameter ("saveFile");
if (fileName != null) {
try {
ObjectOutputStream out = new ObjectOutputStream (
new FileOutputStream (fileName));
out.writeObject (poems);
out.close ();
log ("Saved poems to " + fileName);
} catch (IOException ex) {
log ("Error saving poems: " + ex);
}
} else {
log ("Poems not saved.");
}
super.destroy ();
}
The destroy()
method is called when our poetry servlet is being unloaded; we serialize the current array of poems back into the persistent storage file.
public void doGet (HttpServletRequest httpReq, HttpServletResponse httpRes)
throws ServletException, IOException {
String title = httpReq.getParameter ("title");
if (title == null) {
httpRes.setContentType ("text/plain");
ServletOutputStream out = httpRes.getOutputStream ();
Enumeration keys = ((Hashtable) poems.clone ()).keys ();
while (keys.hasMoreElements ())
out.println (keys.nextElement ().toString ());
} else {
httpRes.setContentType ("application/octet-stream");
ServletOutputStream out = httpRes.getOutputStream ();
if (poems.containsKey (title))
out.write ((byte[]) poems.get (title));
}
}
The doGet()
method handles HTTP get
requests and is called when a client queries the current list of poems or tries to download a named poem.
If the query does not contain a title specification (that is, if the servlet URL is not followed by ?title=XYZ
), we return the current list of poems. We set the content type to plaintext, extract a stream to write our response, and then write the titles of all of our poems.
If a title was specified, we set the content type to raw data and simply write the serialized poem back to the client. Note that at no point in this servlet do we deserialize the poems; we simply assume that they are properly formatted byte arrays.
public void doPost (HttpServletRequest httpReq, HttpServletResponse httpRes)
throws ServletException, IOException {
String title = httpReq.getPathInfo ();
if (title != null) {
title = decode (title.substring (1));
int size = httpReq.getIntHeader ("Content-length");
DataInputStream in = new DataInputStream (httpReq.getInputStream ());
byte[] poem = new byte[size];
in.readFully (poem);
poems.put (title, poem);
}
ServletOutputStream out = httpRes.getOutputStream ();
out.println ((title != null) ? "ACK" : "NAK");
}
We call doPost()
when the client is uploading a poem using an HTTP post request. We take the poem title from the path info request; that is, any pathname that follows our servlet’s name (for example, /servlet/PoetryServlet/this_is_the_title
). Note that this title will be URL-encoded, so we must first manually decode it.
We then determine the size of the encoded poem by querying the Content-length header. Using this value, we read the entire poem and then place it in the poems hashtable.
Finally, we return a response to the user, based on whether the process succeeded or not.
private static String decode (String s) { ... }
This method simply decodes an URL-encoded title string.
Installing the poetry application
To install this application on a Web server, you must install three separate file sets:
-
Install the poetry applet in the Web server’s document tree, which includes the client code and the client-side servlet list classes.
-
Install the poetry servlet (the
PoetryServlet
class file; remember, this servlet doesn’t deserialize the poems so it doesn’t need any extra classes). Assign an initialization parameter saveFile that is a file to which the poem database can be written. - Install the distributed list servlet. Because this servlet accesses deserialized instances of the
Tile
class, we need Tile.class in addition to the servlet classes and all of its helper classes. Assign an initialization parameter loadFile, a file from which the initial list status can be read. The file should be a serializedIDList
, such as the words.dat file included in the source distribution.
The source distribution (in Resources) explains which classes need be installed where. Note that the various distributed list classes have been modified slightly from previous articles, so be sure to replace any old copies of these classes you may have lying around.
Conclusion
The basic non-networked poetry applet is fun to use, but it is limited by the fact that any poetic creation is restricted to an audience of one. Adding a CGI-based backend to provide storage of poems is possible, but as with most CGI, it is not an immensely enjoyable task. Using servlets, however, we can add networking to the poetry applet with almost no effort.
As we have seen here, both the client-side and server-side implementations of such networked applications are not complex at all. We use the URL
class to provide convenient client-side access to HTTP data, and the HttpServlet
class to perform all of the processing of server-side issues. All issues of TCP, proxies, keepalive, and the like are handled for us by these support classes.
Furthermore, if we have access to a transparent mechanism for collaboration, such as the distributed list class that we developed in earlier articles, we can add servlet-based realtime collaboration with even fewer changes. It is worth noting that the low-level PoetryBoard
class has no notion of any of the networking aspects of this application. All it is aware of is providing a simple click-and-drag interface to tiles stored in a list datastructure. All networking details are handled elsewhere.
Enough of the tech-talk. Time to release the poet in you…