Figure 7-10. Sequence diagram
When a browser issues a request, it is always directed to the single
servlet. This servlet then locates the appropriate request handler
based on information found in the requested URL. The request handler
is responsible for interacting with the data adapter layer to create
and update domain objects and for creating the appropriate renderer.
Once the renderer is created, the servlet asks it to render(
) its content. The renderer then asks the appropriate JDOM
producer to create the XML data and performs the transformation using
an XSLT stylesheet. The result of the transformation is sent back to
the client browser.
One request handler might map to several renderers. For example,
suppose the user is trying to post a new message and submits this
information to the PostMsgReqHandler class. If the
request handler determines that some required fields are missing, it
can return an instance of the PostMsgRenderer
class. This allows the user to fill in the remaining fields. On the
other hand, if a database error occurs, an instance of
ErrorRenderer can be returned. Otherwise,
ViewMsgRenderer is returned when the message is
successfully posted. Because request handlers and renderers are
cleanly separated, renderers can be invoked from any request handler.
Example 7-25. ForumServlet.java
package com.oreilly.forum.servlet;
import com.oreilly.forum.ForumConfig;
import com.oreilly.forum.jdbcimpl.DBUtil;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
/**
* The single servlet in the discussion forum.
*/
public class ForumServlet extends HttpServlet {
private ReqHandlerRegistry registry;
/**
* Registers all request handlers and sets up the
* ForumConfig object.
*/
public void init(ServletConfig sc) throws ServletException {
super.init(sc);
// get initialization parameters from the deployment
// descriptor (web.xml)
String jdbcDriverClassName = sc.getInitParameter(
"jdbcDriverClassName");
String databaseURL = sc.getInitParameter(
"databaseURL");
String adapterClassName = sc.getInitParameter(
"adapterClassName");
ForumConfig.setValues(jdbcDriverClassName,
databaseURL, adapterClassName);
try {
// load all request handlers
this.registry = new ReqHandlerRegistry(new HomeReqHandler( ));
this.registry.register(new PostMsgReqHandler( ));
this.registry.register(new ViewMonthReqHandler( ));
this.registry.register(new ViewMsgReqHandler( ));
} catch (Exception ex) {
log(ex.getMessage( ), ex);
throw new UnavailableException(ex.getMessage( ), 10);
}
}
/**
* Closes all database connections. This method is invoked
* when the Servlet is unloaded.
*/
public void destroy( ) {
super.destroy( );
DBUtil.closeAllConnections( );
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws IOException,
ServletException {
ReqHandler rh = this.registry.getHandler(request);
Renderer rend = rh.doPost(this, request, response);
rend.render(this, request, response);
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException,
ServletException {
ReqHandler rh = this.registry.getHandler(request);
Renderer rend = rh.doGet(this, request, response);
rend.render(this, request, response);
}
}
ForumServlet overrides the init(
) method to perform one-time initialization before any
client requests are handled. This is where context initialization
parameters are read from the deployment descriptor and stored in the
ForumConfig instance:
String jdbcDriverClassName = sc.getInitParameter("jdbcDriverClassName");
String databaseURL = sc.getInitParameter("databaseURL");
String adapterClassName = sc.getInitParameter("adapterClassName");
ForumConfig.setValues(jdbcDriverClassName, databaseURL, adapterClassName);
The init( ) method then sets up instances of each
type of request handler. These are registered with the
ReqHandlerRegistry class, which has the ability to
locate request handlers later on.
In the destroy( ) method, which is called when the
servlet is unloaded, any outstanding database connections are closed:
public void destroy( ) {
super.destroy( );
DBUtil.closeAllConnections( );
}
While this currently has no real effect, the code was put in place
because a future version of the software may use database connection
pooling. This allows the application to close all connections in the
pool just before exiting.
The only remaining methods in the servlet are doGet(
) and doPost( ), which are virtually
identical. All these methods do is locate the appropriate request
handler instance, ask the handler to perform a GET or POST, and then
use the renderer to send a response to the client.
Example 7-30. ReqHandlerRegistry.java
package com.oreilly.forum.servlet;
import java.util.*;
import javax.servlet.http.*;
/**
* A utility class that locates request handler instances based
* on extra path information.
*/
public class ReqHandlerRegistry {
private ReqHandler defaultHandler;
private Map handlerMap = new HashMap( );
public ReqHandlerRegistry(ReqHandler defaultHandler) {
this.defaultHandler = defaultHandler;
}
public void register(ReqHandler handler) {
this.handlerMap.put(handler.getPathInfo( ), handler);
}
public ReqHandler getHandler(HttpServletRequest request) {
ReqHandler rh = null;
String pathInfo = request.getPathInfo( );
if (pathInfo != null) {
int firstSlashPos = pathInfo.indexOf('/');
int secondSlashPos = (firstSlashPos > -1) ?
pathInfo.indexOf('/', firstSlashPos+1) : -1;
String key = null;
if (firstSlashPos > -1) {
if (secondSlashPos > -1) {
key = pathInfo.substring(firstSlashPos+1, secondSlashPos);
} else {
key = pathInfo.substring(firstSlashPos+1);
}
} else {
key = pathInfo;
}
if (key != null && key.length( ) > 0) {
rh = (ReqHandler) this.handlerMap.get(key);
}
}
return (rh != null) ? rh : this.defaultHandler;
}
}
Throughout the discussion forum application, URLs take on the
following form:
http://hostname:port/forum/main/home
In this URL, forum represents the web application
and is the name of the WAR file. The next part of the URL,
main, is a mapping to
ForumServlet. Since the WAR file and servlet will
not change, this part of the URL remains constant. The remaining
data, /home, is path information. This is the
portion of the URL that ReqHandlerRegistry uses to
locate instances of ReqHandler. If the path
information is null or does not map to any request
handlers, the default request handler is returned. This simply
returns the user to the home page.
Example 7-37. PostMsgReqHandler.java
package com.oreilly.forum.servlet;
import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
/**
* Handles GET and POST requests for the page that allows users
* to post or reply to a message.
*/
public class PostMsgReqHandler extends ReqHandler {
protected String getPathInfo( ) {
return "postMsg";
}
/**
* When an HTTP GET is issued, show the web page for the
* first time.
*/
protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
try {
// mode must be "postNewMsg" or "replyToMsg"
String mode = request.getParameter("mode");
DataAdapter adapter = DataAdapter.getInstance( );
if ("replyToMsg".equals(mode)) {
long origMsgID = Long.parseLong(
request.getParameter("origMsgID"));
Message inResponseToMsg = adapter.getMessage(origMsgID);
if (inResponseToMsg != null) {
return new PostMsgRenderer(inResponseToMsg);
}
} else if ("postNewMsg".equals(mode)) {
long boardID = Long.parseLong(
request.getParameter("boardID"));
BoardSummary board = adapter.getBoardSummary(boardID);
if (board != null) {
return new PostMsgRenderer(board);
}
}
return new ErrorRenderer("Invalid request");
} catch (NumberFormatException nfe) {
return new ErrorRenderer(nfe);
} catch (DataException de) {
return new ErrorRenderer(de);
}
}
/**
* Handles HTTP POST requests, indicating that the user has
* filled in the form and pressed the Submit button.
*/
protected Renderer doPost(HttpServlet servlet, HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// if the user hit the Cancel button, return to the home page
if (request.getParameter("cancelBtn") != null) {
return new HomeRenderer( );
}
// lots of error checking follows...
if (request.getParameter("submitBtn") == null) {
servlet.log("Expected 'submitBtn' parameter to be present");
return new ErrorRenderer("Invalid request");
}
// a null parameter indicates either a hack attempt, or a
// syntax error in the HTML
String mode = request.getParameter("mode");
String msgSubject = request.getParameter("msgSubject");
String authorEmail = request.getParameter("authorEmail");
String msgText = request.getParameter("msgText");
if (mode == null || msgSubject == null || authorEmail == null
|| msgText == null) {
return new ErrorRenderer("Invalid request");
}
// one of these may be null
String origMsgIDStr = request.getParameter("origMsgID");
String boardIDStr = request.getParameter("boardID");
if (origMsgIDStr == null && boardIDStr == null) {
return new ErrorRenderer("Invalid request");
}
long origMsgID = 0;
long boardID = 0;
try {
origMsgID = (origMsgIDStr != null) ? Long.parseLong(origMsgIDStr) : 0;
boardID = (boardIDStr != null) ? Long.parseLong(boardIDStr) : 0;
} catch (NumberFormatException nfe) {
return new ErrorRenderer("Invalid request");
}
// remove extra whitespace then verify that the user filled
// in all required fields
msgSubject = msgSubject.trim( );
authorEmail = authorEmail.trim( );
msgText = msgText.trim( );
try {
DataAdapter adapter = DataAdapter.getInstance( );
if (msgSubject.length( ) == 0
|| authorEmail.length( ) == 0
|| msgText.length( ) == 0) {
BoardSummary board = (boardIDStr == null) ? null
: adapter.getBoardSummary(boardID);
MessageSummary inResponseToMsg = (origMsgIDStr == null) ? null
: adapter.getMessage(origMsgID);
return new PostMsgRenderer(board, inResponseToMsg,
true, msgSubject, authorEmail, msgText);
}
//
// If this point is reached, no errors were detected so the
// new message can be posted, or a response can be created
//
Message msg = null;
if ("replyToMsg".equals(mode)) {
msg = adapter.replyToMessage(origMsgID, msgSubject,
authorEmail, msgText);
} else if ("postNewMsg".equals(mode)) {
msg = adapter.postNewMessage(boardID, msgSubject,
authorEmail, msgText);
}
if (msg != null) {
MessageSummary inResponseTo = null;
if (msg.getInReplyTo( ) > -1) {
inResponseTo = adapter.getMessage(msg.getInReplyTo( ));
}
return new ViewMsgRenderer(msg, inResponseTo);
}
return new ErrorRenderer("Invalid request");
} catch (DataException dex) {
return new ErrorRenderer(dex);
}
}
}
Unlike other request handlers in this application,
PostMsgReqHandler also has a doPost(
) method. The doGet( ) method is
responsible for returning a renderer that displays the XHTML form,
while the doPost( ) method is responsible for
processing the form submission. Because the XHTML form contains
several required fields and buttons, the doPost( )
method is far more complex than doGet( ). As the
code reveals, almost all of this complexity is introduced because of
error checking and validation logic.
The doPost( ) method checks for illegal/impossible
parameters first, returning an error page if any problems occur.
Next, it checks to see what the user typed in. If the user left a
required field blank, the parameter value will be an empty string
rather than null. Of course, leading and trailing
spaces must be trimmed in case the user hit the space bar:
msgSubject = msgSubject.trim( );
authorEmail = authorEmail.trim( );
msgText = msgText.trim( );
If any of these fields are empty, the
PostMsgRenderer is returned with form field values
pre-filled:
return new PostMsgRenderer(board, inResponseToMsg,
true, msgSubject, authorEmail, msgText);
This gives the user an opportunity to fill in missing values and try
to submit the form again. If all is well, an instance of
ViewMsgRenderer is returned. This allows the user
to view the message that was just submitted.