Walkthrough
Let's step through a typical user session. We assume our real estate agent is on the road, using her mobile phone to access our Real Estate Assistant service. For ease of illustration, we use a phone simulator (available from Openwave) to show user interaction.
Authentication
The agent logs onto our system from her browser, using a bookmarked URL containing our address, and specifies a parameter OP=getQuery. The Real Estate Assistant URL is protected by the following configuration, as defined in our application's deployment descriptor:
<!-- ACL --> <security-constraint> <web-resource-collection> <web-resource-name>MLS Application</web-resource-name> <url-pattern>/query</url-pattern> </web-resource-collection> <auth-constraint> <role-name>agent</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>Authentication Area</realm-name> </login-config>
This XML snippet defines a security constraint that applies to the specified URL pattern (/query), the URL of our servlet. The login configuration that follows specifies HTTP Basic authentication, supplying a realm name for optional display by the client. Note that this form of authentication has a low level of security but is probably adequate for publicly accessible information such as our property data.
Servlet Controller
Once the agent has entered her username and password, the request will be sent to the HttpServlet running within our application server's servlet container. The following code handles this request:
/** * Parse out the operation and call the appropriate method. * @param request the Http request from the user agent * @param response where the response is to be written */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Select viewer based on request headers Viewer viewer = htmlViewer; if (isWapUser(request)) viewer = wmlViewer; /* ensure we have an authenticated user */ String user = request.getRemoteUser(); if (null == user) { /* encode a redirect to the "unauthorised access" deck, N.B. this could be caused by the authentication header not being passed to us, or incorrect web server - servlet engine integration */ viewer.viewError(request, response, Errors.UNAUTHORISED_USER); return; } ...
The doGet operation first selects the right viewer for this agent by calling a method that returns true if this is a WAP user. The isWapUser method is very simple and assumes that "WML" will be present in the HTTP Accept header for all WAP users:
/** * Return true if this request is from a WAP user agent * @param request the Http request from the user agent * @return true if this request is from a WAP user agent */ private boolean isWapUser(HttpServletRequest request) { String acceptHeader = request.getHeader("Accept").toUpperCase(); return acceptHeader.indexOf("WML") >= 0; }
Next, the method makes sure that this agent has been properly authenticated by the "built-in" HTTP Basic authentication, by checking the getRemoteUser parameter in the agent's request.
To satisfy the agent's request, the servlet must determine which workflow operation has been requested. It does this by retrieving the parameter from the request:
String operation = request.getParameter("OP");
If an operation was not specified, an error message is displayed:
viewer.viewError(request, response, Errors.BAD_OPERATION);
Viewing Errors
The viewer object (either an HTML or WML viewer) has overloaded viewError methods that display simple text error messages. The following code from the Viewer parent class shows one of these methods:
/** * Display an error page containing the specified error message and * error (throwable) information * @param request the Http request from the user agent * @param response where the response is to be written * @param error an error message to display * @param throwable An error from which additional info will be * displayed */ public void viewError(HttpServletRequest request, HttpServletResponse response, String error, Throwable throwable) throws IOException { setType(response); String msg = ERROR + error; if (throwable != null) msg += ": " + throwable.toString(); viewFixed(response, msg); }
This method (defined in the WmlViewer child class) sets the type (in our case, to Wireless Markup Language):
response.setContentType("text/vnd.wap.wml");
It then builds an error message with and calls viewFixed (defined in the WmlViewer class) to display this:
/** * View a canned page * @param response where the response is to be written * @param textToDisplay the text to display */ protected void viewFixed(HttpServletResponse response, String textToDisplay) throws IOException { PrintWriter out = response.getWriter(); printPrologue(out); out.println("<wml><card><p>"); out.println(textToDisplay); out.println("</p></card></wml>"); }
Operation Processing
Assuming all is well with our request and it specified an OP parameter, doGet attempts to locate a method by this name in the workflow object. It uses Java's reflection capabilities to invoke this method, allowing us to alter the workflow without breaking the servlet:
// invoke operation method using reflection try { // set parameters for operation method java.lang.Class[] parameterTypes = {Class.forName("javax.servlet.http.HttpServletRequest"), Class.forName("javax.servlet.http.HttpServletResponse"), Class.forName("mls.view.Viewer")}; java.lang.reflect.Method operationMethod = workflow.getClass().getMethod(operation, parameterTypes); if (null == operationMethod) viewer.viewError(request, response, Errors.BAD_OPERATION); else { java.lang.Object[] args = {request, response, viewer}; operationMethod.invoke(workflow, args); } } catch (java.lang.reflect.InvocationTargetException it) { viewer.viewError(request, response, Errors.ERROR_PERFORMING_OP, it.getTargetException()); } catch (Exception e) { viewer.viewError(request, response, Errors.ERROR_PERFORMING_OP, e); }
The operationMethod.invoke call will execute the getQuery method in Workflow:
/** * Operation method to display a query entry page * @param request the Http request from the user agent * @param response where the response is to be written * @param viewer a viewer to display the output */ public boolean getQuery(HttpServletRequest request, HttpServletResponse response, Viewer viewer) throws IOException { try { viewer.viewPage(request, response, QUERY_PAGE, QUERY_CLASS, Viewer.NO_CACHE); } catch (Exception e) { viewer.viewError(request, response, Errors.ERROR_MAKING_QUERY, e); } return true; }
Viewing the Query Page
The getQuery operation displays a new Wireless Markup Language query page via a call to the WmlViewer. WmlViewer creates an empty query object and merges this object with a query template:
/** Display the specified page containing the specified object * @param request the Http request from the user agent * @param response where the response is to be written * @param templateName the name of the XML template * @param object the object to view */ public void viewPage(HttpServletRequest request, HttpServletResponse response, String templateName, java.lang.Object object, String cachePage) throws Exception { setType(response); setCache(cachePage, response); PrintWriter out = response.getWriter(); printPrologue(out); try { merge(object, TEMPLATE_DIR + templateName, out); } catch (Exception e) { viewError(request, response, Errors.MERGE_ERROR, e); } }
This method is actually called by a thin wrapper of the same name that accepts a class name and builds a new instance of the class as the object parameter. ViewPage sets the response type (discussed earlier) and the cache header:
if (cachePage.equals(NO_CACHE)) { response.setHeader("Cache-Control", "no-cache, must-revalidate"); response.setHeader("Pragma", "no-cache"); } ... response.setHeader("forua", "true"); A WML prologue is generated: out.println(XML_PROLOGUE); out.println(mls.common.Config.WML_PROLOGUE);
Finally, merge is called to do the actual XSLT processing of the new query object:
/** * Generate XML from the specified data and XSL style sheet * @param xml a Java object containing data * @param xsl an XSL style sheet file which will generate * appropriate markup * @param out an output print writer */ private void merge(java.lang.Object data, String xsl, PrintWriter out) throws Exception { merge(parseString(TranslatingXmlObject.object2Xml(data)), parse(xsl), out); }
Merging XSL and XML
merge is an overloaded method, and the one that we call translates the specified object into an XML document before calling the general-purpose merge:
/** * Generate XML from the specified XML data and XSL style sheet * @param xml a DOM document containing data as XML * @param xsl an XSL style sheet which will generate * appropriate markup * @param out an output print writer */ private void merge(Document xml, Document xsl, PrintWriter out) throws Exception{ XSLTProcessor processor = XSLTProcessorFactory.getProcessor( new org.apache.xalan.xpath.xdom.XercesLiaison()); XSLTInputSource data = new XSLTInputSource(xml); XSLTInputSource layout = new XSLTInputSource(xsl); XSLTResultTarget target = new XSLTResultTarget(out); processor.process(data, layout, target); }
This method uses the Apache project's XSLT processor, Xerces, to process a query XSL style sheet as defined in Listing 1 (click here to download the listing files for this article). Listing 1 defines a single XSL template with a root rule that generates a WML deck with a single card. If you're unfamiliar with either WML or XSL syntax, see the InformIT articles Wireless Markup Language, Wireless Markup Language - Beyond the Basics, and XSL Transformations.
Post field tags ensure that the correct operation (doQuery) and the required user inputs will be passed to our servlet. The servlet URL is specified (in our test system, this is running on a WebLogic server at port 7001 of the fictional URL http://www.mls123).
Three drop-down select lists are defined, and their values are pulled out of the specified query data structure. For example, the minimum price selection list is defined by an xsl:element element that will result in the generation of a <select> element with its value attribute set from the minPrice field of a Query object:
<!-- Minimum price input field --> Min Price: <xsl:element name="select"> <xsl:attribute name="name">min</xsl:attribute> <xsl:attribute name="value"> <xsl:value-of select="Query/minPrice"/> </xsl:attribute> <option value="0">No Limit</option> <option value="10000">$$10,000</option> ... <option value="300000">$$300,000</option> </xsl:element>
Similarly, we define a maximum price drop-down list and a property type list. Note the use of $$, as $ is a reserved character in WML and must be escaped. The merge method processes this style sheet against the empty query object and produces a WML deck, as shown in Figure 1. The deck is presented as a series of screens—one for each select list. Once the final item has been selected, the agent submits this query for processing.
Figure 1 Mobile interface query.
Processing the Query
As in our agent's initial request, HttpServlet and Workflow objects control the query processing. This time, the doQuery method is called:
/** * Operation method to do a query. If the returned results are * larger than the resultsPerPage, these results are saved in the * user's session for later use - see #fetchNext() * @param request the Http request from the user agent * @param response where the response is to be written * @param viewer a viewer to display the output * @return true */ public boolean doQuery(HttpServletRequest request, HttpServletResponse response, Viewer viewer) throws IOException { try { Query query = new Query(); query.setMinPrice(request.getParameter("min")); query.setMaxPrice(request.getParameter("max")); query.type = request.getParameter("type"); // Get number of results to display per page int resultsPerPage = getNewResultsPerPage(request); if (query.isValid()) { Results results = MlsBusinessObject.doQuery(query); if (results.properties.length > resultsPerPage) { // save request.getSession().setAttribute(LAST_RESULTS, results); Results subset = results.getSubset(0, resultsPerPage); viewer.viewPage(request, response, RESULTS_PAGE, subset, Viewer.CACHE); } else viewer.viewPage(request, response, RESULTS_PAGE, results, Viewer.CACHE); } else { viewer.viewError(request, response, Errors.ERROR_IN_QUERY_PARAMS); } } catch (Exception e) { viewer.viewError(request, response, Errors.ERROR_DOING_QUERY, e); } return true; }
doQuery creates a query object and initializes this from the submitted parameters in the request object. If applicable, the number of properties to display per page is also set (this is only set by a web user). Next, the query parameters are validated and the Multiple Listing Service is called to do the query. If too many results are returned to fit on one page, a subset of the results is displayed by a call to the viewer.
Displaying the Results
The results style sheet is shown in Listing 2 in the listing files for this article. Note that the link definition around line 30 has been split across two lines to make it readable; this would need to be "reassembled" for actual XSLT processing. Once again, this style sheet has only a single root template that produces a WML deck. This time, however, we may produce multiple cards, if there are any results to display.
We begin by defining a results variable that contains a complete list of all properties. Next, we declare a results card; if there are no results (<xsl:when test="count($matches)=0">), we let the agent know this. If there are results, the style sheet iterates over the matches and builds a link that displays the property's address and links to a details card within this WML deck. The unique MLS identifier from the property data identifies the detail card.
Next, the style sheet tests whether there are more results by checking a flag set in the results XML; it then builds a Next link if required. (The next article discusses the function of this link.) The last element added to this card is a Requery link that goes back to the getQuery operation.
The detail cards are generated by looping over all matched properties and displaying either land or property details, depending on the property type. Each of these cards displays a street and price and contains a navigation widget to return to the summary Results card. Figure 2 illustrates the results of an MLS query.
Figure 2 Mobile interface results screens.
The Test XML Data
As mentioned above, the prototype for this system uses an XML test file as its MLS "database." A test set of real estate properties is shown in Listing 3 in the listing files for this article. Results.xml declares two land-only properties and six single-family dwellings, all in the same geographic area. Note that these XML elements map directly to the Java domain classes, Results, Property, Address, and Lot.