• 沒有找到結果。

Source Code

在文檔中 Java and XSLT (頁 167-178)

While this approach has its place, it did not make sense for this particular example because every template produced different output. It would

6.3.3 Source Code

The first piece of source code to examine is shown in Example 6-6. The PersonalData class is simply a data holder and does not contain any XML code or database code. By keeping

classes like this simple, you can easily write standalone unit tests that verify if your code is written properly. If this code were written as part of the servlet instead of a standalone class, it would be very difficult to test outside of the web browser environment.

Example 6-6. PersonalData.java package chap6;

/**

* A helper class that stores personal information. XML gen eration * 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(e veningPhone);

}

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 PersonalData class is merely a data holder, it can include simple validation logic.

For example, the default constructor initializes all fields to non-null values:

public PersonalData( ) { this("", "", "", "", "");

}

Additionally, all of the set methods make use of the private cleanup( ) method:

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 PersonalData class also contains an explicit validation method that checks for all required fields:

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 eveningPhone, so it is not checked here. By putting this method into this class, we further reduce the work required of the servlet.

The next class, PersonalDataXML, is presented in Example 6-7. It is responsible for

converting PersonalData objects into DOM Document objects. By converting to DOM instead

of a text XML file, we avoid having to parse the XML as it is fed into an XSLT processor. Instead, we will use the javax.xml.transform.DOMSource class to pass the DOM tree directly.

Example 6-7. PersonalDataXML.java package 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.

*/

public Document produceDOMDocument(PersonalData personalData,

boolean includeErrors) throws ParserConfigurationException {

// use Sun's JAXP to create the DOM Document

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(

);

DocumentBuilder docBuilder = dbf.newDocumentBuilder( );

Document doc = docBuilder.newDocument( );

// create <page>, the root of the document Element pageElem = doc.createElement("page");

doc.appendChild(pageElem);

// if needed, append <requiredFieldsMissing/>

if (includeErrors && !personalData.isValid( )) { pageElem.appendChild(doc.c reateElement(

"requiredFieldsMissing"));

}

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);

addElem(doc, personalDataElem, "lastName", personalData.getLastName( ), true);

addElem(doc, personalDataElem, "daytimePhone", personalData.getDaytimePhone( ), true);

addElem(doc, personalDataElem, "eveningPhone", personalData.getEveningPhone( ), false);

addElem(doc, personalDataElem, "email", personalData.getEmail( ), true);

return doc;

}

/**

* A helper method that simplifies this class.

*

* @param doc the DOM Document, used as a factory for * creating Elements.

* @param parent the DOM Element to add the child to.

* @param elemName the name of the XML element to create.

* @param elemValue the text content of the new XML element.

* @param required if true, insert 'required="true"' attribute.

*/

private void addElem(Document doc, Element parent, String elemName, String elemValue, boolean required) {

Element elem = doc.createElement(elemName);

elem.appendChild(doc.createTextNode(elemValue));

if (required) {

elem.setAttribute("required", "true");

}

parent.appendChild(elem);

} }

The following code begins with its two import statements. The javax.xml.parsers package contains the JAXP interfaces, and the org.w3c.dom package contains the standard DOM interfaces and classes:

import javax.xml.parsers.*;

import org.w3c.dom.*;

The key to this class is its public API, which allows a PersonalData object to be converted into a DOM Document object:

public Document produceDOMDocument(PersonalData personalDat a,

boolean includeErrors) throws ParserConfigurationException { 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 doc object has been created, we use it to create all remaining elements in the XML data. For example, the <page> element is created first:

// create <page>, the root of the document Element pageElem = doc.createElement("page");

doc.appendChild(pageElem);

Since <page> is the root element, it is the only thing added directly to our document. All

remaining elements will be added as children or descendents of <page>. Even though we are not

adding anything else directly to the doc object, we must continue using it as the factory for creating the remaining elements:

// if needed, append <requiredFieldsMissing/>

if (includeErrors && !personalData.isValid( )) { pageElem.appendChild(doc.createElement(

"requiredFieldsMissing"));

}

Since DOM can be tedious, the children of <personalData> are created in a helper method called addElem( ) :

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 addElem( ) method. A sample of its output is:

<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.java package 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.new Templates(

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.editTempl ates);

}

/**

* 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) thro ws IOException, ServletException {

// locate the personal data object and update it with // the information the user just submitted.

PersonalData pd = getPersonalData(request);

pd.setFirstName(request.getParamet er("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, t his.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(HttpServletRespon se response,

PersonalData personalData, boolean includeErrors,

Templates stylesheet) throws IOException, ServletException { try {

org.w3c.dom.Document domDoc =

this.personalDataXML.produceDOMD ocument(

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 brow ser 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 import statements, indicating dependencies on the servlet API as well as the JAXP package. The servlet itself is a subclass of HttpServlet, as usual, and has three private fields:

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 PersonalDataXML are thread-safe because they are stateless, meaning they contain no data that can be concurrently modified. The Templates instances are compiled representations of the two stylesheets this servlet uses and are also designed to be thread-safe.

As the comments indicate, the init( ) method performs a one-time initialization of the servlet.

A servlet container will invoke this method before this servlet is asked to handle any client requests. The init( ) method is further guaranteed to execute to completion before any other threads can access this servlet, so concurrency is not an issue at this point. If anything fails during initialization, an instance of UnavailableException is thrown:

public void init( ) throws UnavailableException {

TransformerFactory transFact = TransformerFactory.newInstance( );

String curName = null;

...

This exception is provided in the javax.servlet package and indicates that the servlet could not be loaded successfully. In our case, the most common cause of this error is a configuration problem. For example, your XSLT stylesheets may be installed in the wrong directory, or some JAR file was not found.

The next thing the init( ) method does is load the two stylesheets into memory. The XSLT stylesheets are stored on the file system, so StreamSource will be used to read them into JAXP. But you definitely do not want to hardcode the absolute pathname of the stylesheets. If you do this, your code will probably work on your personal machine but will fail once it is deployed onto a production web server. For example, C:/java/tomcat/webapps/chap6/WEB-INF is a

Windows-specific absolute pathname. Using something so specific would cause the servlet to fail on all non-Windows platforms, as well as other Windows machines that have Tomcat installed in a different directory. The best approach is to use a relative pathname such as /WEB-INF, so the stylesheets can be located regardless of where your web application is deployed.

A relative pathname has to be relative to some starting location, so we use the

ServletContext class. ServletContext has the ability to locate resources relative to the deployed directory of the current web application, so you can avoid absolute pathnames in your code. The details of mapping the relative pathname to the absolute pathname are taken care of by the servlet container, thus making your code more portable.

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 curName variable, using the following pathname:

try {

curName = "/WEB-INF/xslt/editPersonalData.xslt";

Two options are available at this point. The ServletContext can provide either an InputStream or a URL, both of which represent the XSLT stylesheet. If you use an

InputStream, however, the XSLT processor sees your stylesheet as a stream of bytes. It will not know where this datastream originated, so it will not automatically know how to resolve URI references. This becomes a problem if your stylesheet imports or includes another stylesheet because this other stylesheet will not be located. To resolve this problem when using

InputStream, the javax.xml.transform.Source interface provides the setSystemId( ) method. This allows the XSLT processor to resolve URI references in the stylesheet (see

Chapter 5).

For this servlet, we avoid this issue by using a URL instead of an InputStream. The URL is converted into a system identifier, which makes it possible to create a StreamSource instance.

That is, in turn, used to create a Templates instance for this stylesheet:

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 log() method causes messages to be written to one of Tomcat's log files, found in the TOMCAT_HOME/logs directory. The UnavailableException simply indicates that this servlet is unavailable, so it will not be loaded into memory. The user will see an error page in their browser at this point.

If the init( ) method completes successfully, the servlet will be available to handle requests from clients. In this servlet, the doGet( ) and doPost( ) methods have been implemented;

therefore, both HTTP GET and POST protocols are supported. When the user first enters the application, they will click on a hyperlink, type a URL into their browser, or visit a saved

bookmark. In all of these cases, the browser issues an HTTP GET request that ultimately causes the doGet( ) method to be invoked:

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 doGet( ) method does is retrieve the instance of PersonalData associated with this particular user. The appropriate code has been factored out into the

getPersonalData( ) helper method, since this same functionality is required by the doPost(

) method as well. You can refer back to Example 6-8 to see how getPersonalData( ) is implemented. It basically uses HttpSession to locate the appropriate instance of

PersonalData. If the object is not found in the session, a new instance is created and stored.

The doGet( ) method then calls the showPage( ) method, which does the actual work of sending the web page to the browser. The parameters to showPage( ) include:

• The HttpServletResponse, which provides access to the PrintWriter. The result of the transformation will be sent to this writer.

• The instance of PersonalData, so the showPage( ) method knows what data to display.

• A false parameter, indicating that error messages should not be shown. That makes sense because doGet( ) is called when the page is first displayed, and users should not be warned about invalid data before they type something.

• A reference to the appropriate stylesheet. In this case, the stylesheet will show the HTML form so the user can fill out his or her information.

Once the user fills out the form and submits it to the servlet, the doPost( ) method is invoked.

The code for doPost( ) is similar to doGet( ) (see Example 6-8). The only difference here is that all incoming data is validated via the PersonalData class. If the request is valid, the "Thank You" page is displayed. Otherwise, the current page is redisplayed with error messages enabled.

As you can see in the code, the only distinction between these two pages is that they use different stylesheets.

The final piece to this puzzle resides in the showPage( ) method. This method begins by creating a DOM Document instance by delegating to the PersonalDataXML helper class. As you can see in the following code, the servlet stays quite small because the DOM generation is factored out into the helper class:

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. You may recall from Chapter 5 that Transformer instances are very lightweight and merely hold state information for the current transformation. Since Transformer instances are not thread-safe, the instance is a local variable in this method. With local variables, each thread gets its own copy:

Transformer trans = stylesheet.newTransformer( );

Next, the content type is configured for the HttpServletResponse, a PrintWriter is obtained, and the transformation is performed. The result tree is sent directly to the response's PrintWriter:

response.setContentType("text/html");

PrintWriter writer = resp onse.getWriter( );

trans.transform(new DOMSource(domDoc), new StreamResult(writer));

} catch (Exception ex) {

showErrorPage(response, ex);

} }

If any exception occurs, the showErrorPage( ) method is invoked. Since an exception can indicate that some XML library is unavailable, the showErrorPage( ) does not attempt to use XML or XSLT for its output. If it does, another similar exception would almost certainly occur.

If any exception occurs, the showErrorPage( ) method is invoked. Since an exception can indicate that some XML library is unavailable, the showErrorPage( ) does not attempt to use XML or XSLT for its output. If it does, another similar exception would almost certainly occur.

在文檔中 Java and XSLT (頁 167-178)