Write your own threaded discussion forum

Use a forum to make your Web site more interactive, provide customer support, and more

Discussion forums are great for providing interactivity and the potential for message exchange. A simple Java-based forum tied to a Web site can offer site visitors ease of use and topical discussion capabilities. Let's build one!

Discussion group systems come in many flavors. The quintessential example of a full-featured discussion forum is Usenet, the Internet "news" network. Usenet is a client/server system consisting of a network of servers that supports tens of thousands of separate newsgroups. Each group contains multiple concurrent discussion topics. Each topic, or thread, contains multiple articles, linked as a series of responses to the first post in the topic. A single simple thread may remain a straight line or turn into a tree as people post follow-up messages to replies and so on.

Usenet users can read articles, post responses in threads, and start their own threads. They can even start their own groups, although that's a little more involved. New threads and articles propagate throughout the world's network of Usenet servers, and are available to anyone accessing the system.

With our Interchange forum applet, we plan to capture the core functionality of Usenet in a Java system, but in a far simpler fashion.

Design points

Feature overview

The discussion Forum we're going to build is the equivalent of one Usenet newsgroup, with a fixed set of threads provided by the site administrator. Users will have the option to read articles submitted under the available threads, post new articles, and reply to others' posts. The Forum administrator will have the option to age articles, limit memory and disk usage, and restrict access to designated portions of the Internet.

Let's get into specifics on how the client can implement its features.

User interface behavior

  • GUI

    The list of threads, the list of articles, a control area, and a display window should be the main features of the interface, since they are the most-used. It doesn't hurt to mirror control options on the menu bar as well, along with other less commonly-used features such as "Close."

  • Modes

    The client interface needs to reflect the current operation that the user is performing. A good way to do this is to make the client support four mutually exclusive modes: READ (text display area shows message text, post/reply button shows "reply"), REPLY (text display shows quoted message text, post/read button shows "post"), NEW (text area displays a blank message, post/read button shows "post"), and POST (id field will be shown if it needs to be filled out).

    The click of a button or selection of a menu item produces an event that performs appropriate actions, sets the client's internal mode, and determines the components showing on the interface.

  • User identity

    The user needs to be able to set and change his/her identity. The user's identity is used to "sign" articles, and is placed in the article list to represent posted articles.

    Selection of the identity option from the menu drives a layout change in which the control panel resizes to accommodate a TextField for identity entry. The identity TextField disappears when it has focus and the return key is pressed.

Communications protocol

To move thread listings from server to client and articles from client to server (and vice versa) a simple protocol is needed. The following does nicely:

  • "Load all threads" gets all the current threads from the server.

  • "Load all articles in thread T" gets all the articles in thread T from the server when the user selects thread T.

  • "Post article A to thread T" will post an article to the server under thread T as soon as the user makes the post. At this point, the postArticle(...) method tries to refresh the thread from the server. If the post is successful but the refresh fails for some reason, it adds the new article to the appropriate thread to reflect this. If the post fails, it alerts the user to retry the post later.

OO stuff

The code for the client is comprised of three classes: ForumLauncher, Forum, and ForumComm. ForumLauncher displays an icon in the page where the Forum client resides. Forum is the actual Frame that presents the GUI and contains most of the application's logic. ForumComm is a communications library that implements the client-side networking.

Coding the client

We'll cover

ForumLauncher

and

Forum

in this article.

ForumComm

and

ForumServer

will be in next month's edition. The full source for

ForumLauncher

and

Forum

is available

here.

Class ForumLauncher

Applet parameters

A sample ForumLauncher tag looks like this:

<applet code=ForumLauncher height=100 width=100>

<param name="bgcolor" value="#ffffff">

<param name="icon" value="images/people.gif">

<param name="title" value="The JavaWorld Forum">

<param name="welcome" value="Welcome to the JavaWorld Forum!//Enjoy!">

</applet>

bgcolor and icon are used by ForumLauncher to blend in with its containing document and display an icon, respectively. Clicking on the icon will launch the main Forum frame.

title and welcome are the title of the Forum frame and the welcome message that appears in the app's display window, respectively. These parameters are passed into the Forum object when it is constructed. Another available parameter for Forum, forumbgcolor, sets the background color of the Forum frame. This parameter doesn't work under Windows because Windows doesn't allow setting the background color of arbitrary components.

The welcome parameter allows for the specification of newlines with the / character. In the above example, Forum's welcome display will display "Welcome to the JavaWorld Forum!", two newline characters, and "Enjoy!"

public class ForumLauncher extends Applet {
  Image icon;
  Frame client;
  String title;
  Color forumBgcolor;
  Vector send;
  public void init () {
    String bgcolor = getParameter ("bgcolor");
    if (bgcolor != null) {
      try {
        setBackground (new Color(Integer.parseInt(bgcolor.substring(1),16))); 
      } catch (NumberFormatException ex) {
          System.out.println ("Invalid format for bgcolor:" + bgcolor);
      }
    }
    icon=getImage(getDocumentBase(),getParameter("icon","images/forum.gif"));

The above sets up the parameters for ForumLauncher and whatever instance variables are needed to hold parameters.

    // send parameters to Forum Frame
    send = new Vector();
    send.addElement (this);
    send.addElement (getParameter ("title", "Sample Forum"));

Here we're creating a Vector and putting in parameters that Forum needs when it is constructed. Why? Because we're not going to construct the Forum class normally. Instead, we're going to construct it only if there is a MOUSE_DOWN in the applet, which prevents Forum from downloading to the user's machine unless he/she wants to launch it.

    String fc = getParameter ("forumbgcolor");
    if (fc != null) {
      try {
        forumBgcolor = new Color (Integer.parseInt (fc.substring(1), 16));  
      } catch (NumberFormatException ex) {
        System.out.println ("Invalid format for forumbgcolor:" + bgcolor);
      }
    }
    send.addElement (forumBgcolor);
    String welcome = getParameter ("welcome");
    if (welcome != null)
      send.addElement (welcome);
  }

The above sets up the remaining parameters for Forum and loads them into the Vector send, from which Forum will later extract them.

  public void paint (Graphics g) {
    int x = icon.getWidth (this);
    int y = icon.getHeight (this);
    g.drawImage (icon, (size().width - x)/2, (size().height - y )/2,this);
  }
  public String getParameter (String param, String def) {
    String result = getParameter (param);
    if (result != null)
      return result;
    else
      return def;
  }   

The ForumLauncher paint(...) method draws the icon image in the center of the applet. An "overload" of the stock getParameter(...) method allows us to set defaults for parameters in the method call.

  public boolean mouseDown (Event e, int x, int y) {
    if (client == null) {
      showStatus("Loading Forum...");
      try {
        client = (Frame) Class.forName("Forum").newInstance();
      }
      catch (ClassNotFoundException ex) {
        System.out.println(ex);
      }
      catch (InstantiationException ex) {
        System.out.println(ex);
      }      
      catch (IllegalAccessException ex) {
        System.out.println(ex);
      }
      client.postEvent (new Event (client, -1, send));
    }
    client.show();
    return super.mouseDown (e, x, y);
  }
}

This is the interesting method in ForumLauncher. The Forum class is only downloaded and constructed when the user clicks in the applet. A new Event containing a reference to the Vector send is posted to client, which is the new instance of Forum.

Class Forum

This class represents the client side of the Forum, with the exception of the low-level networking code.

import java.awt.*;
import java.util.*;
import ForumLauncher;
import ForumComm;
public class Forum extends Frame {
  ForumLauncher parent;
  TextArea console, read, post;
  Panel display, selector, control; 
  Menu file, modes, identity, help;
  CardLayout displayout;
  List threadList, articleList;
  String mode;
  Button prButton;
  TextField id;
  String selectedThread, failedPostThread;
  Hashtable articles;
  AboutBox about;
  ForumComm comm;

Important variables

  String selectedThread, failedPostThread;

These guys are used to store the currently selected thread and any thread associated with the last post, if it failed. failedPostThread is necessary because the user may may make a post attempt, have it fail, and then change threads for some reason. When he or she attempts to repost, the post needs to go to the post thread, not the selected thread. If the post is later successful or the user starts a new post before the current one is successful, failedPostThread is set to null.

  String mode;

This is set by the event handling code to contain the current mode.

  Hashtable articles;

articles is a Hashtable that has the name of the available threads as keys with values of type Vector. Each Vector holds the list of articles associated with the thread key.

#defines

  // Messages
  static final String DEFAULT_HEADER = "<Unsigned Article>";
  static final String ID_MESSAGE = "Please enter a name in the box in the
lower left-hand corner.\nThen press <RETURN>.";
  static final String POSTED_MESSAGE = "Your article was posted!";
  static final String REPOST_ERROR_MESSAGE = "You just posted that
article!";
  static final String HELP_MESSAGE = "...";
  // control options (and events)
  static final String CLOSE = "Close";
  static final String READ = "Read";
  static final String NEW = "New";
  static final String REPLY = "Reply";
  static final String POST = "Post";
  static final String ID = "Change My Identity";
  static final String ABOUT = "About";
  static final String HELP = "Help...";
  static final String DELIMITER = "\u0000";
  static final String MESSAGE_NEWLINE = "/";
  static final String QUOTE_CHARS = "> ";
  static final int LIST_HEIGHT = 6;
  static int HEIGHT = 25;
  static int WIDTH = 60;

These are used to define message strings, the String associated with button presses and menu item selections, and other internals such as the height and width of the display component.

The constructor and GUI setup

 public Forum () {
    // set up MenuBar
    MenuBar bar = new MenuBar ();
    bar.add (file = new Menu ("File"));
    bar.add (modes = new Menu ("Mode"));
    bar.add (identity = new Menu ("Identity"));
    bar.setHelpMenu (bar.add (help = new Menu ("Help")));
    file.add (new MenuItem ("Close"));
    modes.add (new MenuItem (READ));
    modes.add (new MenuItem (NEW));
    identity.add (new MenuItem (ID));
    help.add (new MenuItem (ABOUT));
    help.add (new MenuItem (HELP));
    setMenuBar (bar);
    // display panel: application console, read, and post
    display = new Panel();
    display.setLayout (displayout = new CardLayout());
    display.add ("console", console = new TextArea (HEIGHT, WIDTH));
    display.add ("read", read = new TextArea (HEIGHT, WIDTH));
    display.add ("post", post = new TextArea (HEIGHT, WIDTH));
    read.setEditable (false);
    console.setEditable (false);
    // control 
    control = new Panel();
    control.setLayout(new BorderLayout());
    control.add ("West", new Button (READ));
    control.add ("Center", new Button (NEW));
    control.add ("East", prButton = new Button (REPLY));
    control.add ("South", id = new TextField());
    id.hide();
    // selector panel: thread and article selectors, control
    selector = new Panel();
    selector.setLayout (new BorderLayout());
    selector.add ("North", threadList = new List (LIST_HEIGHT, false));
    selector.add ("Center", articleList = new List());
    selector.add ("South", control);
    add ("Center", display);
    add ("West", selector);
    pack();
 }

The constructor is pretty straightforward: It sets up the menu bar and the control and display elements of the interface.

The text on buttons and menu items doubles as Event identifiers. For example, the string defined in the READ macro appears on a button and a menu item. This string, which is generated as the argument to an ACTION_EVENT resulting from the selection of the associated button or menu item, is recognized as the READ "event" by the event handling code.

1 2 Page 1