ORIGINAL DRAFT

Occasionally, we need to test ourselves against a set of questions to verify our understanding. This may be because we want to prove our value, gauge our ability to learn new material or simply because we enjoy measuring ourselves against the world. For most, taking a test can be a difficult experience, sometimes facilitated through automation. It’s comforting not to have another person hovering around, judging our progress.

Web forms are ideal for presenting the question and answer sessions required in a testing scenario. Not only can users take the test from anywhere, thanks to ubiquitous Web browsers, but our automation can also grade the test, providing useful explanations to teach readers after they’ve answered the questions. In principle this is all very easy to do, especially if you pick the right technologies.

XML is well suited to representing structured data. It’s been used in the Channel Definition Language, to describe chemical structures, and even to embed stock or other information in web pages. The key strength in XML is that it was designed to represent arbitrary, hierarchical data, lending structure to an otherwise potentially undistinguished piece of information. We’re going to use XML to represents the questions in our test.

When I went looking for an XML parser, I found three almost immediately. Sun has what they call ProjectX posted on the JavaSoft site, which at the time of this writing was in early access. Microsoft is a big proponent and early adopter of XML technology and has its own Java XML parser, which is more than suitable for most applications. The one I chose is from IBM. It seemed to be the most complete, supporting both DOM and SAX. It also had the best documentation. XML4J is the library I used to implement this solution, though the others I mentioned should be equally suitable.

XML, DOM, SAX and DTDs

There certainly isn’t enough room in a short article like this to cover the deeper intricacies of XML. It’s generally simple enough to work with and fairly elegant. This article assumes you already have experience working with Java and that you’re familiar with the basic concepts behind Java servlet implementation. None of the code in this article is particularly complex, so even if this is new to you, it should become accessible after reading a quick tutorial. XML is fairly new to most of us, so I’ll cover the basic concepts from a high-level perspective before we start looking at the code.

XML evolved from the more complex SGML (Standard Generalized Markup Language) which was adopted by serious document management companies but seldom completely implemented. In SGML, and XML, document syntax can be described using a DTD (Document Type Definition) either inline or as an external file. The DTD defines the relationship between elements and the permitted structure of the document. XML parsers can work without a DTD or may be told to validate the document against the DTD.

The DOM (Document Object Model) uses a tree representation to store the data structure of an XML document. This is a standardized model managed by the W3C and evolved from the earlier Netscape Navigator and Internet Explorer models. The browser models are referred to as level zero and the current XML standard is level one. A more recent level two standard is also emerging. DOM is a very simple model, centering on the root Document, with a tree of Elements and Attributes, along with a few other supporting concepts.

SAX (Simple API for XML) is an event model that can be used to avoid building a complete tree representation. This is especially important if documents are expected to be very large or if only some portions of the document require attention. SAX events are generated by the parser when it enters or leaves a document, element, attribute or other subtrees. Changes can be made on-the-fly. The model can be use to detect features or relevant data without needing to pay attention to the whole document structure.

Structural Overview

XML documents have a predictable structure, so parsers can loosely handle this structure as long as it is syntactically correct. It’s also possible to impose a limited set of elements and attributes to ensure the data is valid. To do this, XML requires a DTD that can be used to specify what is permissible in a given document.

Listing 1 shows our question DTD. We’ll be using the looser, unvalidated interpretation because we want to accommodate embedded HTML tags, should the author wish to enhance the presentation, but we’ll take a quick look at the DTD because it describes the expected format of our test files. If the files do not adhere to this format, the application will produce unexpected results (possibly failing altogether without much useful feedback).

The DTD first declares that it uses US-ASCII encoding and defines the elements and attributes permitted or expected for each of the elements. The root of our question documents are the test element, which contains question, answer and explain subsections. The test element has a required name attribute, which is the working name, followed by an equal sign and some quoted text. A pair of question tags also contain this type of text data, as do the explain tags.

An answer is one or more item elements and requires two attributes, the type of answer (which has to be either CHECKBOX or RADIO) and a valid answer string (which must contain a comma-delimited list of values for correct answers, with the first item index starting at zero). The items themselves are plain text surrounded by item tags. This definition syntax is very powerful and much more comprehensive than this example might indicate. We have no need for a more complex definition in our application, but the power to do much more is built-in to XML.

Listing 2 shows what a question file actually looks like. You can compare this against Figure 1, which shows the hierarchical relationship for each of the elements in the file. If you look closely at the listing, you’ll see that we use the attributes to define the question name in the test tag, and the type and value for the answer.

Figure 1: Question Hierarchy. The XML structure for 
questions, begins with a test, which includes a question, answer and explain section. The answer section
includes one or more items.

Figure 1: Question Hierarchy. The XML structure for questions, begins with a test, which includes a question, answer and explain section. The answer section includes one or more items.

Figure 2 shows what a question looks like in your browser. The QuestionServlet translates the XML representation into an HTML page before serving it up. By doing this dynamically on the server side, we introduce considerable flexibility in that all the mechanics are taken care of for us in the servlet. A test author can create questions and drop files in the questions directory where they become immediately accessible.

Figure 2: Question 3 in a web browser, 
generated from the XML representation in Listing 2.

Figure 2: Question 3 in a web browser, generated from the XML representation in Listing 2.

Our servlet is made aware of the number and position of each question in the test. Figure 2 shows both a previous and next button, along with the score button. If no preceding questions exist, the previous button will not be shown. The same applies to the next button if no subsequent questions are available.

Document Object Model

Our implementation makes use of the Document Object Model. In order to keep our code as clear as possible, we’ll implement a handful of static methods that do a few common things. Listing 3 shows the DOMUtil class which includes the following methods.

</tr> </thead> </table> Each of these methods provides us with operations we'll need to access relevant information in the Document model. The code is based on the W3C (World Wide Web Consortium) standard Java interfaces which are found in the org.w3c.dom package. Most of the XML parsers provide additional functionality, but by sticking to the DOM interfaces, we are guaranteed to be compatible with all but the most rebellious implementations. Listing 3 shows that the readDocument method creates a parser, sets up a FileInputStream, reads the document, closes the stream and returns the Document object. The findNode method traverses the tree from a given node, checking each of its children using the standard getChildNodes() method. If we run into a matching name, we return the node. Otherwise the search is applied recursively until we find the node or reach bottom, in which case we return a null value. The getNodeAttribute is a convenience method that helps us avoid casting or null pointer problems. If the Node is not an Element or if the attribute is missing, we get a null value back. Otherwise, we expect to see the string value of the attribute. The printSubtree method works much like findNode and traverses the children of a given node. Its purpose is to write the content in readable form, primarily because we want to allow authors to embed hypertext markup tags to enhance readability where this is useful. A note of caution is warranted, however. XML expects all open and close tags to be present. So, while browsers are forgiving about missing closing tags (such as a

paragraph tag without a </p> closing tag), XML is not. There are two versions of printSubtree. The second one has an extra argument so we can carry the root node with us through the recursive traversal. We typically want to print the children under a given node and not the node itself. We need a reference to the root node inside the search loop. You'd normally use the first version, which expects only PrintWriter and Node arguments. There's a difference between markup and content nodes in XML. The nodes are either Element or Text nodes, respectively. For each element we encounter, we write an opening markup tag and write the subtree before the closing tag. This is where we avoid writing the root tags but not the children. Each opening tag is just the Element name with the opening and closing angle brackets on either side. The same is true of closing tags, with the added slash character after the opening angle bracket. For the Text nodes, we simply get the node value and write it out explicitly. The QuestionView class handles parsing the XML document and producing the HTML view on-demand. The parsing is largely taken care of by the third party XML parser. We use the DOMUtil methods to pull out the nodes and attributes we are interested in and store them in a few instance variables. Listing 4 shows how we take care of this in the constructor and provide a method called getQuestionPage to fetch the HTML representation. The getQuestionPage method does a lot of work. To make it easier to read what's going on, we delegate most of the formatting to other methods. These methods are useful enough that many of them can become reusable, so we put them in another utility class called HTMLUtil. I won't explain what each of the methods do, but you'll see by looking at them in Listing 5, that they produce HTML output, sending it to the PrintWriter passed in as the first argument. The methods are convenient primarily because they let us pass a few pertinent arguments while the rest of the formatting is taken care of for us. It's worth noting that we could have constructed a document tree to produce the HTML content. It turns out to be easier to do it this way because HTML doesn't always adhere to the XML syntax. The XML Parser from IBM ships with the DTD for HTML but will not parse a typical web page without complaining about the kind of syntax details any HTML author would typically ignore. When confronted with the decision, I chose the approach that produced more recognizable code, both because it is easier to write, and because it is easier for you to make sense of the code this way. ## So Many Questions We need a couple of classes to work directly with questions. Each Questions object will hold information about the XML document, the question id, the list of valid answers and the state of answers provided by a user. Listing 6 shows the Question class, which requires a constructor with a numerical ID and an XML filename argument. We support a number of access methods, including getID, getScore, getUserResponse and setUserResponse. We also provide a method called getQuestionPage that returns the HTML page through the specified PrintWriter. Most of the work for this method is delegated to the an instance of the QuestionView object. We don't hold the page or document model in memory outside the scope of a servlet call to avoid needless memory consumption under load. The page is generated on-the-fly and the QuestionView object is discarded when this method falls out of scope. Listing 7 shows the Selection class, which is responsible for making valid answers and user selections easier to manage. We parse the comma-delimited number string in the setSelection method, which is automatically called by the constructor. We use a private addKey method to put entries into a Hashtable. We also provide a method called isSelected to test whether an entry is present and a toString method to reproduce the text representation on-demand. Finally, an equals method lets us compare one Selection against another to see if they're the same. To track multiple questions in a test, we use a Test object. This is the class that collects question file names from the questions directory and keeps track of all the Question objects in relative order. The Question instances don't hold the document in memory, but they do keep track of user responses. To make this possible in a web server, we rely on the HttpSession object and store the Test instance as part of the session. Figure 4 shows how the Servlet session management keeps multiple users in proper context.

Figure 4: Session management for multiple users. Each 
session has an associated Test object which keeps track of the questions and related user responses.

Figure 4: Session management for multiple users. Each session has an associated Test object which keeps track of the questions and related user responses.

Listing 8 shows code for the Test class, which provides access to the questions through the getQuestion method. The questions are stored in a Vector and accessed by index value. The getQuestionList method actually populates the vector after calling the private getFileList method to collect the XML file names. We also support a getScored method to determine if the user has pressed the score button. If they have, the answered questions can be viewed along with their actual answers, but the entries for those questions can no longer be edited. (They can actually be edited, but the answers were already recorded and any new responses are ignored). Figure 4 shows a question after the user has scored it and returned to view the results.
Figure 4: A questions which has been answered is presented 
along with the answer if viewed after scoring. If the user has pressed the score button, answered questions are no
longer editable. Correct answers are highlighted in green and incorrect answers in red.

Figure 4: A questions which has been answered is presented along with the answer if viewed after scoring. If the user has pressed the score button, answered questions are no longer editable. Correct answers are highlighted in green and incorrect answers in red.

The test class is also responsible for generating the score page. This page shows the user their currently answered, correct and incorrect questions, along with a link back to each. Figure 5 shows what this looks like. The getScorePage in Listing 8 does the actual work. Most of the code is HTML markup. We use the HTMLUtil methods in several cases. The main logic centers on providing a link for each question, along with its status in the left column of a table, with the statistical results in the right column.
Figure 5: The Score page lets you know what the current
score is, along with the status of each question and a link back to it. Questions which are marked as "
No Answers" are still editable, but answered questions are not. The right column shows overall statistics.

Figure 5: The Score page lets you know what the current score is, along with the status of each question and a link back to it. Questions which are marked as " No Answers" are still editable, but answered questions are not. The right column shows overall statistics.

## Taking the Test The servlet class itself, QuestionServlet, is presented in Listing 9. We use the HttpSession object to store the state, which requires that we use the same instance of our Test class for any given user. The HttpSession object is created by the web server and persists through multiple HTTP connections for that user. It is reset on the client side if either the cookie cache is cleared or the browser is restarted. The server holds it until a timeout occurs, usually set to about 30 minutes of user inactivity. The QuestionServlet class provides a getParameter method to make argument retrieval easier. We also implement a getInteger method to fetch integer parameters and an exception method to help debug any serious errors. The getTest method handles getting the session object and retrieving the Test object if the session already exists. If a new session is created, a new Test instance is also created and bound to the session. The main logic for the Question servlet is in the service method, which will capture any GET, POST or other HTTP request. For our purposes, a page request and a form post are essentially the same, differing only in their origin. After getting the output stream and Test instance, we first record any posted user responses. We do this by walking the CHECKBOX or RADIO button list and building a Selection instance to store the value, which we associated with the relevant question in the Test object. After storing any persistent information, we determine which response page to serve up. The user will have pressed the PREV, NEXT or SCORE button. In the first two cases, we determine which question is to be presented and use the getQuestionPage method to stream out the results. If the SCORE button was pressed, we use the getScorePage method to provide proper feedback. If the user pressed one of the question links on the score page, we provide the requested question page, based on the question argument.
Figure 6: The QuestionServlet uses the Test and
Question objects to manage user interaction. The Question object uses the QuestionView to generate a question
page. Both the QuestionView and Test objects make use of the Selection objects to manage user and valid
selections. The HTMLUtil and DOMUtil classes can be thought of as static method libraries.

Figure 6: The QuestionServlet uses the Test and Question objects to manage user interaction. The Question object uses the QuestionView to generate a question page. Both the QuestionView and Test objects make use of the Selection objects to manage user and valid selections. The HTMLUtil and DOMUtil classes can be thought of as static method libraries.

Figure 6 shows the way each of the classes in this article relate to each other. This diagram should help you understand the usage relationships and is not designed to reflect inheritance. In fact, only the QuestionServlet extends another class directly. ## Deploying the Servlet To get this application to work on a Web server, you'll need a suitable Servlet environment, some minor configuration and access to the XML4J classes. The XML Parser code from IBM comes in a JAR file called xml4j_1_1_9.jar (the version I used was 1.1.9). If your server doesn't allow you to extend the runtime classes with a JAR file, you may have to dearchive the JAR file into the classes directory. If these classes are missing, the servlet will not operate properly. Both the Java Web Server and JRun use a servlets.properties file to store servlet configuration elements. The QuestionServlet is smart enough to know where it is running and how to construct the relative URLs it uses, but it also needs to know in which directory it can look for the XML question files. The required entries in the servlets.properties file look like this: ``` servlet.QuestionServlet.code=QuestionServlet servlet.QuestionServlet.preload=false servlet.QuestionServlet.args=questions=/questions ``` The preload value is set to false to make development easier. The important argument here is the "questions=/questions" entry under servlet.QuestionServlet.args, which stipulates that a directory called questions, off the server's root directory, should contain nothing but properly formatted questions in XML form. Most servlet engines also provide a user interface for configuring these arguments and the servlet will function correctly as long as a questions argument is properly defined. ## Conclusion As you've seen in this article, using XML in combination with servlets provides a powerful combination. By using an XML parser to build a Document Object Model and using that model to construct an HTML view which is served up dynamically on the web, we can provide access to a variety of information without having to create more elaborate representations on separate web pages. By keeping the information dynamic, we save countless hours designing and developing web pages and server-side scripts that require serious changes every time we add a question. In short, server-side XML is clearly destined to support countless practical solutions and less predictable innovation over the coming years. Listing 1 ``` <!ELEMENT test (question,answer,explain)> <!ATTLIST test name CDATA #REQUIRED> <!ELEMENT question (#PCDATA)> <!ELEMENT answer (item)+> <!ATTLIST answer type CDATA #REQUIRED> <!ATTLIST answer valid CDATA #REQUIRED> <!ELEMENT item (#PCDATA)> <!ELEMENT explain (#PCDATA)></PRE> ``` Listing 2 ```Java Java Applets, Applications and Servlets share the following features in common: Each can run on any Java-enabled platform. Each can take advantage of the Javabeans component architecture. Each can run in a browser, depending on how the browser is configured. Each can have a main clause that can be executed directly from the command line. Java Applets, Applications and Servlets can run on any Java-enabled platform, can make use of the JavaBeans architecture, and may include a main clause which can be executed from the command line. Only Applets can run in browsers. ``` Listing 3 ```Java import java.io.*; import org.w3c.dom.*; import com.ibm.xml.parser.Parser; public class DOMUtil { public static Document readDocument(String filename) throws IOException { Parser parser = new Parser(filename); InputStream input = new FileInputStream(filename); Document doc = parser.readStream(input); input.close(); return doc; } public static Node findNode(Node node, String name) { if (node.getNodeName().equals(name)) return node; if (node.hasChildNodes()) { NodeList list = node.getChildNodes(); int size = list.getLength(); for (int i = 0; i < size; i++) { Node found = findNode(list.item(i), name); if (found != null) return found; } } return null; } public static String getNodeAttribute(Node node, String name) { if (node instanceof Element) { Element element = (Element)node; return element.getAttribute(name); } return null; } public static void printSubtree( PrintWriter writer, Node node) { printSubtree(writer, node, node); } public static void printSubtree( PrintWriter writer, Node root, Node node) { if (node instanceof Element) { if (node != root) writer.print("\n<" + node.getNodeName() + ">"); if (node.hasChildNodes()) { NodeList list = node.getChildNodes(); int size = list.getLength(); for (int i = 0; i < size; i++) { printSubtree(writer, root, list.item(i)); } } if (node != root) writer.print("</" + node.getNodeName() + ">"); } else if (node instanceof Text) { writer.print(node.getNodeValue().trim()); } } } ``` Listing 4 ```Java import java.io.*; import java.net.*; import java.util.*; import org.w3c.dom.*; public class QuestionView { String title, type; Node question, answer, explain; Selection validAnswers; public QuestionView(Document document) { Node test = DOMUtil.findNode(document, "test"); title = DOMUtil.getNodeAttribute(test, "name"); question = DOMUtil.findNode(document, "question"); answer = DOMUtil.findNode(document, "answer"); type = DOMUtil.getNodeAttribute(answer, "type"); validAnswers = new Selection( DOMUtil.getNodeAttribute(answer, "valid")); explain = DOMUtil.findNode(document, "explain"); } public Selection getValidAnswers() { return validAnswers; } public void getQuestionPage(PrintWriter writer, int id, Selection userResponse, String action, boolean show, boolean prev, boolean next) throws IOException { // HTML AND FORM SETUP HTMLUtil.writeDocumentHeader(writer, title, action); HTMLUtil.writeInputField(writer, "HIDDEN", "THIS_QUESTION", "" + id); // QUESTION IS IN A TABLE HTMLUtil.writeTableHeader(writer); writeQuestionCell(writer, title, question); HTMLUtil.writeTableFooter(writer); // ANSWERS ARE IN A TABLE HTMLUtil.writeTableHeader(writer); NodeList list = answer.getChildNodes(); int size = list.getLength(); int index = 0; for (int i = 0; i < size; i++) { Node item = list.item(i); boolean isValid = validAnswers.isSelected(index); boolean isSelected = userResponse != null && userResponse.isSelected(index); if (item.getNodeName().equals("item")) { writer.println("<TD WIDTH=20"); writer.println(" VALIGN=top HALIGN=right>"); HTMLUtil.writeInputField(writer, type, type.equalsIgnoreCase("radio") ? "CHOICES" : "CHOICE" + index, "" + index, isSelected); writer.println("</td>"); index++; } } HTMLUtil.writeTableFooter(writer); // EXPLANATION IS IN A TABLE if (show) { HTMLUtil.writeTableHeader(writer); writeExplainCell(writer, explain); HTMLUtil.writeTableFooter(writer); } // BUTTONS if (prev) HTMLUtil.writeInputField( writer, "SUBMIT", "PREV", "<< PREV"); if (next) HTMLUtil.writeInputField( writer, "SUBMIT", "NEXT", "NEXT >>"); HTMLUtil.writeInputField(writer, "SUBMIT", "SCORE", "SCORE"); HTMLUtil.writeDocumentFooter(writer); HTMLUtil.writeSignature(writer); writer.flush(); } private void writeQuestionCell(PrintWriter writer, String title, Node question) { writer.println(""); } private void writeExplainCell(PrintWriter writer, Node explain) { writer.println(""); } } ``` Listing 5 ```Java import java.io.*; public class HTMLUtil { public static String formatAttr(String name, String value) { return " " + name + "=" + '"' + value + '"'; } public static void writeDocumentHeader(PrintWriter writer, String title, String action) { writer.println(""); writer.println("" + title + ""); writer.println(""); writer.println("<FORM" + formatAttr("METHOD", "POST") + formatAttr("ACTION", action) + ">"); } public static void writeDocumentFooter(PrintWriter writer) { writer.println("</FORM>"); } public static void writeTableHeader(PrintWriter writer) { writer.println("<TABLE" + formatAttr("BORDER", "0") + formatAttr("WIDTH", "100%") + formatAttr("CELLSPACING", "0") + formatAttr("CELLPADDING", "2") + ">"); } public static void writeTableFooter(PrintWriter writer) { writer.println("</TABLE>
"); } public static void writeInputField(PrintWriter writer, String type, String name, String value) { writeInputField(writer, type, name, value, false); } public static void writeInputField(PrintWriter writer, String type, String name, String value, boolean check) { writer.println("<INPUT" + formatAttr("type", type) + formatAttr("name", name) + formatAttr("value", value) + (check ? formatAttr("checked", "true") : "") + ">"); } public static void writeHighlightHeader(PrintWriter writer, boolean show, boolean isValid, boolean isSelected) { writer.println("<FONT" + (show && isValid ? " COLOR=#008000" : "") + (show && !isValid && isSelected ? " COLOR=#800000" : "") + ">"); writer.println( (show && (isValid | isSelected) ? "" : "")); } public static void writeHighlightFooter(PrintWriter writer, boolean show, boolean isValid, boolean isSelected) { writer.println( (show && (isValid | isSelected) ? "" : "")); writer.println("</FONT>"); } public static void writeSignature(PrintWriter writer) { writer.println("This application was written by "); writer.println(""); writer.println("Claude Duguay at"); writer.println(""); writer.println("Atrieva Corp."); } } ``` Listing 6 ```Java import java.io.*; import java.net.*; import java.util.*; import org.w3c.dom.*; public class Question { public static final int SCORE_NONE = 0; public static final int SCORE_PASS = 1; public static final int SCORE_FAIL = 2; protected int id; protected String filename; protected Selection validAnswers; protected Selection userResponse; public Question(int id, String filename) { this.id = id; this.filename = filename; } public int getID() { return id; } public int getScore() { if (userResponse == null || validAnswers == null) return SCORE_NONE; if (userResponse.equals(validAnswers)) return SCORE_PASS; return SCORE_FAIL; } public Selection getUserResponse() { return userResponse; } public void setUserResponse(Selection userResponse) { this.userResponse = userResponse; } public void getQuestionPage( PrintWriter writer, String action, boolean show, boolean prev, boolean next) throws IOException { Document document = DOMUtil.readDocument(filename); QuestionView view = new QuestionView(document); validAnswers = view.getValidAnswers(); view.getQuestionPage(writer, id, userResponse, action, show, prev, next); } public String toString() { return "Question(" + id + "," + '"' + filename + '"' + ")"; } } ``` Listing 7 ```Java import java.util.*; public class Selection { protected Hashtable table = new Hashtable(); public Selection() {} public Selection(String text) { setSelections(text); } public void setSelections(String text) { StringTokenizer tokenizer = new StringTokenizer(text, ",", false); while (tokenizer.hasMoreTokens()) { String item = tokenizer.nextToken(); addKey(new Integer(item)); } } private void addKey(Integer key) { if (!table.containsKey(key)) table.put(key, key); } public boolean isSelected(int selection) { Integer key = new Integer(selection); return table.containsKey(key); } public String toString() { StringBuffer buffer = new StringBuffer(); for (int i = 0; i < 20; i++) { if (isSelected(i)) { if (buffer.length() > 0) buffer.append(","); buffer.append("" + i); } } return buffer.toString(); } public boolean equals(Selection sel) { return toString().equalsIgnoreCase(sel.toString()); } } ``` Listing 8 ```Java import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class Test { protected String path; protected Vector questions = new Vector(); protected boolean scored = false; public Test(String path) { this.path = path; } public boolean getScored() { return scored; } public Question getQuestion(int index) { if (index < 0 || index >= questions.size()) return null; return (Question)questions.elementAt(index); } private String[] getFileList() { File file = new File(path); String[] list = file.list(); Vector vector = new Vector(); for (int i = 0; i < list.length; i++) { if (list[i].toLowerCase().endsWith(".xml")) { vector.addElement(list[i].substring( 0, list[i].length() - 4)); } } int count = vector.size(); list = new String[count]; for (int i = 0; i < list.length; i++) { list[i] = (String)vector.elementAt(i); } return list; } public void getQuestionList() throws IOException { String[] list = getFileList(); for (int i = 0; i < list.length; i++) { String fullPath = path + File.separator + list[i] + ".xml"; questions.addElement( new Question(i, fullPath)); } } public void getScorePage(PrintWriter writer, String home) { scored = true; writer.println(""); writer.println("Your Score"); writer.println(""); HTMLUtil.writeTableHeader(writer); writer.println("<TD VALIGN=top>"); writer.println("

Question Status

"); writer.println("</td><TD VALIGN=top>"); writer.println("

Your Score

"); writer.println("</td>"); writer.println("<TD VALIGN=top>"); int correct = 0, incorrect = 0, unanswered = 0; for (int i = 0; i < questions.size(); i++) { Question question = getQuestion(i); writer.print("<A HREF=" + '"'); writer.print(home + "?question="); writer.print(question.getID()); writer.print("&show=true"); writer.print('"' + ">"); writer.print("Question " + question.getID()); writer.print("</A>: "); switch (question.getScore()) { case Question.SCORE_PASS: { writer.println("<FONT COLOR=#008000>"); writer.println("Correct
"); writer.println("</FONT>"); correct++; break; } case Question.SCORE_FAIL: { writer.println("<FONT COLOR=#800000>"); writer.println("Incorrect
"); writer.println("</FONT>"); incorrect++; break; } default: { writer.println("No Answer
"); unanswered++; break; } } } writer.println("</td><TD VALIGN=top>"); int score = (int)((float)correct / (correct + incorrect + unanswered) * 100); writer.println("

Your final score is " + score + "%

"); writer.println("You failed to answer " + unanswered + " Question(s)
"); writer.println("You got " + correct + " Correct Answer(s)
"); writer.println("You got " + incorrect + " Incorrect Answer(s)"); writer.print("</BODY></HTML>"); writer.println("</td>"); writer.println("</TABLE>
"); HTMLUtil.writeSignature(writer); writer.flush(); } } ``` Listing 9 ```Java import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class QuestionServlet extends HttpServlet { public String getParameter( HttpServletRequest req, String name) { String[] values = req.getParameterValues(name); if (values == null) return null; return values[0]; } public int getInteger( HttpServletRequest req, String name) { String value = getParameter(req, name); if (value == null) return -1; return Integer.parseInt(value); } public void exception(HttpServletResponse res, Exception exception) { try { res.setContentType("text/html"); PrintWriter out = new PrintWriter(res.getOutputStream()); out.println(""); out.println("Imaging Servlet Exception"); out.println(""); out.println("

Imaging Servlet Error

"); out.println(""); exception.printStackTrace(out); out.println(""); } catch (IOException e) {} } private Test getTest(HttpServletRequest req, PrintWriter out) throws IOException { HttpSession session = req.getSession(true); Object value = session.getValue("test"); Test test = null; if (value instanceof Test) { test = (Test)value; } else { test = new Test(req.getRealPath( getInitParameter("questions"))); test.getQuestionList(); session.putValue("test", test); } return test; } public void service( HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { try { PrintWriter out = new PrintWriter(res.getOutputStream()); Test test = getTest(req, out); // Record any posted answers if not scored int current = getInteger(req, "THIS_QUESTION"); if (current != -1) { StringBuffer selection = new StringBuffer(); Question question = test.getQuestion(current); // Allow answers for no response or not scored yet. if (question.getUserResponse() == null || !test.getScored()) { String[] choices = req.getParameterValues("CHOICES"); // Radio if (choices != null) { for (int i = 0; i < choices.length; i++) { if (i > 0) selection.append(","); selection.append(choices[i]); } } // Check box else { for (int i = 0; i < 20; i++) { String choice = getParameter(req, "CHOICE" + i); if (choice != null) { if (selection.length() > 0) selection.append(","); selection.append(choice); } } } String answer = selection.toString(); // Remember only if not null if (answer.length() > 0) question.setUserResponse( new Selection(answer)); } } // Get the requested question name int id = -1; if (getParameter(req, "question") != null) id = getInteger(req, "question"); if (getParameter(req, "NEXT") != null) id = current + 1; //getInteger(req, "NEXT_QUESTION"); if (getParameter(req, "PREV") != null) id = current - 1; //getInteger(req, "PREV_QUESTION"); if (id == -1) id = test.getQuestion(0).getID(); String path = req.getRequestURI(); int pos = path.indexOf('?'); if (pos > -1) path = path.substring(0, pos); String thisURL = "http://" + req.getServerName() + path; // Return appropriate question or score page if (getParameter(req, "SCORE") != null) { res.setContentType("text/html"); test.getScorePage(out, thisURL); } else { Question prev = test.getQuestion(id - 1); Question next = test.getQuestion(id + 1); res.setContentType("text/html"); Question question = test.getQuestion(id); question.getQuestionPage(out, thisURL, (test.getScored() && (question.getUserResponse() != null)), prev != null, next != null); } out.flush(); } catch (Exception e) { exception(res, e); } } } ```

Method</td> Description
readDocument Given a filename, return a Document object containing the parsed model in tree form.
findNode Given a Node object, locate and return a child Node with the given name.
getNodeAttribute Get a Node attribute by name, returning its String representation.
printSubtree Write out the subtree from a given Node down to the specified PrintWriter.
Table 1: Document Object Model utility methods. Each of these methods provides easy access to the internals of a DOM tree structure and is implemented in a separate utility class for convenience.
"); HTMLUtil.writeHighlightHeader(writer, show, isValid, isSelected); DOMUtil.printSubtree(writer, item); HTMLUtil.writeHighlightFooter(writer, show, isValid, isSelected); writer.println("
"); writer.println("<H1" + HTMLUtil.formatAttr("ALIGN", "center") + ">" + title + "</H1>"); DOMUtil.printSubtree(writer, question); writer.println("
"); writer.println("<FONT COLOR=#008000>"); writer.println("ANSWER:"); writer.println("</FONT>"); DOMUtil.printSubtree(writer, explain); writer.println("