Previous | Next
Another Servlet ExampleIn our next example, the servlet will utilize DOM and XSLT to create its web pages. This achieves our goal of separation between data and presentation, making it possible to fully customize the HTML output without making any changes to the Java code. Although an XML approach makes the code more complex for a small example program such as this, the benefits quickly outweigh the costs as web applications get more sophisticated. The same is true for an Enterprise JavaBeans approach. For a trivial program, the configuration requirements seem very complex; but as the application complexity increases, the benefits of a sophisticated architecture become obvious. Our program will consist of two web pages, allowing visitors to enter personal information. The first page will prompt for their name, phone, and email, and the second page will display a summary of the data that was entered. The first page does validation, forcing the user to enter all of the required fields. DesignThe primary goal of this small application is to demonstrate how to use XSLT from a servlet. Specifically, JAXP will be used with DOM to create some dynamic XML data, then XSLT stylesheets will be used to transform that data into HTML. The design is presented in Figure 6-4. Figure 6-4. Personal data designAs Figure 6-4 shows, The The decision to use a separate helper class for XML generation was not arbitrary. Many people like to put code directly into classes like The first web page is shown in Figure 6-5. As you can see, required fields are marked with an asterisk (*). This screen is rendered using editPersonalData.xslt. Figure 6-5. Blank personal information formFigure 6-6 shows how this same screen looks after the user clicks on the Submit button. If data is missing, an error message is displayed in red and required fields are marked in bold. Any other error messages are also displayed in red. This view is also rendered using Figure 6-6. Personal information form with errorsOnce all of the data has been entered properly, the screen shown in Figure 6-7 is displayed. Unlike the previous examples, this screen is rendered using confirmPersonalData.xslt. To make changes to any of these screens, one needs to edit only the appropriate stylesheet. Figure 6-7. Confirmation pageXML and StylesheetsDeciding how to structure your XML can have significant impact on your ability to customize the output of a web application. In our current example, the same XML file is used for all web pages. This XML is shown in Example 6-3. Example 6-3. Example XML output<?xml version="1.0" encoding="UTF-8"?> <page> <!-- the next element is optional: --> <!-- <requiredFieldsMissing/> --> <personalData> <firstName required="true">Eric</firstName> <lastName required="true">Burke</lastName> <daytimePhone required="true">636-123-4567</daytimePhone> <eveningPhone/> <email required="true">burke_e@yahoo.com</email> </personalData> </page> As you can see, the XML is very minimal. None of the captions, such as The It is important to mention that this XML is used only for documentation purposes and for testing the XSLT stylesheets. Once you move into a production environment, the XML will be generated dynamically using the The XSLT stylesheet that creates the HTML form is shown in Example 6-4. The stylesheets are substantially longer than the XML data, which is typical. In a more simplistic approach to servlet development, all of this logic would be hardcoded into the source code as a series of Example 6-4. editPersonalData.xslt<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml"> <xsl:output method="xml" indent="yes" encoding="UTF-8" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/> <!-- *************************************************************** ** Top level template. Creates the framework for the XHTML page ************************************************************--> <xsl:template match="/"> <html> <head><title>Edit Personal Information</title></head> <body> <xsl:apply-templates select="page/personalData"/> </body> </html> </xsl:template> <!-- *************************************************************** ** Match the <personalData> element. ************************************************************--> <xsl:template match="personalData"> <h3>Personal Information</h3> <xsl:if test="../requiredFieldsMissing"> <div > Error: one or more required fields are missing. </div> </xsl:if> <i>Fields marked with (*) are required.</i> <form action="/chap6/personalData" method="post"> <table > <!-- Select all immediate children, such as firstName, lastName, daytimePhone, etc... --> <xsl:apply-templates/> </table> <div > <hr/> <input type="submit" name="submitBtn" value="Submit"/> </div> </form> </xsl:template> <!-- *************************************************************** ** Output a new row in the table for each field. ************************************************************--> <xsl:template match="firstName|lastName|daytimePhone|eveningPhone|email"> <tr> <xsl:if test="@required="true" and ../../requiredFieldsMissing and .="""> <xsl:attribute name="style"> <xsl:text>color:red; font-weight:bold;</xsl:text> </xsl:attribute> </xsl:if> <td> <xsl:choose> <xsl:when test="name( )="firstname""> First Name:</xsl:when> <xsl:when test="name( )="lastname""> Last Name:</xsl:when> <xsl:when test="name( )="daytimephone""> Daytime Phone:</xsl:when> <xsl:when test="name( )="eveningphone""> Evening Phone:</xsl:when> <xsl:when test="name( )="email""> Email:</xsl:when> </xsl:choose> </td> <td> <input type="text" name="{name( )}" value="{.}"/> </td> <td> <xsl:if test="@required="true"">*</xsl:if> </td> </tr> </xsl:template> </xsl:stylesheet> The first seven lines of editPersonalData.xslt contain boilerplate code that configures the XSLT processor to produce XHTML 1.0 using the transitional DTD. In particular, our result tree uses the The next template matches the <xsl:template match="personalData"> <h3>Personal Information</h3> <xsl:if test="../requiredFieldsMissing"> <div > Error: one or more required fields are missing. </div> </xsl:if> This template then produces the
<i>Fields marked with (*) are required.</i> <form action="/chap6/personalData" method="post"> The template finally produces a table so that all of the headings and text fields are properly aligned. As in earlier stylesheet examples, this template creates the table, while another template creates each row in the table: <table > <!-- Select all immediate children, such as firstName, lastName, daytimePhone, etc... --> <xsl:apply-templates/> </table> <div > <hr/> <input type="submit" name="submitBtn" value="Submit"/> </div> </form> </xsl:template> Since this particular instance of <xsl:template match="firstName|lastName|daytimePhone|eveningPhone|email"> This template first produces a <tr> <xsl:if test="@required="true" and ../../requiredFieldsMissing and .="""> <xsl:attribute name="style"> <xsl:text>color:red; font-weight:bold;</xsl:text> </xsl:attribute> </xsl:if> The template then produces its first <td> <xsl:choose> <xsl:when test="name( )="firstname""> First Name:</xsl:when> <xsl:when test="name( )="lastname""> Last Name:</xsl:when> <xsl:when test="name( )="daytimephone""> Daytime Phone:</xsl:when> <xsl:when test="name( )="eveningphone""> Evening Phone:</xsl:when> <xsl:when test="name( )="email""> Email:</xsl:when> </xsl:choose> </td> This is still better than hardcoding the captions into the XML or servlet because we can make changes to the stylesheet without recompiling anything. You can even change the captions to a foreign language without affecting any of the Java code, offering remarkable flexibility to web page designers. The next column in the table contains the input field: <td> <input type="text" name="{name( )}" value="{.}"/> </td> In the XHTML output, this yields a cell containing <td> <xsl:if test="@required="true"">*</xsl:if> </td> </tr> </xsl:template> The next stylesheet, confirmPersonalData.xslt, is listed in Example 6-5. This stylesheet is shorter because it shows only a summary of what the user entered on the previous page. It does not have to display any error messages or show an HTML form. The overall structure of the stylesheet is identical to editPersonalData.xslt, however, so a line-by-line description is not necessary. Example 6-5. confirmPersonalData.xslt<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml"> <xsl:output method="xml" indent="yes" encoding="UTF-8" doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"/> <!-- *************************************************************** ** Top level template. Creates the framework for the XHTML page ************************************************************--> <xsl:template match="/"> <html> <head> <title>Personal Data Summary</title> </head> <body> <xsl:apply-templates select="page/personalData"/> </body> </html> </xsl:template> <!-- *************************************************************** ** Match the <personalData> element. ************************************************************--> <xsl:template match="personalData"> <h2>Thank You!</h2> <h3>Your Information...</h3> <table > <!-- Select all immediate children, such as firstName, lastName, daytimePhone, etc... --> <xsl:apply-templates/> </table> <p><a href="/chap6/personaldata">Click here to edit this information...</a></p> </xsl:template> <!-- *************************************************************** ** Output a new row in the table for each field. ************************************************************--> <xsl:template match="firstName|lastName|daytimePhone|eveningPhone|email"> <tr> <td> <xsl:choose> <xsl:when test="name( )="firstname""> First Name:</xsl:when> <xsl:when test="name( )="lastname""> Last Name:</xsl:when> <xsl:when test="name( )="daytimephone""> Daytime Phone:</xsl:when> <xsl:when test="name( )="eveningphone""> Evening Phone:</xsl:when> <xsl:when test="name( )="email""> Email:</xsl:when> </xsl:choose> </td> <td> <b><xsl:value-of select="."/></b> </td> </tr> </xsl:template> </xsl:stylesheet> Source CodeThe first piece of source code to examine is shown in Example 6-6. The Example 6-6. PersonalData.javapackage chap6; /** * A helper class that stores personal information. XML generation * is intentionally left out of this class. This class ensures * that its data cannot be null, nor can it contain extra * whitespace. */ public class PersonalData { private String firstName; private String lastName; private String daytimePhone; private String eveningPhone; private String email; public PersonalData( ) { this("", "", "", "", ""); } public PersonalData(String firstName, String lastName, String daytimePhone, String eveningPhone, String email) { this.firstName = cleanup(firstName); this.lastName = cleanup(lastName); this.daytimePhone = cleanup(daytimePhone); this.eveningPhone = cleanup(eveningPhone); this.email = cleanup(email); } /** * <code>eveningPhone</code> is the only optional field. * * @return true if all required fields are present. */ public boolean isValid( ) { return this.firstName.length( ) > 0 && this.lastName.length( ) > 0 && this.daytimePhone.length( ) > 0 && this.email.length( ) > 0; } public void setFirstName(String firstName) { this.firstName = cleanup(firstName); } public void setLastName(String lastName) { this.lastName = cleanup(lastName); } public void setDaytimePhone(String daytimePhone) { this.daytimePhone = cleanup(daytimePhone); } public void setEveningPhone(String eveningPhone) { this.eveningPhone = cleanup(eveningPhone); } public void setEmail(String email) { this.email = cleanup(email); } public String getFirstName( ) { return this.firstName; } public String getLastName( ) { return this.lastName; } public String getDaytimePhone( ) { return this.daytimePhone; } public String getEveningPhone( ) { return this.eveningPhone; } public String getEmail( ) { return this.email; } /** * Cleanup the String parameter by replacing null with an * empty String, and by trimming whitespace from non-null Strings. */ private static String cleanup(String str) { return (str != null) ? str.trim( ) : ""; } } Although the public PersonalData( ) { this("", "", "", "", ""); } Additionally, all of the set methods make use of the private private static String cleanup(String str) { return (str != null) ? str.trim( ) : ""; } As a result, instances of this class will avoid null references and whitespace, eliminating the need to perform constant error checking in the servlet and XML generation classes. Trimming whitespace is particularly helpful because a user may simply press the spacebar in one of the required fields, potentially bypassing your validation rules. The public boolean isValid( ) { return this.firstName.length( ) > 0 && this.lastName.length( ) > 0 && this.daytimePhone.length( ) > 0 && this.email.length( ) > 0; } The only field that is not required is The next class, Example 6-7. PersonalDataXML.javapackage chap6; import javax.xml.parsers.*; import org.w3c.dom.*; /** * Responsible for converting a PersonalData object into an XML * representation using DOM. */ public class PersonalDataXML { /** * @param personalData the data to convert to XML. * @param includeErrors if true, an extra field will be included in * the XML, indicating that the browser should warn the user about * required fields that are missing. * @return a DOM Document that contains the web page. */ The following code begins with its two import javax.xml.parsers.*; import org.w3c.dom.*; The key to this class is its public API, which allows a
The includeErrors parameter indicates whether or not to include the <requiredFieldsMissing/> element in the result. If this method throws a ParserConfigurationException, the most likely cause is a CLASSPATH problem. This frequently occurs when an older version of JAXP is present. When using JAXP, it takes a few lines of code to obtain the appropriate implementation of the DocumentBuilder abstract class. By using the factory pattern, our code is safely insulated from vendor-specific DOM implementations: // use Sun's JAXP to create the DOM Document DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance( ); DocumentBuilder docBuilder = dbf.newDocumentBuilder( ); Document doc = docBuilder.newDocument( ); Once the // create <page>, the root of the document Element pageElem = doc.createElement("page"); doc.appendChild(pageElem); Since // if needed, append <requiredFieldsMissing/> if (includeErrors && !personalData.isValid( )) { pageElem.appendChild(doc.createElement( "requiredFieldsMissing")); } Since DOM can be tedious, the children of Element personalDataElem = doc.createElement("personalData"); pageElem.appendChild(personalDataElem); // use a private helper function to avoid some of DOM's // tedious code addElem(doc, personalDataElem, "firstName", personalData.getFirstName( ), true); ... You can refer back to Example 6-7 for the complete implementation of the <firstName required="true">Eric</firstName> The final piece of code, PersonalDataServlet.java, is presented in Example 6-8. This is a basic approach to servlet development that works for smaller programs such as this, but has a few scalability problems that we will discuss later in this chapter. Although we have removed all of the HTML and XML generation from this servlet, it is still responsible for handling incoming requests from the browser. As your web application grows to more and more screens, the code gets correspondingly larger. Example 6-8. PersonalDataServlet.javapackage chap6; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; /** * A demonstration servlet that produces two pages. In the first page, * the user is prompted to enter "personal information", including * name, phone number, and Email. In the second page, a summary of this * information is displayed. XSLT is used for all HTML rendering, * so this servlet does not enforce any particular look and feel. */ public class PersonalDataServlet extends HttpServlet { private PersonalDataXML personalDataXML = new PersonalDataXML( ); private Templates editTemplates; private Templates thanksTemplates; /** * One-time initialization of this Servlet. */ public void init( ) throws UnavailableException { TransformerFactory transFact = TransformerFactory.newInstance( ); String curName = null; try { curName = "/WEB-INF/xslt/editPersonalData.xslt"; URL xsltURL = getServletContext( ).getResource(curName); String xsltSystemID = xsltURL.toExternalForm( ); this.editTemplates = transFact.newTemplates( new StreamSource(xsltSystemID)); curName = "/WEB-INF/xslt/confirmPersonalData.xslt"; xsltURL = getServletContext( ).getResource(curName); xsltSystemID = xsltURL.toExternalForm( ); this.thanksTemplates = transFact.newTemplates( new StreamSource(xsltSystemID)); } catch (TransformerConfigurationException tce) { log("Unable to compile stylesheet", tce); throw new UnavailableException("Unable to compile stylesheet"); } catch (MalformedURLException mue) { log("Unable to locate XSLT file: " + curName); throw new UnavailableException( "Unable to locate XSLT file: " + curName); } } /** * Handles HTTP GET requests, such as when the user types in * a URL into his or her browser or clicks on a hyperlink. */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { PersonalData personalData = getPersonalData(request); // the third parameter, 'false', indicates that error // messages should not be displayed when showing the page. showPage(response, personalData, false, this.editTemplates); } /** * Handles HTTP POST requests, such as when the user clicks on * a Submit button to update his or her personal data. */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // locate the personal data object and update it with // the information the user just submitted. PersonalData pd = getPersonalData(request); pd.setFirstName(request.getParameter("firstName")); pd.setLastName(request.getParameter("lastName")); pd.setDaytimePhone(request.getParameter("daytimePhone")); pd.setEveningPhone(request.getParameter("eveningPhone")); pd.setEmail(request.getParameter("email")); if (!pd.isValid( )) { // show the 'Edit' page with an error message showPage(response, pd, true, this.editTemplates); } else { // show a confirmation page showPage(response, pd, false, this.thanksTemplates); } } /** * A helper method that sends the personal data to the client * browser as HTML. It does this by applying an XSLT stylesheet * to the DOM tree. */ private void showPage(HttpServletResponse response, PersonalData personalData, boolean includeErrors, Templates stylesheet) throws IOException, ServletException { try { org.w3c.dom.Document domDoc = this.personalDataXML.produceDOMDocument( personalData, includeErrors); Transformer trans = stylesheet.newTransformer( ); response.setContentType("text/html"); printWriter writer = response.getWriter( ); trans.transform(new DOMSource(domDoc), new StreamResult(writer)); } catch (Exception ex) { showErrorPage(response, ex); } } /** * If any exceptions occur, this method can be called to display * the stack trace in the browser window. */ private void showErrorPage(HttpServletResponse response, Throwable throwable) throws IOException { PrintWriter pw = response.getWriter( ); pw.println("<html><body><h1>An Error Has Occurred</h1><pre>"); throwable.printStackTrace(pw); pw.println("</pre></body></html>"); } /** * A helper method that retrieves the PersonalData object from * the HttpSession. */ private PersonalData getPersonalData(HttpServletRequest request) { HttpSession session = request.getSession(true); PersonalData pd = (PersonalData) session.getAttribute( "chap6.PersonalData"); if (pd == null) { pd = new PersonalData( ); session.setAttribute("chap6.PersonalData", pd); } return pd; } } Our servlet begins with a long list of public class PersonalDataServlet extends HttpServlet { private PersonalDataXML personalDataXML = new PersonalDataXML( ); private Templates editTemplates; private Templates thanksTemplates; It is important to ensure that each of these fields is thread-safe. Because many clients share the same servlet instance, it is highly probable that these fields will be accessed concurrently. Instances of As the comments indicate, the public void init( ) throws UnavailableException { TransformerFactory transFact = TransformerFactory.newInstance( ); String curName = null; ... This exception is provided in the The next thing the A relative pathname has to be relative to some starting location, so we use the In this example, chap6.war is deployed to Tomcat's webapps directory. Tomcat will expand it into the webapps/chap6 directory, which contain subdirectories that match the directory structure of the WAR file. We start by assigning the current XSLT filename to the try { curName = "/WEB-INF/xslt/editPersonalData.xslt"; Two options are available at this point. The For this servlet, we avoid this issue by using a URL xsltURL = getServletContext( ).getResource(curName); String xsltSystemID = xsltURL.toExternalForm( ); this.editTemplates = transFact.newTemplates( new StreamSource(xsltSystemID)); The same process is repeated for the second stylesheet, followed by basic exception handling: curName = "/WEB-INF/xslt/confirmPersonalData.xslt"; xsltURL = getServletContext( ).getResource(curName); xsltSystemID = xsltURL.toExternalForm( ); this.thanksTemplates = transFact.newTemplates( new StreamSource(xsltSystemID)); } catch (TransformerConfigurationException tce) { log("Unable to compile stylesheet", tce); throw new UnavailableException("Unable to compile stylesheet"); } catch (MalformedURLException mue) { log("Unable to locate XSLT file: " + curName); throw new UnavailableException( "Unable to locate XSLT file: " + curName); } } The If the protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { PersonalData personalData = getPersonalData(request); // the third parameter, 'false', indicates that error // messages should not be displayed when showing the page. showPage(response, personalData, false, this.editTemplates); } The first thing the The
Once the user fills out the form and submits it to the servlet, the The final piece to this puzzle resides in the private void showPage(HttpServletResponse response, PersonalData personalData, boolean includeErrors, Templates stylesheet) throws IOException, ServletException { try { org.w3c.dom.Document domDoc = this.personalDataXML.produceDOMDocument( personalData, includeErrors); This method then proceeds to create a new instance of Transformer trans = stylesheet.newTransformer( ); Next, the content type is configured for the response.setContentType("text/html"); printWriter writer = response.getWriter( ); trans.transform(new DOMSource(domDoc), new StreamResult(writer)); } catch (Exception ex) { showErrorPage(response, ex); } } If any exception occurs, the DeploymentFigure 6-8 shows the complete contents of the WAR file used in this example. You may notice that SplashScreenServlet.class is still listed in this WAR file. This example is merely an extension of the example created earlier in this chapter. As in the earlier example, placing the Figure 6-8. WAR file for PersonalDataServletThe XSLT stylesheets are placed under the WEB-INF/xslt directory. Since anything under the WEB-INF directory is hidden from clients, the XSLT stylesheets are not directly visible to anyone visiting your website. If you want to make these stylesheets publicly visible, move them out of the WEB-INF directory. The index.html file, for example, is the publicly visible "home page" for this web application. It merely contains a link that the user can click on to view the servlet. Although the stylesheets are hidden from clients, they are accessible from your Java code. Referring back to the code in Example 6-8, the curName = "/WEB-INF/xslt/editPersonalData.xslt"; URL xsltURL = getServletContext( ).getResource(curName); As this code illustrates, the locations of the stylesheets are entirely relative to their position in the WAR file. Therefore, your servlet will still work as the web application is moved onto a production web server. The deployment descriptor, listed in Example 6-9, has been expanded to include the new Example 6-9. Expanded deployment descriptor<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC "-//Oracle, Inc.//DTD Web Application 2.2//EN" "http://java.oracle.com/j2ee/dtds/web-app_2.2.dtd"> <web-app> How to Compile, Deploy, and RunIn Java, it often seems that half of the battle is devoted to figuring out CLASSPATH issues. In order to compile this example, the following JAR files must be listed on the CLASSPATH:
Of course, the directory containing your own source code must also be listed on the CLASSPATH. Once everything is set up, you can compile PersonalData.java, PersonalDataXML.java, and PersonalDataServlet.java by typing As mentioned earlier in this chapter, use the jar command to create the WAR file. To create chap6.war, simply arrange your files into the directory structure shown in Figure 6-8 and issue the following command from the directory that contains index.html:
This command places chap6.war in the parent of your current working directory; the forward slash (
This shows the table of contents for the WAR file, which must match the structure shown in Figure 6-8. Deployment to Tomcat is easy: just copy chap6.war to the TOMCAT_HOME/webapps directory while Tomcat is not running. You can attempt to execute the servlet now, but it will probably not work because jaxp.jar, xalan.jar, and crimson.jar must be installed in the TOMCAT_HOME/lib directory before they can be available for your web application. The most difficult aspect of this step is installing the correct versions of these JAR files. Depending on which version of Tomcat you are running, older versions of jaxp.jar and crimson.jar may already be found in the TOMCAT_HOME/lib directory. The safest approach is to download JAXP 1.1, which includes all three of these JAR files, and copy them from the JAXP distribution to the TOMCAT_HOME/lib directory. Once these steps are complete, start Tomcat and access the following URL:
This should bring up the personal information page with a blank form, ready for input. Locating Stylesheets with Initialization ParametersAs you just saw, an easy way to locate stylesheets is simply to place them somewhere underneath the WEB-INF directory of a WAR file. While this is an ideal solution for solitary web applications, there are situations where the same stylesheets are shared across a whole group of web apps. In this case, embedding the stylesheets into various WAR files is not viable. Ideally, the stylesheets will be located in a shared directory somewhere, but that directory location will not be hardcoded into any servlets. The simple way to accomplish this is via initialization parameters. These are name/value pairs of strings specified in the deployment descriptor and retrieved via the Servlet initialization parameters are tied to specific servlets, and context initialization parameters are tied to an entire web application. For the purposes of specifying the XSLT stylesheet location, it makes sense to use context parameters. These can be specified in the deployment descriptor as follows: <web-app> And the values of these parameters can be retrieved using the following methods on the public interface ServletContext { // if the parameter name does not exist, return null String getInitParameter(String name); Enumeration getInitParameterNames( ); ...remaining methods omitted } So in order to locate the stylesheet, one might write the following code in a servlet's public class MyServlet extends HttpServlet { private String xsltDirectory; public void init(ServletConfig config) throws ServletException { super.init(config); Now that the actual location of the stylesheets has been moved into the deployment descriptor, changes can be made without any edits to the servlet. |