Meeting SurvexIn the rest of this chapter, you'll build the Survex . Survex is a generic survey application. It is configured for a specific survey through an XML file, such as Listing 2.1. Listing 2.1 survey.xml<?xml version="1.0"?> <survey> <question> <name>email</name> <title>Welcome to our XML book survey</title> <label>Thank you for participating in our survey.</label> <input>Enter your email address</input> <next><text>usingxml</text></next> </question> <question> <name>usingxml</name> <title>XML and You</title> <label>Do you use XML?</label> <choice> <option> <label>I do</label> <value>yes</value> </option> <option> <label>No but I plan to use it</label> <value>planning</value> </option> <option> <label>No and I don't plan to use it</label> <value>no</value> </option> </choice> <next> <if> <eq> <answer>usingxml</answer> <text>no</text> </eq> <block> <save><answer>email</answer></save> <text>done</text> </block> <text>booktraining</text> </if> </next> </question> <question> <name>booktraining</name> <title>XML Books</title> <label>Do you need more XML books?</label> <choice> <option> <label>Yes, I would like more XML books</label> <value>yes</value> </option> <option> <label>No, I have all the books I need</label> <value>no</value> </option> </choice> <next> <if> <eq> <answer>booktraining</answer> <text>yes</text> </eq> <text>timeframe</text> <block> <save><answer>email</answer></save> <text>done</text> </block> </if> </next> </question> <question> <name>timeframe</name> <title>Timeframe</title> <label>When do you plan to buy new XML books?</label> <choice> <option> <label>Now</label> <value>now</value> </option> <option> <label>Within 3 months</label> <value>months</value> </option> <option> <label>Within a year</label> <value>year</value> </option> <option> <label>I don't know yet</label> <value>unknown</value> </option> </choice> <next> <block> <save><answer>email</answer></save> <text>done</text> </block> </next> </question> <question> <name>done</name> <title>Thank you</title> <label>Thank you for your time!</label> <next><text>done</text></next> </question> </survey> The survey is a list of question in which each question has the following:
The first question in the listing will be rendered as in Figure 2.1. Note that the script could not be simpler; it unconditionally moves to the next question: <question> <name>email</name> <title>Welcome to our XML book survey</title> <label>Thank you for participating in our survey.</label> <input>Enter your email address</input> <next><text>usingxml</text></next> </question> Figure 2.1. The survey first asks for your email address.
Designing SurvexThe model behind Survex is shown in Figure 2.2. The main classes are
Figure 2.2. The script is modeled as Statement descendants.
The Data StructureAt the heart of the data structure is the Question class (see Listing 2.2). The Question defines a number of properties: the name , title , and label , as well as the input or the list of options . Note that the code enforces an exclusive on the input and the list of options . Finally, the script is used. Listing 2.2 Question.javapackage com.psol.survex; import java.io.*; import java.util.*; public class Question { protected String name, title, label, input; protected Option[] options; protected Statement script; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public Option[] getOptions() { return options; } public void setOptions(Option[] options) { this.options = options; input = null; } public String getInput() { return input; } public void setInput(String input) { this.input = input; options = null; } public void setScript(Statement script) { this.script = script; } public Statement getScript() { return script; } } Question uses the Option class , in Listing 2.3, to store the properties for the various options. Each option has a label and a value . Listing 2.3 Option.javapackage com.psol.survex; import java.io.*; public class Option { protected String label, value; public void setLabel(String label) { this.label = label; } public String getLabel() { return label; } public void setValue(String value) { this.value = value; } public String getValue() { return value; } } At the root of the data structure is the Survey class (see Listing 2.4). It maintains the list of questions in a dictionary for fast retrieval. Listing 2.4 Survey.javapackage com.psol.survex; import java.io.*; import java.util.*; public class Survey { protected String rootName; protected Dictionary questions = new Hashtable(); public Enumeration getKeys() { return questions.keys(); } public void addQuestion(Question question) { if(questions.isEmpty()) rootName = question.getName(); questions.put(question.getName(),question); } public Question getQuestion(String name) { return (Question)questions.get(name); } public Question getRootQuestion() { return (Question)questions.get(rootName); } } Building a Script InterpreterA script is an object that implements the Statement interface (see Listing 2.5). The interface is trivial, defining only one method, apply() , which executes the statement and returns a string. For simplicity, the string is the only data type. Also, no local variables exist, only global parameters. Listing 2.5 Statement.javapackage com.psol.survex; import java.util.Dictionary; import javax.servlet.ServletException; public interface Statement { public String apply(Dictionary parameters) throws ServletException; } Looking back at Listing 2.1, you can identify the following statements:
The <text> statement is implemented in the class named Constant (see Listing 2.6). Constant has one property, text , and its apply() method returns the value of the text property. Listing 2.6 Constant.javapackage com.psol.survex; import java.util.Dictionary; import javax.servlet.ServletException; public class Constant implements Statement { protected String text; public void setText(String text) { this.text = text; } public String apply(Dictionary parameters) throws ServletException { return text; } } The <answer> XML element is implemented in the Parameter class in Listing 2.7. This class has one property, name , and its apply() method returns the parameter whose name matches the name property. As you will see, the servlet loads the parameters with the visitor's choices. Listing 2.7 Parameter.javapackage com.psol.survex; import java.util.Dictionary; import javax.servlet.ServletException; public class Parameter implements Statement { protected String name; public void setName(String name) { this.name = name; } public String apply(Dictionary parameters) throws ServletException { String st = (String)parameters.get(name); return null != st ? st : ""; } } Equal (in Listing 2.8) supports the <eq> statement . Equal has two properties, arg1 and arg2 , both of which are Statement s themselves . Equal executes the two Statement s (by calling their apply() method) and compares the results. Note Equal illustrates how Statement s are combined. The scripting language has a distinct functional style: Each Statement is a function (it takes one or more parameters and returns a value). Also, no global variables exist. A functional style is simpler to understand and, remember, you are looking for a simple-to-use scripting language. For more sophistication, you would have turned to an existing scripting language. Listing 2.8 Equal.javapackage com.psol.survex; import java.util.Dictionary; import javax.servlet.ServletException; public class Equal implements Statement { protected Statement arg1, arg2; public void setArgs(Statement arg1,Statement arg2) { this.arg1 = arg1; this.arg2 = arg2; } public String apply(Dictionary parameters) throws ServletException { String value1 = arg1.apply(parameters), value2 = arg2.apply(parameters); return value1.equals(value2) ? "true" : "false"; } } If has three properties: cond , then , and _else . It executes the first statement: cond , the condition. Depending on the result, it next executes the then or _else statement (see Listing 2.9). Listing 2.9 If.javapackage com.psol.survex; import java.util.Dictionary; import javax.servlet.ServletException; public class If implements Statement { protected Statement cond, then, _else; public void setArgs(Statement cond, Statement then, Statement _else) { this.cond = cond; this.then = then; this._else = _else; } public String apply(Dictionary parameters) throws ServletException { if(cond.apply(parameters).equals("true")) return then.apply(parameters); else return _else.apply(parameters); } } Save is a special function because it has a side effect: It creates a file and writes the parameters. The script uses this before terminating the survey (see Listing 2.10). Tip Listing 2.10 saves the survey results under the visitor's email address. Email addresses are a simple mechanism to identify visitors . Obviously, some people have several email addresses, whereas some families share email addresses, but it's accurate enough for our needs. An added bonus is that when a visitor changes his mind and answers differently, the new answer overrides the older one. Listing 2.10 Save.javapackage com.psol.survex; import java.io.*; import java.util.*; import javax.servlet.ServletException; public class Save implements Statement { protected Statement filename; public void setFilename(Statement filename) { this.filename = filename; } public void escape(Writer w,String s) throws IOException { for(int i = 0;i < s.length();i++) { char c = s.charAt(i); if(c == '<') w.write("<"); else if(c == '&') w.write("&"); else if(c == '\ '') w.write("'"); else if(c == '"') w.write("""); else if(c > '\ u007f') { w.write("&#"); w.write(Integer.toString(c)); w.write(';'); } else w.write(c); } } public String apply(Dictionary parameters) throws ServletException { try { String fname = filename.apply(parameters); File file = new File("results",fname + ".xml"); Writer writer = new FileWriter(file); writer.write("<?xml version='1.0'?><survex>"); Enumeration keys = parameters.keys(); while(keys.hasMoreElements()) { String name = (String)keys.nextElement(); writer.write("<question><name>"); escape(writer,name); writer.write("</name><answer>"); escape(writer,(String)parameters.get(name)); writer.write("</answer></question>"); } writer.write("</survex>"); writer.close(); return fname; } catch(IOException e) { throw new ServletException(e); } } } Block offers a solution to combine several Statement s. It executes each Statement and returns the result of the last one. In effect, this is similar to the { } construct in Java (see Listing 2.11). Listing 2.11 Block.javapackage com.psol.survex; import java.util.*; import javax.servlet.ServletException; public class Block implements Statement { protected Statement[] statements; public void setStatements(Statement[] statements) { this.statements = statements; } public String apply(Dictionary parameters) throws ServletException { String result = ""; // on the stack they are collected in reverse order for(int i = statements.length - 1;i >= 0;i--) result = statements[i].apply(parameters); return result; } } Reading the Configuration FileThe extensive data structure must be read from the XML configuration file. This is the role of SurveyReader , a class that implements the SAX's ContentHandler interface . SurveyReader is demonstrated in Listing 2.12. Listing 2.12 SurveyReader.javapackage com.psol.survex; import org.xml.sax.*; import java.util.*; public class SurveyReader implements ContentHandler { protected Stack stack; protected StringBuffer buffer; public Survey getSurvey() { return (Survey)stack.pop(); } public void setDocumentLocator(Locator locator) {} public void startDocument() { stack = new Stack(); } public void endDocument() {} public void startElement(String namespaceURI, String localName, String tag, Attributes atts) { if(tag.equals("survey")) stack.push(new Survey()); else if(tag.equals("question")) stack.push(new Question()); else if(tag.equals("choice")) stack.push(new Vector()); else if(tag.equals("option")) stack.push(new Option()); else if(tag.equals("input")) buffer = new StringBuffer(); else if(tag.equals("text")) { stack.push(new Constant()); buffer = new StringBuffer(); } else if(tag.equals("answer")) { stack.push(new Parameter()); buffer = new StringBuffer(); } else if(tag.equals("if")) stack.push(new If()); else if(tag.equals("eq")) stack.push(new Equal()); else if(tag.equals("save")) stack.push(new Save()); else if(tag.equals("block")) stack.push(new Block()); else if(tag.equals("name") tag.equals("title") tag.equals("label") tag.equals("value")) buffer = new StringBuffer(); } public void endElement(String namespaceURI, String localName, String tag) { if(tag.equals("question")) { Question question = (Question)stack.pop(); Survey survey = (Survey)stack.peek(); survey.addQuestion(question); } else if(tag.equals("choice")) { Vector vector = (Vector)stack.pop(); Option[] options = new Option[vector.size()]; vector.copyInto(options); Question question = (Question)stack.peek(); question.setOptions(options); } else if(tag.equals("option")) { Option option = (Option)stack.pop(); Vector vector = (Vector)stack.peek(); vector.addElement(option); } else if(tag.equals("input")) { Question question = (Question)stack.peek(); question.setInput(buffer.toString()); buffer = null; } else if(tag.equals("text")) { Constant constant = (Constant)stack.peek(); constant.setText(buffer.toString()); buffer = null; } else if(tag.equals("answer")) { Parameter parameter = (Parameter)stack.peek(); parameter.setName(buffer.toString()); buffer = null; } else if(tag.equals("if")) { Statement _else = (Statement)stack.pop(), then = (Statement)stack.pop(), cond = (Statement)stack.pop(); If _if = (If)stack.peek(); _if.setArgs(cond,then,_else); } else if(tag.equals("eq")) { Statement arg1 = (Statement)stack.pop(), arg2 = (Statement)stack.pop(); Equal equal = (Equal)stack.peek(); equal.setArgs(arg1,arg2); } else if(tag.equals("save")) { Statement filename = (Statement)stack.pop(); Save save = (Save)stack.peek(); save.setFilename(filename); } else if(tag.equals("block")) { Vector vector = new Vector(); Statement s = (Statement)stack.pop(); while(!(s instanceof Block)) { vector.addElement(s); s = (Statement)stack.pop(); } Statement[] statements = new Statement[vector.size()]; vector.copyInto(statements); ((Block)s).setStatements(statements); stack.push(s); } else if(tag.equals("name")) { Question question = (Question)stack.peek(); question.setName(buffer.toString()); buffer = null; } else if(tag.equals("title")) { Question question = (Question)stack.peek(); question.setTitle(buffer.toString()); buffer = null; } else if(tag.equals("label")) { Object o = stack.peek(); if(o instanceof Question) ((Question)o).setLabel(buffer.toString()); else ((Option)o).setLabel(buffer.toString()); buffer = null; } else if(tag.equals("value")) { Option option = (Option)stack.peek(); option.setValue(buffer.toString()); buffer = null; } else if(tag.equals("next")) { Statement script = (Statement)stack.pop(); Question question = (Question)stack.peek(); question.setScript(script); } } public void characters(char ch[],int start,int len) { if(null != buffer) buffer.append(ch,start,len); } public void ignorableWhitespace(char ch[], int start, int length) {} public void processingInstruction(String target,String data) {} public void skippedEntity(String name) {} public void startPrefixMapping(String prefix,String uri) {} public void endPrefixMapping(String prefix) {} } Notice that this class uses a different approach to tracking states than the DocumentHandler from Chapter 1, "Lightweight Data Storage." Specifically, instead of using constants, it uses a stack. In startElement() , it pushes objects on the stack: else if(tag.equals("option")) stack.push(new Option()); And in endElement() , it pops. In most cases, it will pass them (as properties) to their parents, which are also in the stack: else if(tag.equals("option")) { Option option = (Option)stack.pop(); Vector vector = (Vector)stack.peek(); vector.addElement(option); } Putting It All Together in the ServletFrom these building blocks, building the servlet is not difficult. The servlet class, Survex , is shown in Listing 2.13. Listing 2.13 Survex.javapackage com.psol.survex; import java.io.*; import java.util.*; import org.xml.sax.*; import javax.servlet.*; import javax.servlet.http.*; import org.xml.sax.helpers.*; public class Survex extends HttpServlet { public static final String PARSER_NAME = "org.apache.xerces.parsers.SAXParser"; protected Survey survey; public void init() throws ServletException { try { XMLReader xmlReader = XMLReaderFactory.createXMLReader(PARSER_NAME); SurveyReader sreader = new SurveyReader(); xmlReader.setContentHandler(sreader); xmlReader.parse("survey.xml"); survey = sreader.getSurvey(); } catch(IOException e) { throw new ServletException(e); } catch(SAXException e) { throw new ServletException(e); } } public Dictionary getParameters(HttpServletRequest request) { Dictionary parameters = new Hashtable(); Enumeration keys = survey.getKeys(); while(keys.hasMoreElements()) { String name = (String)keys.nextElement(), value = request.getParameter(name); if(null != value) parameters.put(name,value); } return parameters; } public void writeHTML(Question question, String servletpath, Writer writer, Dictionary parameters) throws IOException { writer.write("<HTML><HEAD><TITLE>"); writer.write("A Survex Survey: "); writer.write(question.getTitle()); writer.write("</TITLE></HEAD><BODY>"); writer.write("<FORM ACTION='"); writer.write(servletpath); writer.write("'METHOD='POST'>"); writer.write("<INPUT TYPE='HIDDEN'NAME='name'VALUE='"); writer.write(question.getName()); writer.write("'>"); writer.write("<TABLE ALIGN='CENTER'BORDER='1'>"); writer.write("<TR><TD BGCOLOR='black'><B>"); writer.write("<FONT COLOR='white'>"); writer.write(question.getTitle()); writer.write("</B></FONT></TD></TR><TR><TD><P>"); writer.write(question.getLabel()); if(null != question.getOptions()) { writer.write("<P>"); Option[] options = question.getOptions(); for(int i = 0;i < options.length;i++) { writer.write("<INPUT TYPE='RADIO'NAME='"); writer.write(question.getName()); writer.write("'VALUE='"); writer.write(options[i].getValue()); writer.write("'>"); writer.write(options[i].getLabel()); writer.write("<BR>"); } } else if(null != question.getInput()) { writer.write("<P>"); writer.write(question.getInput()); writer.write(": <INPUT TYPE='TEXT'NAME='"); writer.write(question.getName()); writer.write("'>"); } if(null != question.getOptions() null != question.getInput()) writer.write("<P><INPUT TYPE='SUBMIT'VALUE='Next'>"); writer.write("</TD><TR></TABLE>"); Enumeration keys = parameters.keys(); while(keys.hasMoreElements()) { String parameter = (String)keys.nextElement(); writer.write("<INPUT TYPE='HIDDEN'NAME='"); writer.write(parameter); writer.write("'VALUE='"); writer.write((String)parameters.get(parameter)); writer.write("'>"); } writer.write("</FORM></BODY></HTML>"); writer.flush(); } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Question question = survey.getRootQuestion(); if(null != question) writeHTML(question, request.getServletPath(), response.getWriter(), new Hashtable()); else response.sendError(HttpServletResponse.SC_NOT_FOUND); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Dictionary parameters = getParameters(request); String name = request.getParameter("name"); Question question = null; if(null == name) question = survey.getRootQuestion(); else { question = survey.getQuestion(name); if(null != question) { Statement script = question.getScript(); name = script.apply(parameters); question = survey.getQuestion(name); } } if(null != question) writeHTML(question, request.getServletPath(), response.getWriter(), parameters); else response.sendError(HttpServletResponse.SC_NOT_FOUND); } } Review the following listing step by step. The first method is init() , which reads the XML configuration file upon loading. Next, the class defines two helper methods : getParameters() and writeHTML() . getParameters() collects the answers for all the questions. As you will see, the browser always has the entire list of answers and passes them to the servlet with each request: public Dictionary getParameters(HttpServletRequest request) { Dictionary parameters = new Hashtable(); Enumeration keys = survey.getKeys(); while(keys.hasMoreElements()) { String name = (String)keys.nextElement(), value = request.getParameter(name); if(null != value) parameters.put(name,value); } return parameters; } writeHTML() prints a Question as an HTML page. Notice that the page includes the various answers as hidden input fields. Another hidden field contains the name of the current question. These hidden fields are returned to the server by the browser with each request, as in the following: public void writeHTML(Question question, String servletpath, Writer writer, Dictionary parameters) throws IOException { writer.write("<HTML><HEAD><TITLE>"); writer.write("A Survex Survey: "); writer.write(question.getTitle()); writer.write("</TITLE></HEAD><BODY>"); writer.write("<FORM ACTION='"); writer.write(servletpath); writer.write("'METHOD='POST'>"); writer.write("<INPUT TYPE='HIDDEN'NAME='name'VALUE='"); writer.write(question.getName()); writer.write("'>"); writer.write("<TABLE ALIGN='CENTER'BORDER='1'>"); writer.write("<TR><TD BGCOLOR='black'><B>"); writer.write("<FONT COLOR='white'>"); writer.write(question.getTitle()); writer.write("</B></FONT></TD></TR><TR><TD><P>"); writer.write(question.getLabel()); if(null != question.getOptions()) { writer.write("<P>"); Option[] options = question.getOptions(); for(int i = 0;i < options.length;i++) { writer.write("<INPUT TYPE='RADIO'NAME='"); writer.write(question.getName()); writer.write("'VALUE='"); writer.write(options[i].getValue()); writer.write("'>"); writer.write(options[i].getLabel()); writer.write("<BR>"); } } else if(null != question.getInput()) { writer.write("<P>"); writer.write(question.getInput()); writer.write(": <INPUT TYPE='TEXT'NAME='"); writer.write(question.getName()); writer.write("'>"); } if(null != question.getOptions() null != question.getInput()) writer.write("<P><INPUT TYPE='SUBMIT'VALUE='Next'>"); writer.write("</TD><TR></TABLE>"); Enumeration keys = parameters.keys(); while(keys.hasMoreElements()) { String parameter = (String)keys.nextElement(); writer.write("<INPUT TYPE='HIDDEN'NAME='"); writer.write(parameter); writer.write("'VALUE='"); writer.write((String)parameters.get(parameter)); writer.write("'>"); } writer.write("</FORM></BODY></HTML>"); writer.flush(); } doGet() outputs the first (or root) question to get the user started. doPost() , on the other hand, is where all the fun is because it uses the script to decide on which question to post. doPost() first retrieves the name of the current question and the answers to the various questions. Next, it calls the script to compute the name of the next question. It couldn't be simpler! The following demonstrates this: public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Dictionary parameters = getParameters(request); String name = request.getParameter("name"); Question question = null; if(null == name) question = survey.getRootQuestion(); else { question = survey.getQuestion(name); if(null != question) { Statement script = question.getScript(); name = script.apply(parameters); question = survey.getQuestion(name); } } if(null != question) writeHTML(question, request.getServletPath(), response.getWriter(), parameters); else response.sendError(HttpServletResponse.SC_NOT_FOUND); } |