Release your inner poet: Use servlets to create a collaborative poetry app

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.

The poetry 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.

The collaborative framework

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 poetry servlet

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

Resources

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.

1 2 3 Page 1
Page 1 of 3