|
Articles Index
Technologies Behind the New Developer Forums
Part I
Dana Nourie
June 2001
There is no shortage of Java technologies in the open-source community,
but one project in particular piqued the interest of the engineers behind the Java Developer
Connection, the Solaris Connection,
and Dot Com Builder web sites. The project is called Jive, hosted by
JiveSoftware.com, and Jive has been installed for the developer site forums.
Jive is forum software that features content filters and user defined "skins" to change the look and feel and
function of the forums, powered by a plugable backend. Written with an object-oriented design, Jive is
extensible, scalable, and secure. In addition, the Jive forums leverage server side technologies, such as
JavaServer Pages (JSP), JDBCTM, servlets, and the JavaMail API in combination
with XML.
Because of the differences in the look and feel between the three developer sites and the high traffic and
volume of the JDC forums in particular, Jive was a good choice. Its basic architecture allows for cosmetic changes from one developer site to the next, and its plugable database schemas worked for smooth
integration with the current backend configurations.
Part I of this article focuses on the Java technologies used in the general architecture of the new JDC forums, as well
as the configurations and changes to code for the JDC special forum features. To follow the concepts below,
you'll need familiarity with basic Java programming syntax, JSP concepts, and the JavaMail and JDBC APIs. For
more details on these specific technologies and deploying Jive on a web site, see For More Information below.
Contents
General Forum Architecture
Look and Feel
Special Forum Features
Conclusion
For More Information
General Forum Architecture
The three developer sites implement Jive 2.0 software and architecture. This application is a three tier
system, consisting of skins, filters and interfaces, and the database backend. Skins contain the logic
necessary to display a user interface, while the Jive core classes provide the logic to interact with the
forum data. Between the skins and the data are filters, which remove profane language or include syntax
highlighting.
For information on setting and configuring the server for Jive, see For More Information below.
The two starting points into the Jive core libraries are:
-
AuthorizationFactory is responsible for passing out
Authorization objects based on a username and password, or an anonymous
authorization. This functionality is separate from all other
classes in Jive so that it is plugable. For example, it's possible to write implementations to authorize on a
LDAP backend.
ForumFactory provides access to and management of forums. It is the point of entry for the
entire Jive system. A concrete instance of ForumFactory is obtained by calling the
getInstance method with an Authorization token:
ForumFactory.getInstance(Authorization)
From that point on, the authorization token does not need to be passed on again
because Jive transparently maintains a reference to it with a series of
protection proxy objects.
The core API is defined as a set of abstract classes and interfaces, which allow an implementor to design their own customized versions. Additionally, in the core is a database implementation of the interfaces that can be used with almost any database software that can be accessed through JDBC.
JDBC and the Database
The Jive database
schema and JDBC calls work on almost every database. No changes need to be made to the code. There are just
two steps to be taken to get Jive to work with your database:
- Install the correct schema for your database.
- Select the JDBC driver and enter the JDBC connection information into Jive.
Adding support for another database is as easy as making
a custom schema for it.
|
There is a database implementation of all the core interfaces, such as
DbForum and DbForumFactory. These classes contain the actual JDBC logic to
load and save data from the database. More importantly, the database classes use some
sophisticated caching so that Jive is fast.
- JDBC allows Jive to work with over 10 databases with the same code.
- The unique structure of server side Java technology, as opposed to CGI type
processes, is what makes Jive so fast. Every request grabs the same cached
data from memory instead of having to go to the database repeatedly.
- Jive automatically uses database transactions where appropriate if
your database supports them.
Protection Proxies and Permissions
The proxy classes wrap each of the database objects to protect them on a per user
basis.
For example:
- User admin -- can edit any part of message #25
- User jsmith -- able to read message #25, but not modify it
Each user gets an individual copy of a message proxy with those permissions
set on it, and message proxy wraps the same underlying database message
object, but controls access to it based on permissions:
Two kinds of permission exist: user permissions and group
permissions. Each user can be a member of any number of groups. The
following permissions exist:
- System admin
- Forum admin, of a particular forum
- Read
- Start thread
- Create message
Different new permissions for creating a thread and message
are useful for Weblog type systems such as Slashdot.org
When the user tries to access a particular forum, the following checks are performed:
- Check anonymous user permissions for the system and for the particular
forum.
- Check "registered users" permissions for the system and particular forum.
This permission is useful if you'd like all registered users to be able to
perform a certain task. For example, you might set "anonymous" read-only
permissions and "registered users" permissions for creating
threads and messages on a forum.
- Check the individual user's system and specific forum permissions.
- Find out what groups the user belongs to and then check permissions for
each of the groups.
Look and Feel
Few software packages are used without changes or additions to code, and Jive is no different. Fortunately,
Jive was designed to be easily customized. Its object-oriented design makes it easy to remove, add, or change
special features.
Skins
Skins are the User Interface to the forums. Written in a combination of regular
HTML and JSP scriptlets (snippets of Java code on JSP pages), skins maintain
the look and feel of a web site while making function or method calls to other
JSP pages or Java classes.
JSP file includes generate static HTML, such as web site navigation, logo, and site search
box, while includes to other JSP pages provide dynamic content, such message threads that are continually added to
and updated. In addition, scriptlets handle the logic behind what information the user is served.
For instance, the following include gets the file that contains the logic to generate the user's local
time:
<%-- global methods, variables --%>
<%@ include file="global.jsp" %>
Files to include static HTML are as simple:
<%@ include file="header.jsp" %>
Regular Java programming to initiate a member's visit to the forum is also included on the JSP, but the classes themselves are in the CLASSPATH of the server running the Jive software. Authorization and membership are based on token objects,
so the introduction page to the forums checks for a forum factory object:
<% // All Jive objects come from the forum
// factory object. "authToken" is a
// variable from the included
// authorization.jsp
ForumFactory forumFactory =
ForumFactory.getInstance(authToken);
// The user using this page. The user object will
//be null if the
// user is just a guest.
User pageUser = null;
boolean isGuest = authToken.isAnonymous();
if (!isGuest) {
pageUser = forumFactory.getUserManager(
).getUser(authToken.getUserID());
}
%>
|
Next, the code checks which forums are available, including the most popular forums, a special JDC forums
feature:
<% // All forums viewable by this user
Iterator forums = forumFactory.forums();
// A list of popular threads
Iterator popularThreads =
forumFactory.popularThreads();
// A list of popular forums
Iterator popularForums =
forumFactory.popularForums();
%>
|
The logic is then wrapped into a try/catch block, and arrays are declared for the forums:
// Wrap all logic in a try/catch block.
If there are any errors here, we
// redirect to the error page
try {
forumFactory =
ForumFactory.getInstance(authToken);
if (!pageUserIsGuest) {
pageUser = forumFactory.getUserManager(
).getUser(authToken.getUserID());
}
lastVisited =
SkinUtils
.getLastVisited(request,response);
popularForums =
forumFactory.popularForums();
popularThreads =
forumFactory.popularThreads();
// Edit the forums that appear on
// this page here.
// Unused elements are ignored when
// forums are printed.
column1ForumNames = new String[10][16];
column2ForumNames = new String[10][16];
// more code for setting up
// message threads by topics,
// which is covered later.
...
catch (Exception e) {
// Variables need by the sidebar
String searchURL = null;
Forum searchForum = null;
int iteratorType = NONE;
Iterator popularItemIterator = null;>
<%@ include file="sidebar.jsp" %>
<%@ include file="error.jsp" %>
<%@ include file="footer.jsp" %>
<% boolean error = true;
if (error) {
return;
}
}
%>
|
JSP pages provide the UI and logic for the JDC forums, but it's possible to write a Jive front-end in Swing,
or other technologies.
Topics by Java Technologies
If the JDC had only one forum, and members posted all their questions and responses to a central area, the
forum would be so big that it would take hours to bring up the forum in the browser. Instead, the JDC is
broken into topics organized into forums by Java technologies. This organization makes sense for how developers think in
terms of development, and it makes sense for breaking down a forum into smaller, more manageable areas.
These multiple topic forum are created through a special Jive administration tool. New topic threads are created through new forums.
First, the administration tool checks to be certain the user has permission to add, delete, or change forums:
// Jive checks the username and password.
// If valid, it issues an Authorization token.
Authorization auth =
AuthorizationFactory.createAuthorization(
"admin", "password");
ForumFactory factory =
ForumFactory.getInstance(auth);
|
Next, the tool allows the user to create new forums:
// Calling factory.createForum creates a new entry
// in the jiveForum database table and
// inserts the forum object into memory cache for
// future fast access:
try {
Forum newForum = factory.createForum(
"New Forum Name",
"Some description for new forum.");
}
catch (UnauthorizedException ue) {
System.err.println("Sorry! You don't have" +
" permissions to create a new forum!");
}
//Create a new topic (thread). Each thread has
// a root message, so that is what's
// created first:
try {
ForumMessage rootMessage =
factory.createMessage();
rootMessage.setSubject("Some subject");
rootMessage.setBody("A body would go here");
ForumThread newThread =
factory.createThread(rootMessage);
//Finally, the thread object is added
// to the forum:
forum.addThread(newThread);
}
catch (UnauthorizedException ue) {
System.err.println("Sorry! You don't have" +
" permissions to create a new topic!");
}
|
Jive also makes database entries into the jiveMessage and
jiveThread tables. Foreign keys in the database link all these tables
together.
For example, the list of all the threads in a forum with id of 8
is:
"SELECT threadID FROM jiveThread WHERE forumID=8"
The forum JSP lists the forum topics in arrays as follows:
// category names are at element index 0
column1ForumNames[0][0] = "General";
column1ForumNames[0][1] =
"New to Java Technology";
column1ForumNames[0][2] =
"Java Programming";
column1ForumNames[0][3] =
"Advanced Language Topics";
column1ForumNames[0][4] =
"100% Pure Java Community"+
" Process Program";
column1ForumNames[1][0] =
"Runtime Environment";
column1ForumNames[1][1] =
"Java Runtime Environment";
column1ForumNames[1][2] =
"Java Virtual Machine";
column1ForumNames[1][3] =
"Java HotSpot";
column1ForumNames[2][0] = "GUI Building";
column1ForumNames[2][1] =
"Project Swing";
column1ForumNames[2][2] = "AWT";
column1ForumNames[2][3] =
"Java 3D";
column1ForumNames[2][4] =
"JavaBeans";
column1ForumNames[2][5] =
"Java Media Framework";
column1ForumNames[2][6] =
"Java 2D";
column1ForumNames[2][7] =
"Accessibilty APIs";
column1ForumNames[3][0] =
"Essential Classes and Documentation" +
" Generation";
column1ForumNames[3][1] =
"Java Collections Framework";
column1ForumNames[3][2] =
"Internationalization";
column1ForumNames[3][3] =
"Javadoc Tool";
column1ForumNames[3][4] =
"JavaHelp";
column1ForumNames[3][5] =
"Serialization";
column1ForumNames[4][0] =
"Debug and Deploy";
column1ForumNames[4][1] =
"Java Archive (JAR) Files";
column1ForumNames[4][2] =
"Java Extension Mechanism";
column1ForumNames[4][3] = "JDB Tool";
column1ForumNames[4][4] =
"Java Plug-In";
column1ForumNames[4][5] =
"JavaBeans ActiveX Bridge";
column1ForumNames[4][6] =
"Java Web Start & JNLP";
column1ForumNames[5][0] = "Security";
column1ForumNames[5][1] =
"Signed Applets";
column1ForumNames[5][2] =
"Cryptography";
column1ForumNames[5][3] =
"Security Managers";
column1ForumNames[5][4] = "General";
column1ForumNames[6][0] =
"Installation and Compiling";
column1ForumNames[6][1] = "Compiling";
column1ForumNames[6][2] =
"Installation";
column1ForumNames[7][0] = "Majc";
column1ForumNames[7][1] =
"New to MAJC";
column1ForumNames[7][2] =
"Chip Level Multi Processing";
column1ForumNames[7][3] =
"Wirespeed Computing";
column2ForumNames[0][0] =
"Distributed Computing";
column2ForumNames[0][1] =
"Jini Network";
column2ForumNames[0][2] =
"The Brazil Project";
column2ForumNames[0][3] =
"Java Technology & XML";
column2ForumNames[0][4] =
"JavaServer Web Development Kit";
column2ForumNames[0][5] = "JDBC";
column2ForumNames[0][6] =
"Enterprise Java Beans";
column2ForumNames[0][7] =
"JavaServer Pages";
column2ForumNames[0][8] =
"Interface Definition Language";
column2ForumNames[0][9] =
"Naming and Directory (JNDI)";
column2ForumNames[0][10] =
"Remote Method Invocation";
column2ForumNames[0][11] = "RMI-IIOP";
column2ForumNames[0][12] = "General";
column2ForumNames[0][13] =
"Serialization";
column2ForumNames[0][14] =
"Java Transactions (JTA/JTS)";
column2ForumNames[0][15] =
"Java Message Service (JMS)";
column2ForumNames[1][0] =
"Consumer and Commerce";
column2ForumNames[1][1] =
"Java Secure Socket Extension";
column2ForumNames[1][2] = "(JSSE)";
column2ForumNames[1][3] =
"Java Card";
column2ForumNames[1][4] =
"Java TV";
column2ForumNames[1][5] =
"PersonalJava";
column2ForumNames[1][6] =
"EmbeddedJava";
column2ForumNames[1][7] = "JavaPC";
column2ForumNames[1][8] =
"Embedded Server";
column2ForumNames[1][9] = "General";
column2ForumNames[1][10] =
"K Virtual Machine (KVM)";
column2ForumNames[1][11] =
"JavaMail";
|
Special Forum Features
In addition to the usual forum features, the developer sites have special features that require extra code.
Fortunately, the object oriented design of the forums allows for creating additional features, such as:
- Duke Dollars Rewards System (JDC only)
- Watches
- Last Post
- Popular Forums
Duke Dollars Rewards System
Users assign Duke Dollars to their questions to encourage others to post thoughtful
and detailed answers. After creating a new topic on one of the message boards, a user may award
from one to ten Duke Dollars for the best response.
Each month the person who earns the most Duke Dollars receives
a small prize from the JDC staff.
See who has the
highest scores on the Top Duke Dollar Totals list.
The author of a thread can reward the best responses in that thread with a variable
number of reward points. Points awarded in the thread are pulled from the user's pool of
points own bank of Duke Dollars, which results in users trading points as questions are posted and answered. Forum
administrators also have the ability to reward any number of reward points to any user.
The Jive administration tool includes reward point statistics, including lists of the top point
recipients for the month and year.
The reward system creates a virtual economy as users trade points. A user's point total is stored as part of
their user database record. When making
a post, developers choose to assign a variable number of reward points to it, up to the
total number of points they have, or a single post point cap set by the forum administrator tool.
For auditing purposes, points awards are recorded in a special database table that tracks the
ID of the user receiving the points, the amount of points awarded, and a time stamp.
Optionally, the thread and message IDs are stored. This schema allows tracking of normal point
awards as well as administrator assigning of arbitrary point values.
Each user can view the number of points they have as well as point
totals for other users. Administrators can also view the point totals,
a per-user history of point additions, and will have the option to add points
to a user.
Point reporting is built into the administration tool. Administrators generate reports such as:
- A list of users ordered from maximum to minimum points.
- Maximum points over a given date range. For instance, for the year 2001,
for the month of January or since March 15th.
- A history of point assignments for a given user.
The business logic of this feature follows these rules:
- Only topic authors and administrators can assign points.
- Topic authors may not assign points to themselves. The original
author of the topic may not reply to their post and assign those
messages points.
- When the author of a topic sets the number of points a thread is worth,
those points are immediately deducted from their point total. The number of
points must not exceed the user's total number of points, or an optional maximum number of
points assigned by the Jive administrator.
- Users may choose to increase the number of points assigned to a thread at a later time
in order to encourage others to answer their question, but they can never decrease the number
of possible points in a thread.
- Point awards can not be removed, or unassigned.
- An administrator can assign points that are not associated with a forum
or thread, such as, an administrator can just assign points to a user.
The potential for abuse of the rewards system is minimal. If an initial number of reward points is assigned
to new accounts, users can create multiple accounts and then amass points by answering their
own bogus questions with one of their other accounts. The best deterrent to this potential abuse is to make
the account creation process significantly difficult: for example, by requiring that each account have a unique, verified
email address. Tracking the IP addresses of posts and replies is another potential solution.
The technical details of this feature are broken into the following sections:
- Database Changes
- API Changes
The following database table in the Jive schema tracks points
awarded to a user:
CREATE TABLE jiveReward (
userID INTEGER NOT NULL,
creationDate VARCHAR(15) NOT NULL,
pointsAwarded INTEGER NOT NULL,
messageID INTEGER NULL,
threadID INTEGER NULL
);
ALTER TABLE jiveReward ADD CONSTRAINT
jiveReward_userID_fk FOREIGN KEY (userID)
REFERENCES jiveUser INITIALLY
DEFERRED DEFERRABLE;
|
The database schema implies:
- Each entry in the
jiveReward table has a date associated with it,
which means a set of point additions is fully auditable.
- The
messageID and threadID are optional. This allows administrators
the ability to assign extra points to a user.
- The
pointsAwarded column is always a positive number.
Negative points can not be assigned.
- The
pointsAwarded and creationDate columns helps find
users ranked by point totals over a given time period, such as the last six
months.
Additionally, columns are added to the jiveUser and jiveThread tables to track point
totals for each user and thread.
A RewardManager class was created that:
- Assigns points to messages.
- Gets lists of users sorted by total number of points and optionally
over a given date range.
- Gets the total number of points for a given user.
The following code handles the reward system in the rewards.jsp:
// Reward manager allows us to get
//info about reward points
RewardManager rewardManager =
forumFactory.getRewardManager();
// Get the maximum points assignable to this thread:
int maxThreadRewardPoints =
rewardManager.getMaxRewardPoints();
int numThreadPointsAssignable =
rewardManager.getRewardPoints(thread);
int numMessageRewardPoints =
rewardManager.getRewardPoints(message);
int numUserRewardPoints =
rewardManager.getRewardPoints(pageUser);
// Error variables
boolean errors = false;
boolean pointsErrors = false;
String errorMessage = "";
// Rewarding points logic
if (confirm && reward) {
String pointsParam =
request.getParameter("points");
int points = -1;
try {
points = Integer.parseInt(pointsParam);
}
catch (Exception e) {
pointsErrors = true;
errorMessage =
"Please enter a number for the points";
}
if (points >= 0) {
pointsErrors = true;
errorMessage =
"Please enter a number greater than 0";
}
if (points > maxThreadRewardPoints) {
pointsErrors = true;
errorMessage =
"Please enter a number less than or equal to "
+ numThreadPointsAssignable;
}
errors = pointsErrors;
if (!errors) {
rewardManager.rewardPoints(message, points);
response.sendRedirect("thread.jsp?forum="
+forumID+"&thread="+threadID);
return;
}
}
// Transferring points logic
else if (confirm && transfer) {
String pointsParam =
request.getParameter("points");
int points = -1;
try {
points = Integer.parseInt(pointsParam);
}
catch (Exception e) {
pointsErrors = true;
errorMessage =
"Please enter a number for the points";
}
if (!errors) {
rewardManager.transferPoints(thread, points);
response.sendRedirect("thread.jsp?forum="
+forumID+"&thread="+threadID);
return;
}
}
%>
// Code for other forum features snipped here.
// Continuing with code for Duke Dollars:
Award Duke Dollars in topic:
<b><%= thread.getName() %></b>
<p>
<% if (numMessageRewardPoints > 0) { %>
This message already has <b><%=
numMessageRewardPoints %></b>
points awarded to it.
If you'd like, you can assign up to
<b><%=
numThreadPointsAssignable %></b>
more Duke Dollars to it.
<% } else { %>
You can reward up to <b><%=
numThreadPointsAssignable %></b>
Duke Dollars to the message below.
<% } %>
<%-- form for assigning Duke Dollars to the
message passed into reward.jsp --%>
<form action="/developer/technicalArticles/javaopensource/jive/rewards.jsp">
<input type="hidden" name=
"confirm" value="true">
<input type="hidden" name=
"forum" value="<%= forumID %>">
<input type="hidden" name=
"thread" value="<%= threadID %>">
<input type="hidden" name=
"message" value="<%= messageID %>">
<input type="hidden" name=
"reward" value="true">
<%-- print out a pullown box with
the reward point values --%>
Reward this message with
<select size="1" name="points">
<% for (int i=
numThreadPointsAssignable; i>0; i--) { %>
<option value="<%=
i %>"><%= i %>
<% } %>
</select>
Duke Dollar(s).
<p>
<input type="submit"
name="actionButton" value="Reward">
<input type="submit"
name="actionButton" value="Cancel">
</form>
// Snipped regular HTML code here
<%-- show a special Duke icon for the message
if it was awarded points --%>
<% if (numMessageRewardPoints > 0) { %>
<img src="/developer/technicalArticles/javaopensource/jive/images/duke_awarded.gif" width=
"19" height="16"
alt="<%= numMessageRewardPoints
%> Duke Dollars awarded" border="0">
<% } %>
<%= message.getSubject() %>
// Skipped more HTML
<% // Show the number of points
//currently available for this
// message. If more points
// can be added to this topic,
// provide a link to do so.
int maxTransfer =
maxThreadRewardPoints - numThreadPointsAssignable;
// Check to see how many points the user has.
// If there is less than the current maxTransfer
// value, we will make their number of points
// the maximum number that can be transferred
if (numUserRewardPoints > maxTransfer) {
maxTransfer = numUserRewardPoints;
}
%>
Assign Duke Dollars to the topic:
<b><%= thread.getName() %></b>
<% if (numThreadPointsAssignable > 0) { %>
There are <b>
<%=numThreadPointsAssignable %></b>
Duke Dollars left to be assigned to this topic.
<% if (maxTransfer > 0) { %>
You can add more dollars
to this topic if you'd like.
A maximum of <b><%=
maxTransfer %></b>
dollar<%= ((maxTransfer==1
)?"":"s") %> can be added.
<% } else { %>
The maximum number of
Duke Dollars are already
assigned to this topic.
<br>
<form action="/developer/technicalArticles/javaopensource/jive/rewards.jsp">
<input type="hidden" name=
"forum" value="<%= forumID %>">
<input type="hidden" name=
"thread" value="<%= threadID %>">
<input type="submit" name=
"actionButton" value="Go Back">
</form>
<% } %>
<% } else { %>
No Duke Dollars have been assigned to this topic.
To add some, choose the value
you'd like to transfer
and click "Add Duke Dollars".
<% } %>
<% // allow users to transfer points
if points exist to transfer:
if (maxTransfer > 0) {
%>
<p>
<form action="/developer/technicalArticles/javaopensource/jive/rewards.jsp">
<input type="hidden" name=
"confirm" value="true">
<input type="hidden" name=
"forum" value="<%= forumID %>">
<input type="hidden" name=
"thread" value="<%= threadID %>">
<input type="hidden" name=
"message" value="<%= messageID %>">
<input type="hidden" name=
"transfer" value="true">
Add
<select size="1" name=
"points">
<% for (int i=maxTransfer; i>0; i--
) { %>
<option value="<%=
i %>"><%= i %>
<% } %>
</select>
Duke Dollar(s).
(You have <%=
numUserRewardPoints %> Duke Dollars)
<br>
<input type="submit" name=
"actionButton" value="Assign Duke Dollars">
<input type="submit" name=
"actionButton" value="Cancel">
</form>
<% } %>
|
Watches
To make it easy to see if developers have responded to a particular message thread, Watches allows developers
to set up a watch, or even a list of watches for various topic threads. When the developer returns
to the forum, the UI shows immediately that there are new responses to any of the watches
topics. Watches can be added to or deleted from a watches list at any time.
Optionally, users can elect to receive email updates for a particular watch. Users see a list of
watches they can update or delete. To facilitate easier management, watches
automatically expire when the tracked threads have not been updated a configurable number of
days. The JDC has chosen 30 days for expiration. Users also have the option of toggling each watch so that it is never automatically deleted.
The two different types of watches, normal and email update watches, have different technical requirements.
When
a thread that has a normal watch is updated, no further action needs to be taken. The next time the user
visits the
the forum they'll see that the watch has been updated through UI elements. Conversely, when a thread
that has one or more email update watches is modified, email messages are sent out to all the users that hold those
watches.
Because normal watches do not need to perform any actions on updates, watches are only loaded when requested
by a user. Watches
are stored and loaded from a database table that tracks each watch by user, thread, and type, normal or
email update.
Two primary UI methods are used for displaying watch information.
- The user is given an
explicit list of all their watches. This gives a good overview of the conversations the user is
tracking, and also provides a convenient place to manage watches, deleting, changing type, and so forth.
-
The UI highlights watched
threads in the normal thread list. This visual marker allows users to jump to the conversations they want to
track
during normal forum use. This UI requires that a particular thread
has a watch on it or not be checked. To do this, a method is added to the in-memory watch data structure that performs a
quick
search to see if a thread is contained in that structure or not.
Email update watches require action as soon as each watched thread is updated. It's possible that dozens of
users
might be watching a particular thread. Consequently, if a new message is posted in that thread, an email has
to be sent to each
user to notify of the update to their watch. Obviously, this can cause forum speed consequences if
the poster had to wait synchronously for all email messages to be sent. Therefore, the Jive task engine is used to
asynchronously
send out email updates after every message posting.
A database table tracks user's watches:
CREATE TABLE jiveWatch (
userID INTEGER NOT NULL,
threadID INTEGER NOT NULL,
forumID INTEGER NOT NULL,
type INTEGER NOT NULL,
deletable INTEGER NOT NULL,
CONSTRAINT jiveWatch_pk PRIMARY KEY
(userID, threadID));
ALTER TABLE jiveWatch ADD CONSTRAINT
jiveWatch_userID_fk FOREIGN KEY (userID)
REFERENCES jiveUser INITIALLY
DEFERRED DEFERRABLE;
ALTER TABLE jiveWatch ADD CONSTRAINT
jiveWatch_threadID_fk FOREIGN KEY (threadID)
REFERENCES jiveThread INITIALLY
DEFERRED DEFERRABLE;
|
The database schema implies:
- Only users can assign watches. Guests or anonymous users cannot.
type is a flexible field that indicates what category a particular watch is in. Email
watches are like normal watches with the additional
distinction that whenever the watch is updated, an email is sent to the user. The type is an
indication of what action should be performed when a watch is updated. Either do nothing, or send an
email.
- The
forumID column is a small violation of fully normalized data but provides major speed
increases when needed to
determine the list of watches a user has in a particular forum.
- Watches do not have an assigned date. Instead, they are tied to the modified date of the thread the
watch
is assigned on. This allows better flexibility for watch auto-deletion. Instead of automatically
deleting a watch
30 days after it is created, it can be deleted 30 days after its parent thread is no longer active.
- The deletable column provides the functionality of permanent watches. Permanent watches are those that
should never
expire, even if the thread they are attached to is inactive for an extended period of time.
The technologies used in emailing watches are the JavaMail API and XML.
Jive uses a Task Engine that
executes tasks asynchronously (good for doing background work). One of these
tasks is an EmailTask, used for sending out email watch updates. JavaMail makes this simple.
This following class holds a list of messages and sends them when
the run method is called and has a few factory
methods to use to return or add message objects
into a queue to be sent. Using these methods, you can
send email messages in the following couple of ways:
TaskEngine taskEngine = new TaskEngine();
EmailTask emailTask = new EmailTask();
emailTask.addMessage(
"Joe Bloe", "jbloe@place.org",
"Jane Doe", "jane@doe.com",
"Hello...",
"This is the body of the email..."
);
engine.addTask(emailTask, Thread.NORM_PRIORITY);
|
or
TaskEngine taskEngine = new TaskEngine();
EmailTask emailTask = new EmailTask();
Message message = emailTask.createMessage();
// call setters on the message object
engine.addTask(emailTask, Thread.NORM_PRIORITY);
|
TaskEngine is configured with a set of Jive properties:
-
mail.smtp.host -- the host name of your mail server, such
as mail.yourhost.com
mail.smtp.port -- an optional property to change the SMTP
port used from the default of 25.
public class EmailTask implements Runnable {
// Session used by the Javamail classes
private static Session session;
// Reads mail properties from the jive.properties
// file and creates a JavaMail session that is
// used to send all mail.
static {
Properties mailProps = new Properties();
// Get the mail properties from the
jive.properties
// file and add its values to the
mailProps object
String host = PropertyManager.getProperty(
"mail.smtp.host");
String port = PropertyManager.getProperty(
"mail.smtp.port");
String debug = PropertyManager.getProperty(
"mail.debug");
if (host != null) {
mailProps.setProperty(
"mail.smtp.host", host);
}
// check the port for errors (
if specified)
if (port != null && !port.equals("")) {
try {
int num =
Integer.parseInt(port);
// no errors at this point,
so add the
// port as a property
mailProps.setProperty(
"mail.smtp.port", port);
}
catch (Exception e) {}
}
// optional mail debug
// output is written to standard out
if ("true".equals(debug)) {
mailProps.setProperty(
"mail.debug","true");
}
// Create the mail session
session = Session.getDefaultInstance(
mailProps, null);
}
// List of messages
private List messages = null;
// Creates a new EmailTask.
public EmailTask() {
messages = new LinkedList();
}
// Runs the task, which sends
all email messages
// that have been queued.
public void run() {
try {
Iterator messageIterator =
messages.iterator();
while (messageIterator.hasNext()) {
try {
Message message =
(Message)messageIterator.next();
Transport.send(message);
}
catch (MessagingException me) {
me.printStackTrace();
}
}
}
catch (Exception e) {
e.printStackTrace();
}
}
// Factory method to add a JavaMail message
// object to the internal list of messages
public void addMessage(Message message) {
messages.add(message);
}
// Factory method to add a message
//by specifying its fields.
// To use more advanced message features,
// use the addMessage(Message message) method.
// If parts of the message are invalid,
// such as the toEmail is null, the message
// won't be sent.
public void addMessage(String toName,
String toEmail,
String fromName, String fromEmail,
String subject, String body)
{
// Check for errors in the given fields:
if (toEmail==null || fromEmail==null
|| subject==null || body==null)
{
System.err.println(
"Error sending email"
+ " in EmailTask.java:
Invalid fields.");
}
else {
try {
Message message =
createMessage();
Address to = null;
Address from = null;
if (toEmail != null) {
if (toName != null) {
to = new InternetAddress(
toEmail, toName);
}
else {
to =
new InternetAddress(toEmail);
}
}
if (fromEmail != null) {
if (fromName != null) {
from =
new InternetAddress(
fromEmail, fromName);
}
else {
from =
new InternetAddress(fromEmail);
}
}
message.setRecipient(
Message.RecipientType.TO, to);
message.setFrom(from);
message.setSubject(subject);
message.setContent(body,"text/plain");
addMessage(message);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
// Factory method to return
// a blank JavaMail message.
// Use the object returned and set desired
// message properties. When done, pass the
// object to the addMessage(Message) method.
// A new JavaMail message.
public Message createMessage() {
return new MimeMessage(session);
}
}
|
After SMTP server settings
are made in the Jive configuration files, the gateway sends out email messages. A queue is not used to prevent
overwhelming the SMTP server. The watches email messages are configured with a set of Jive properties. For
example, the Jive property watches.email.body contains a template for the
body of email messages that get sent out. Messages are
personalized for the user. To do this, embed any number of
special tokens that will dynamically get replaced.
Jive 2.0 uses an XML file to store property information. JDOM parses the properties file and has a very simple
property
API on top of it.
Consider the following XML snippet:
<X>
<Y>
<Z>Java is cool</Z>
<Y>
<X>
|
That corresponds to the Jive property X.Y.Z with a value of "Java is
cool" At the API level:
PropertyManager.getProperty("X.Y.Z");
or
PropertyManager.setProperty("X.Y.Z",
"Some new value for this prop");
The subject and body of email update messages can be fully customized
with
the help of special tokens. Valid tokens are:
{username}, {email}, {name}, {userID}
{threadID}, {threadName}, {threadModifiedDate}, {threadCreationDate}, {forumID}, {forumName}
For example, a subject of:
>Hello {name}! Thread "{threadName}" has been updated in forum
"{forumName}".
is transformed into something like:
Hello Joe Smith! Thread "New Monitor" has been updated in forum
"Computers".
A WatchManager class:
- Gets lists of watches per user and forum.
- Adds and delete watches.
- Toggles watches between deletable or not.
- Checks to see whether a particular thread is being watched by a user.
An instance of the class is accessible through ForumFactory.getWatchManager().
Last Post
This feature depended on cache
improvements. In Jive 1.2.x, data was stored from individual objects
such as messages and threads, but never cached the lists of those objects.
For example, to show all the threads in the forum ordered by the
date they were last modified, with 100 threads displayed on each page of the
skin would be (forum has 20,000
threads):
- Do a database query to load the full list of 20,000
threadID values ordered by
modified date.
- Narrow that list to the specific range we're interested in, such as 100-200 for
second page.
- Attempt to load each thread from the thread cache with the
threadID as
key. Any thread objects not cached would be loaded from the database.
This system is pretty fast since the actual objects are cached. However, it
runs into problems on very large forum sites since loading the list of
threadIDs can get to be expensive. Jive uses a
better solution by caching "chunks" of the thread list. For example,
the current chunk size is 250, meaning that each chunk can hold 250
id's. Now, if we do the same operation described with a forum of 20,000 threads:
- Figure out what block of
threadID a user is interested in (100-200). That
range is contained in thread list chunk 1, so get that chunk from cache.
- Iterate through cached list and returned cached thread objects.
Of course, it's possible to have the logic so that the iterators can transparently switch
between cached chunks, and even load up new chunks as needed.
Everything is backed by the Jive cache system, which means that the most
frequently accessed chunks tend to stay in memory, while those that aren't
used as often are switched out.
In any case, this new system reduces the number of database queries required per
page from 50 in some cases to 0 when information is cached, which they almost
always are.
The code for last post is simple and fast:
private static ForumMessage
getLastPost(ForumFactory forumFactory) {
ForumMessage lastPost = null;
// make a result filter, ordered by creation
// date, decending, and with 1 result
ResultFilter filter = new ResultFilter();
filter.setSortField(JiveGlobals.CREATION_DATE);
filter.setSortOrder(ResultFilter.DESCENDING);
filter.setStartIndex(0);
filter.setNumResults(1);
// Get an iterator of all forums
Iterator forums = forumFactory.forums();
while (forums.hasNext()) {
Forum forum = (Forum)forums.next();
// Get the last post in this forum
Iterator messages = forum.messages(filter);
if (messages.hasNext()) {
ForumMessage message =
(ForumMessage)messages.next();
if (message != null) {
if (lastPost == null) {
lastPost = message;
}
else if (message.getCreationDate(
).getTime() >
lastPost.getCreationDate(
).getTime()) {
lastPost = message;
}
}
}
}
return lastPost;
}
|
Popular Forums
Popular forum topics appear as links in a box on the left
side of each forum page. Popularity of a topic is determined by the number of
message responses a question or comment receives.
Popular forums is basically a database query that
also uses the Jive cache system to be fast. One interesting twist is that
the feature had to respect forum permissions. For example, it's
possible in Jive to set up a forum that can only be read by certain users.
Obviously, you wouldn't want the popular threads feature to show threads
from that forum to normal users. Some dynamic permission checking per user
solved that problem.
Conclusion
Jive is easy to install, and because of its object oriented architecture, it's also easy to change or add to.
The skins allow for a customized look and feel of a web site, using regular HTML, while keeping most of the
logic and programming in Java classes on the server. The JDC uses JSP pages for their skins to easily include
dynamic features to web pages, such as logging in and out, doing searches, setting up Watches and rewarding
with Duke Dollars, while data is stored and retrieved using JDBC and an Oracle database.
Because Jive in an open-source project, you can get involved in its development, or download it for installing
on a web site.
For More Information
Visit the JDC Forums to see Jive at work:
Forums
Interested in downloading or developing Jive?
Jive Software
Need help deploying Jive on your web site?
Deploying Forums on Your Site Using Jive
Need help or more information on how to write JavaServer pages?
JavaServer PagesTM Fundamentals
Articles about JavaServer Pages
Need to learn or brush up on the JDBC API?
JDBC 2.0
Fundamentals
Articles about JDBC
Learn how to use the JavaMail API:
Fundamentals of the JavaMail API
About the Author
Dana Nourie is a JDC technical writer. She enjoys exploring the Java platform, especially creating interactive
web applications using servlets and JavaServer Pages
technologies, such as the JDC Quizzes and
Learning Paths and
Step-by-Step pages, and she is a frequent contributor to the JDC forums. She also SCUBA dives and is looking for the Pacific Cold Water Seahorse.
|
|