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
TextFieldfor identity entry. The identityTextFielddisappears 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.
Notice that the display is a CardLayout consisting of three separate displays: โconsole,โ โread,โ and โpost.โ displayout is an instance of CardLayout created in order to be able to globally manipulate display.
Wiring up the events
Most of what happens in the client is a reaction to user events in the GUI. The event handler is therefore the most interesting part of the Forum code.
public boolean handleEvent (Event e) {
// launch Event
if ((e.id == -1) && (e.target == this)) {
Vector receive = (Vector) e.arg;
Enumeration en = receive.elements();
parent = (ForumLauncher) en.nextElement();
// Load threads from server
comm = new ForumComm (parent);
articles = comm.loadAllThreads();
updateThreadList();
setTitle ((String) en.nextElement());
Color c = (Color) en.nextElement();
if (c != null)
setBackground (c);
String welcome = null;
try {
welcome = (String) en.nextElement();
StringBuffer w = new StringBuffer();
int idx = 0, nIdx;
while ((nIdx = welcome.indexOf (MESSAGE_NEWLINE, idx)) >= 0) {
w.append (" n").append (welcome.substring (idx, nIdx));
idx = nIdx + 1;
}
if (idx < welcome.length ())
w.append (" n").append (welcome.substring (idx));
welcome = new String (w);
welcome = welcome.substring (2);
} catch (NoSuchElementException ex) {}
if (welcome == null)
welcome = "";
console.setText (welcome);
}
The โlaunch Eventโ is the Event posted to Forum by its parent, ForumLauncher, at construction. This part of handleEvent(โฆ) retrieves its parameters from the Vector posted to it by its parent as the argument to the launch Event. It also grabs the discussion thread list from the server with a call to the ForumComm loadAllThreads() method.
It is desirable to have newline characters in the welcome message, but most browsers ignore them as parameters. Therefore, to support newline characters, the welcome message may contain their representation, as denoted by the MESSAGE_NEWLINE character. The while construct inside the try... statement manually replaces the MESSAGE_NEWLINE character with a real newline character.
// WINDOW_DESTROY Events
else if ((e.id == Event.WINDOW_DESTROY) && (e.target == this))
hide();
else if ((e.id == Event.ACTION_EVENT) && ((String)e.arg).equals(CLOSE))
hide();
// READ, NEW, and REPLY mode change Events
else if ((e.id == Event.ACTION_EVENT) && (((String)e.arg).equals(READ))) {
displayout.show (display, "read");
prButton.setLabel (REPLY);
mode = READ;
}
else if ((e.id == Event.ACTION_EVENT) && (((String)e.arg).equals(NEW))) {
post.setText("");
displayout.show(display, "post");
prButton.setLabel (POST);
mode = NEW;
}
else if ((e.id == Event.ACTION_EVENT)&&(((String)e.arg).equals(REPLY))) {
if ((read.getText()).equals("")) {
console.setText ("Reply to what?");
displayout.show (display, "console");
}
else{
// quote reply
StringBuffer repl = new StringBuffer ();
String art = read.getText();
int idx = 0, nIdx;
while ((nIdx = art.indexOf ("n", idx)) >= 0) {
repl.append (QUOTE_CHARS).append (art.substring (idx, nIdx+1));
idx = nIdx + 1;
}
if (idx < art.length ())
repl.append (QUOTE_CHARS).append (art.substring (idx));
repl.append ("nn");
post.setText(repl.toString());
displayout.show(display, "post");
prButton.setLabel (POST);
mode = REPLY;
}
}
// POST Event
else if ((e.id == Event.ACTION_EVENT) && (((String)e.arg).equals(POST))) {
// check for previously-failed post
String t = failedPostThread;
if (t == null)
t = selectedThread;
if (postArticle (post.getText(), t)) {
console.setText (POSTED_MESSAGE);
displayout.show (display, "console");
mode = POST;
}
}
The READ and NEW events are straightforward. They reset the display so that the user sees the correct command options and correct card of display. REPLY is a bit more involved, in that it quotes the current articleโs text by prepending the QUOTE_CHARS macro to every line. The while loop manually tokenizes the text for the reply, because StringTokenizer returns two contiguous instances of the delimiter character as one token instead of two, which has the unfortunate effect of condensing multiple blank formatting lines into a single line.
The POST event shows the correct display card and options, but adds a call to the postArticle(...) method, which does the work of trying to post the text contained in the post TextArea. The instance variable failedPostThread is checked to see if it contains anything. If it does, the current POST is an attempt to retry a failed POST. If not, itโs a new POST to the currently selected thread.
// Identity -> ID event
else if ((e.id == Event.ACTION_EVENT) && (((String)e.arg).equals(ID))) {
id.show();
inval (id);
validate();
console.setText (ID_MESSAGE);
displayout.show (display, "console");
}
// TextField return Event (the end of ID mode)
else if ((e.id == Event.ACTION_EVENT) && (e.target == id)) {
id.hide();
inval (id);
validate();
if (mode == POST)
displayout.show (display, "post");
else if (mode == READ)
displayout.show (display, "read");
else
displayout.show (display, "new");
}
The ID event occurs when the user selects the menu item for changing his/her identity. Id is not useful except in the context of a POST; if the user attempts to post and has not set his/her id, a new ID event is thrown and the user is forced to set an id. When the ID event occurs, the id TextField is displayed by means of a call to its show() method. Due to a layout bug in Netscape, it is necessary to call a custom layout method, inval(...), to make sure the relayout occurs correctly.
Hitting the return key in the id TextField hides the id TextField and resets display to show the card associated with the current mode. Again, a call to inval(...) makes everything come out right.
// Thread selection
else if ((e.id == Event.ACTION_EVENT) && (e.target == threadList)) {
selectedThread = threadList.getSelectedItem();
articles.put (selectedThread, comm.loadThreadArticles(selectedThread));
read.setText("");
displayout.show (display, "read");
updateArticleList();
}
Selection of a thread from threadList may be accomplished by a double-click on one of its elements. This event will cause the communications code to try to load the articles in the selected thread from the server. If this is successful, the local articles in the selected thread will reflect the articles in the serverโs thread. If unsuccessful, the threadโs contents will remain unchanged. The list of articles is updated to reflect the articles in the thread, if any.
// Article selection
else if ((e.id == Event.LIST_SELECT) && (e.target == articleList)) {
Vector threadArts = (Vector) articles.get (selectedThread);
String a=(String)threadArts.elementAt(articleList.getSelectedIndex());
// pull off header (if any)
String art = new String();
StringTokenizer strtok = new StringTokenizer (a, DELIMITER);
if (strtok.hasMoreTokens())
art = strtok.nextToken();
if (strtok.hasMoreTokens())
art = strtok.nextToken();
read.setText (art);
postEvent (new Event (this, Event.ACTION_EVENT, READ));
}
// Help->About...
else if ((e.id==Event.ACTION_EVENT)&&(((String)e.arg).equals(ABOUT))){
if (about == null)
about = new AboutBox();
else
about.show();
}
else if ((e.id==Event.ACTION_EVENT)&&(((String)e.arg).equals(HELP))) {
console.setText (HELP_MESSAGE);
displayout.show (display, "console");
}
return super.handleEvent(e);
} // end handleEvent
Selecting an article causes the article to be displayed in the read card of the display component. First the article body is extracted from the article string by taking the second token of the raw article string. If for some reason there is no article header (that is, there is only one token), the whole article will be put in the display area.
Support functions
boolean postArticle (String art, String t) {
// check for null message body
if (art.equals("") || art == null) {
console.setText ("Can't post a null message body...");
displayout.show (display, "console");
return false;
}
// check for selected thread
if (t == null) {
console.setText ("Please select a thread for your post.");
displayout.show (display, "console");
return false;
}
// check id
if ((id.getText()).equals("")) {
postEvent (new Event (this, Event.ACTION_EVENT, ID));
return false;
}
// check for attempted repost
if (mode == POST) {
console.setText (REPOST_ERROR_MESSAGE);
displayout.show (display, "console");
return false;
}
// it's ok, so add headers and post
String wholeArt = id.getText() + DELIMITER + art;
if (! comm.postArticle (wholeArt, t)) {
console.setText ("Post failed due to a network problem. Please try
again.nnIf you change mode to " + READ + " or " + NEW + ", this post
will be lost.");
displayout.show (display, "console");
failedPostThread = t;
return false;
}
// refresh. if it fails, fake it
Vector a = comm.loadThreadArticles (t);
if (a.isEmpty()) {
a = (Vector) articles.get (t);
a.addElement (wholeArt);
}
articles.put (t, a);
updateArticleList();
failedPostThread = null;
return true;
} // end postArticle
The postArticle(...) method first checks for four error conditions, and returns false if any of these exist. If the current POST passes the checks, the postArticle(...) method of the ForumComm is called to make the actual networking calls to the server. In the event that the post to the server fails, a failure message is posted to the console card of display and the failedPostThread instance variable is set to the currently-selected thread.
If the post is successful, a thread refresh is attempted. In the unlikely event that the refresh fails (even though the post just succeeded), the newly-posted article is manually added to the article list in the post thread. failedPostThread is set to null.
void updateThreadList() {
if (threadList.countItems() > 0)
threadList.clear();
for (Enumeration en = articles.keys(); en.hasMoreElements();) {
threadList.addItem ((String) en.nextElement());
}
}
void updateArticleList () {
if (articleList.countItems() > 0)
articleList.clear();
Vector articlesInThread = (Vector) articles.get (selectedThread);
for (Enumeration en=articlesInThread.elements(); en.hasMoreElements();){
// Extract header
String art = (String) en.nextElement();
String header = DEFAULT_HEADER;
StringTokenizer strtok = new StringTokenizer (art, DELIMITER);
if ((strtok.hasMoreElements()) && (strtok.countTokens() > 1))
header = strtok.nextToken();
articleList.addItem (header);
}
}
These two methods update the thread and article list, respectively. updateArticleList() pulls off the first token from each whole article in the thread and puts them into articleList as list elements. If an article has no header, a default header, DEFAULT_HEADER, is used instead.
// manual layout for Netscape's layout bug
void inval (Component c) {
Component d = c.getParent();
if (d != null)
inval (d);
if (c instanceof Container) {
Container e = (Container) c;
for (int i = 0; i < e.countComponents(); ++ i)
e.getComponent (i).invalidate();
c.invalidate();
}
}
The above method is a workaround for a Netscape problem involving re-laying out components. Without this method, the layout changes needed to accommodate the comings and goings of the id TextField donโt work properly.
Conclusion
The basic framework presented in this article can be extended to provide more advanced client function. For example, the
display
component could be custom-built to support more complex display types, such as images and HTML. Also, more sophisticated synchronization between client and server, such as checking for the existence of each post thread and removing those that no longer exist from the client, would be useful. Another optimization would be to pull each article from the server only when requested by the user.
In next monthโs column, we will develop the networking and server portions of the Forum application.


