Book HomeJava and XSLTSearch this book

7.4. Servlet Implementation

We are almost finished! The remaining piece of the puzzle is to coordinate activity between the web browser, database, domain objects, JDOM producers, and XSLT stylesheets. This task lies in the servlet implementation and related classes. In an XSLT-driven web application, the servlet itself really does not do all that much. Instead, it acts as a mediator between all of the other actions taking place in the application.

Figure 7-9 shows the UML class diagram for the com.oreilly.forum.servlet package. This design consists of a few key classes along with numerous subclasses of Renderer and ReqHandler. These subclasses are very repetitive in nature, which is indicative of the highly structured application design that XML and XSLT facilitate.

Figure 7-9

Figure 7-9. Servlet design

A single-servlet design has been adopted for this application. In this approach, the ForumServlet intercepts all inbound requests from clients. The requests are then delegated to subclasses of ReqHandler, which handle requests for individual pages. Once the request has been processed, a subclass of Renderer selects the XML and XSLT stylesheet. XSLTRenderHelper does the actual XSLT transformation, sending the resulting XHTML back to the browser.

This is not designed to be a heavyweight web application framework. Instead, it is just a simple set of coding conventions and patterns that help keep the application highly modular. It is easy to eliminate the ReqHandler classes and use several servlets instead. The main advantage of explicit request handlers and renderers is that the design is clearly modularized, which may promote more consistency across a team of developers.

The overall flow of control may be the hardest part to understand. Once this flow is clear, the implementation is a matter of creating additional request handlers and renderers. Figure 7-10 is a UML sequence diagram that shows how a single web browser request is intercepted and processed.

Figure 7-10

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.

The code for ForumServlet is shown in Example 7-25. As already mentioned, this is the only servlet in the application.

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.

The code for ReqHandler.java is shown in Example 7-26. This is an abstract class that provides doGet( ) and doPost( ) methods. By default, each method returns an error message back to the client, so a derived class must override one or both methods to enable HTTP GET and/or POST. Once the method is complete, the derived class must return an instance of Renderer, which produces the next page to display.

Example 7-26. ReqHandler.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * All request handlers must extend from this class.
 */
public abstract class ReqHandler {
    protected abstract String getPathInfo( );

    protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        return new ErrorRenderer("GET not allowed");
    }

    protected Renderer doPost(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        return new ErrorRenderer("POST not allowed");
    }
} 

The Renderer class is shown in Example 7-27. This class, like ReqHandler, is abstract. Derived classes are responsible for nothing more than producing content to the HttpServletResponse. Basically, each page in the discussion forum application is created using a subclass of Renderer.

Example 7-27. Renderer.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * All page renderers must extend from this class.
 */
public abstract class Renderer {
    public abstract void render(HttpServlet servlet,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException;
} 

The most basic renderer is ErrorRenderer, which is shown in Example 7-28. This class displays an error message in a web browser using simple println( ) statements that generate HTML. Unlike all other parts of this application, the ErrorRenderer class does not use XML and XSLT. The reason for this is that a large percentage of errors occurs because an XML parser is not properly configured on the CLASSPATH.[33] If this sort of error occurs, this renderer will not be affected.

[33] CLASSPATH issues are discussed in great detail in Chapter 9, "Development Environment, Testing, and Performance".

NOTE: ErrorRenderer can be written to use XML and XSLT, provided that a try/catch block catches any transformation errors and reverts to println( ) statements for error reporting.

Example 7-28. ErrorRenderer.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * Shows an error page. Since errors are frequently caused by improperly
 * configured JAR files, XML And XSLT are not used by this class.
 * If XML and XSLT were used, then the same CLASSPATH issue that caused
 * the original exception to occur would probably cause this page
 * to fail as well.
 */
public class ErrorRenderer extends Renderer {
    private String message;
    private Throwable throwable;

    public ErrorRenderer(Throwable throwable) {
        this(throwable, throwable.getMessage( ));
    }

    public ErrorRenderer(String message) {
        this(null, message);
    }

    public ErrorRenderer(Throwable throwable, String message) {
        this.throwable = throwable;
        this.message = message;
    }

    public void render(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        response.setContentType("text/html");
        PrintWriter pw = response.getWriter( );
        // just show a simple error page for now.
        pw.println("<html>");
        pw.println("<body>");
        pw.println("<p>");
        pw.println(this.message);
        pw.println("</p>");
        if (this.throwable != null) {
            pw.println("<pre>");
            this.throwable.printStackTrace(pw);
            pw.println("</pre>");
        }
        pw.println("</body></html>");
    }
} 

XSLTRenderHelper, shown in Example 7-29, is a utility class used by all remaining renderers. This class does the low-level XSLT transformations, eliminating a lot of duplicated code in each of the renderers. XSLTRenderHelper also maintains a cache of stylesheet filenames so they do not have to be repeatedly located using the ServletContext.getRealPath( ) method.

Example 7-29. XSLTRenderHelper.java

package com.oreilly.forum.servlet;

import com.oreilly.javaxslt.util.StylesheetCache;
import java.io.*;
import java.net.URL;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import org.jdom.*;
import org.jdom.output.*;

/**
 * A helper class that makes rendering of XSLT easier. This
 * eliminates the need to duplicate a lot of code for each
 * of the web pages in this app.
 */
public class XSLTRenderHelper {
    private static Map filenameCache = new HashMap( );

    /**
     * Perform an XSLT transformation.
     *
     * @param servlet provides access to the ServletContext so
     *                the XSLT directory can be determined.
     * @param xmlJDOMData JDOM data for the XML Document.
     * @param xsltBaseName the name of the stylesheet without a directory.
     * @param response the Servlet response to write output to.
     */
    public static void render(HttpServlet servlet, Document xmlJDOMData,
            String xsltBaseName, HttpServletResponse response)
            throws ServletException, IOException {

        String xsltFileName = null;
        try {
            // figure out the complete XSLT stylesheet file name
            synchronized (filenameCache) {
                xsltFileName = (String) filenameCache.get(xsltBaseName);
                if (xsltFileName == null) {
                    ServletContext ctx = servlet.getServletContext( );
                    xsltFileName = ctx.getRealPath(
                            "/WEB-INF/xslt/" + xsltBaseName);
                    filenameCache.put(xsltBaseName, xsltFileName);
                }
            }

            // write the JDOM data to a StringWriter
            StringWriter sw = new StringWriter( );
            XMLOutputter xmlOut = new XMLOutputter("", false, "UTF-8");
            xmlOut.output(xmlJDOMData, sw);

            response.setContentType("text/html");
            Transformer trans = StylesheetCache.newTransformer(xsltFileName);

            // pass a parameter to the XSLT stylesheet
            trans.setParameter("rootDir", "/forum/");

            trans.transform(new StreamSource(new StringReader(sw.toString( ))),
                            new StreamResult(response.getWriter( )));
        } catch (IOException ioe) {
            throw ioe;
        } catch (Exception ex) {
            throw new ServletException(ex);
        }
    }

    private XSLTRenderHelper( ) {
    }
} 

XSLTRenderHelper performs the XSLT transformation by first converting the JDOM Document into a String of XML and then reading that String back into a JAXP-compliant XSLT processor. This is not necessarily the most efficient way to integrate JDOM with JAXP, but it works reliably with some beta versions of JDOM. By the time you read this, JDOM will have more standardized APIs for integrating with JAXP.

Another utility class, ReqHandlerRegistry, is shown in Example 7-30. This class is responsible for locating instances of ReqHandler based on path information found in the request URL. Basically, path information is any text that occurs after a slash character (/) following the servlet mapping. HttpServletRequest includes a method called getPathInfo( ) that returns any path information that is present.

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.

The first real request handler, HomeReqHandler, is shown in Example 7-31. This class is quite simple and merely returns an instance of HomeRenderer. The code is simple because the home page does not have any modes of operation other than to display all message boards. Other request handlers are more complex because they must process HttpServletRequest parameters.

Example 7-31. HomeReqHandler.java

package com.oreilly.forum.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * This is the 'default' request handler in the app. The
 * first inbound request generally goes to an instance
 * of this class, which returns the home page renderer.
 */
public class HomeReqHandler extends ReqHandler {

    protected String getPathInfo( ) {
        return "home";
    }

    protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        return new HomeRenderer( );
    }

} 

All of the request handlers must override the getPathInfo( ) method. This determines the path info portion of the URL, so each request handler must return a unique string.

The renderer for the home page, shown in Example 7-32, is also quite simple. As with the home request handler, this renderer is simple because it has only one mode of operation. Like other renderers, this class gets some data from the database using the DataAdapter class, asks a JDOM producer to convert the data into XML, and then tells XSLTRenderHelper which XSLT stylesheet to use when performing the transformation.

Example 7-32. HomeRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Shows the home page.
 */
public class HomeRenderer extends Renderer {

    public void render(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        try {
            // get the data for the home page
            DataAdapter adapter = DataAdapter.getInstance( );

            // an iterator of BoardSummary objects
            Iterator boards = adapter.getAllBoards( );

            // convert the data into XML (a JDOM Document)
            Document doc = new Document(HomeJDOM.produceElement(boards));

            // apply the appropriate stylesheet
            XSLTRenderHelper.render(servlet, doc, "home.xslt", response);
        } catch (DataException de) {
            new ErrorRenderer(de).render(servlet, request, response);
        }
    }
} 

ViewMonthReqHandler, shown in Example 7-33, is slightly more complex than the home page request handler. Since this request handler requires the board id, month number, and year number as parameters, it must perform validation before it can handle the request properly.

Example 7-33. ViewMonthReqHandler.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.*;

/**
 * Handle a request to view a month for a message board.
 */
public class ViewMonthReqHandler extends ReqHandler {

    protected String getPathInfo( ) {
        return "viewMonth";
    }

    protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        try {
            DataAdapter adapter = DataAdapter.getInstance( );

            // these are all required parameters
            long boardID = 0L;
            int month = 0;
            int year = 0;
            try {
                boardID = Long.parseLong(request.getParameter("boardID"));
                month = Integer.parseInt(request.getParameter("month"));
                year = Integer.parseInt(request.getParameter("year"));
            } catch (Exception ex) {
                return new ErrorRenderer("Invalid request");
            }
            BoardSummary board = adapter.getBoardSummary(boardID);
            if (board == null) {
                return new ErrorRenderer("Invalid request");
            }

            return new ViewMonthRenderer(board, new MonthYear(month, year));
        } catch (DataException de) {
            return new ErrorRenderer(de);
        }
    }
} 

Throughout this application, a seemingly harsh approach to error handling is followed. If any "impossible" requests are detected, the user is presented with a terse error message:

try {
    boardID = Long.parseLong(request.getParameter("boardID"));
    month = Integer.parseInt(request.getParameter("month"));
    year = Integer.parseInt(request.getParameter("year"));
} catch (Exception ex) {
    return new ErrorRenderer("Invalid request");
}

When considering error-handling approaches, the primary concern should be break-in attempts by hackers. It is far too easy for a user to determine which parameters are passed to a web application and then try to wreak havoc by manually keying in various permutations of those parameters. By checking for illegal parameters and simply rejecting them as invalid, a web application gains a big security advantage.

Web Application Security

 

In the ViewMonthRegHandler class, a NumberFormatException is thrown if any of these parameters are nonnumeric or null. Basically, there are only two possible causes for this sort of error. First, one of the XSLT stylesheets may have a bug, making it forget to pass one of these required parameters. If this is the case, a developer should theoretically catch this error during development and testing. The second possibility is that someone is manually keying in parameters without using the standard XHTML user interface. This could be a hacker attacking the site by probing for an application error, so we simply deny the request.

 

Standalone GUI applications do not have to contend with such issues because the user interface can prevent illegal user input. But web applications are essentially wide open for the entire world to see, so developers must adopt a highly defensive style of programming. If suppressing hack attempts is not a priority, the code could simply redirect the user to the home page when an illegal request occurs. It might be a good idea to write a log file entry that contains the requesting user's IP address and any other relevant information when errors occur. Log entries can be very useful when diagnosing application bugs as well.

ViewMonthRenderer is shown in Example 7-34. This is another simple class that displays an entire month's worth of messages in a given board. Although the XHTML display can be quite complex for this page, the JDOM producer and XSLT stylesheet handle the real work, keeping the Java code to a minimum.

Example 7-34. ViewMonthRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.adapter.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Renders a page that shows all messages in a given month.
 */
public class ViewMonthRenderer extends Renderer {

    private BoardSummary board;
    private MonthYear month;

    public ViewMonthRenderer(BoardSummary board, MonthYear month) {
        this.board = board;
        this.month = month;
    }

    public void render(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        try {
            // convert the data into XML (a JDOM Document)
            Document doc = new Document(ViewMonthJDOM.produceElement(
                    this.board, this.month));

            // apply the appropriate stylesheet
            XSLTRenderHelper.render(servlet, doc,
                    "viewMonth.xslt", response);
        } catch (DataException de) {
            throw new ServletException(de);
        }
    }
} 

ViewMsgReqHandler, shown in Example 7-35, requires a parameter named msgID. As before, if this parameter is invalid, an error page is displayed to the user. Otherwise, an instance of ViewMsgRenderer is returned to the servlet.

Example 7-35. ViewMsgReqHandler.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.*;

/**
 * Handle a request to view a message.
 */
public class ViewMsgReqHandler extends ReqHandler {

    protected String getPathInfo( ) {
        return "viewMsg";
    }

    protected Renderer doGet(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {
        try {
            DataAdapter adapter = DataAdapter.getInstance( );

            // msgID is a required parameter and must be valid
            String msgIDStr = request.getParameter("msgID");

            if (msgIDStr == null) {
                servlet.log("Required parameter 'msgID' was missing");
                return new ErrorRenderer("Invalid request");
            }

            Message msg = adapter.getMessage(Long.parseLong(msgIDStr));
            MessageSummary inResponseTo = null;
            if (msg.getInReplyTo( ) > -1) {
                inResponseTo = adapter.getMessage(msg.getInReplyTo( ));
            }
            return new ViewMsgRenderer(msg, inResponseTo);
        } catch (NumberFormatException nfe) {
            servlet.log("'msgID' parameter was not a number");
            return new ErrorRenderer("Invalid request");
        } catch (DataException de) {
            return new ErrorRenderer(de);
        }
    }
} 

The corresponding renderer, ViewMsgRenderer, is shown in Example 7-36. This class follows the same basic approach as other renderers: it produces a JDOM Document and uses XSLTRenderHelper to perform the XSLT transformation.

Example 7-36. ViewMsgRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Show the "view message" page.
 */
public class ViewMsgRenderer extends Renderer {

    private Message message;
    private MessageSummary inResponseTo;

    public ViewMsgRenderer(Message message, MessageSummary inResponseTo) {
        this.message = message;
        this.inResponseTo = inResponseTo;
    }

    public void render(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {

        // convert the data into XML (a JDOM Document)
        Document doc = new Document(ViewMessageJDOM.produceElement(
                this.message, this.inResponseTo));

        // apply the appropriate stylesheet
        XSLTRenderHelper.render(servlet, doc, "viewMsg.xslt", response);
    }
} 

The next class, PostMsgReqHandler, is shown in Example 7-37. In the doGet( ) method, the mode parameter indicates whether the user is trying to post a new message or reply to an existing message. The doGet( ) method is invoked as a result of an HTTP GET request, such as the user clicking on a hyperlink or typing in a URL.

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.

The source code for PostMsgRenderer is shown in Example 7-38.

Example 7-38. PostMsgRenderer.java

package com.oreilly.forum.servlet;

import com.oreilly.forum.*;
import com.oreilly.forum.domain.*;
import com.oreilly.forum.xml.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;

/**
 * Show the web page that allows a user to post or reply to
 * a message.
 */
public class PostMsgRenderer extends Renderer {
    private MessageSummary inResponseToMsg;
    private BoardSummary board;
    private String msgSubject;
    private String authorEmail;
    private String msgText;
    private boolean showError;

    /**
     * This constructor indicates that the user is replying to an
     * existing message.
     */
    public PostMsgRenderer(Message inResponseToMsg) {
        this.board = inResponseToMsg.getBoard( );
        this.inResponseToMsg = inResponseToMsg;
        this.showError = false;
        this.msgSubject = "Re: " + inResponseToMsg.getSubject( );
        this.authorEmail = "";

        StringTokenizer st = new StringTokenizer(
                inResponseToMsg.getText( ), "\n");
        StringBuffer buf = new StringBuffer( );
        buf.append("\n");
        buf.append("\n> -----Original Message-----");
        buf.append("\n>   Posted by ");
        buf.append(inResponseToMsg.getAuthorEmail( ));
        buf.append(" on ");
        buf.append(inResponseToMsg.getCreateDate().toString( ));
        buf.append("\n");
        while (st.hasMoreTokens( )) {
            String curLine = st.nextToken( );
            buf.append("> ");
            buf.append(curLine);
            buf.append("\n");
        }
        buf.append("> ");
        this.msgText = buf.toString( );
    }

    /**
     * This constructor indicates that the user is posting
     * a new message.
     */
    public PostMsgRenderer(BoardSummary board) {
        this(board, null, false, "", "", "");
    }

    /**
     * This constructor is used when the user submitted a form
     * but did not fill out all required fields.
     */
    public PostMsgRenderer(BoardSummary board,
            MessageSummary inResponseToMsg,
            boolean showError,
            String msgSubject,
            String authorEmail,
            String msgText) {
        this.board = board;
        this.inResponseToMsg = inResponseToMsg;
        this.showError = showError;
        this.msgSubject = msgSubject;
        this.authorEmail = authorEmail;
        this.msgText = msgText;
    }

    public void render(HttpServlet servlet, HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {

        // convert the data into XML (a JDOM Document)
        Document doc = new Document(PostMessageJDOM.produceElement(
                this.board,
                this.inResponseToMsg,
                this.showError,
                this.msgSubject,
                this.authorEmail,
                this.msgText));

        // apply the appropriate stylesheet
        XSLTRenderHelper.render(servlet, doc, "postMsg.xslt", response);
    }
} 

As the code shows, this class has several constructors that support different modes of operation. The first constructor does the most work, prefixing the original message with > characters as many email clients do when creating replies to existing messages. Other than having several constructors, however, the renderer works just like other renderers in the application. The JDOM producer and XSLT stylesheet actually do most of the work, distinguishing between the various modes of operation.



Library Navigation Links

Copyright © 2002 O'Reilly & Associates. All rights reserved.