"We want to simulate search and results pages. There should be a page where we enter the number of results to return (this would simulate a search query). It should look something like this:"
"There should also be an error page (not shown) if the user enters
anything but a positive integer into the field."
"The results page should show at most 10 results per page and should
provide links for next, previous, and results 1-10, 11-20, etc. The
links should only be there if there is a next or a previous."
Drawing the state diagram, we realize we have something like this:
In the diagram, "numbered navigation link" refers to any of the 1-10
| 11-20 | 21-27 links that take you directly to those results.
/*
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*/package com.startups.testmvc;
import com.startups.validation.ValidatableObject;
import com.startups.validation.ValidationFailedException;/**
* The model for the ChoosingNumberOfResults state. In this
* state, the user chooses the number of results to be
* displayed in subsequent pages (this simulates a search
* result that could span multiple pages).
**/
public class ChoosingNumberOfResultsModel extends ValidatableObject {
private String number = "";//
// Accessors
//
public String getDesiredNumberOfResults() {
return this.number;
}public void setDesiredNumberOfResults(String desiredNumber) {
this.number = desiredNumber;
}//
// Validations
//
/**
* Check that the desired number of results is an integer > 0.
**/
public void validateDesiredNumberOfResults() throws ValidationFailedException {
try {
int theNumber = Integer.parseInt(this.number);
if (theNumber < 1) {
throw new ValidationFailedException(
"Number of results must be > 0");
}
} catch (NumberFormatException e) {
throw new ValidationFailedException(
"Number of results must be an integer.");
} catch (NullPointerException e) {
throw new ValidationFailedException(
"Somebody set the desired number to null");
}
}
}
/*Going through piece by piece, we have the declaration:
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*/package com.startups.testmvc;
import java.util.Random;
import com.startups.validation.ValidatableObject;
import com.startups.validation.ValidationFailedException;/**
* The model for displaying the results of a simulated
* search query. These results may be displayed across
* several jsp pages. A single jsp view may only display
* 10 results at a time. Because of this, the model keeps
* track of the position that is being displayed by the
* current view (via getStartingIndex()).
**/
public class DisplayingResultsModel extends ValidatableObject {
private static final Random random = new Random();
private String[] resultsArray;
private int startingIndex;//
// Accessors
//
/**
* Set the number of results in this model.
* Calling this method will generate a bunch
* of random results. In principle, this is
* similar to what would happen if we did a
* search. The parameter wouldn't be a number,
* but a string of search terms. Instead of
* generating random results, we'd actually
* hit the database and come up with some
* sensible results, which would probably
* be more complicated than simple strings.
**/
public void setResultsNumber(int number) {
this.resultsArray = new String[number];
fillResultsArray();
}/**
* Fill the results array with random gibberish.
**/
private void fillResultsArray() {
for (int i = 0; i < this.resultsArray.length; i++) {
this.resultsArray[i] = String.valueOf(
DisplayingResultsModel.random.nextInt());
}
}/**
* Sets the starting index of the results to display.
**/
public void setStartingIndex(int startingIndex) {
this.startingIndex = startingIndex;
}/**
* Return the starting index of the results to display.
* A single jsp view may only view a fraction
**/
public int getStartingIndex() {
return this.startingIndex;
}/**
* Return the total number of results that could be displayed.
**/
public int getNumberOfResults() {
return this.resultsArray.length;
}/**
* Return the result at the given index. The result
* is a simple string.
**/
public String getResultAt(int index) {
return this.resultsArray[index];
}/**
* Returns the ending index of the results to display.
* This is either the starting index + 10 or the
* index of the last result.
**/
public int getEndingIndex() {
if (this.resultsArray.length > getStartingIndex() + 10) {
return getStartingIndex() + 10;
} else {
return resultsArray.length;
}
}
//
// Validation
//
/**
* Make sure that the starting index is >= 0 and is
* not greater than the total number of results.
**/
public void validateStartingIndex() throws ValidationFailedException {
if (getStartingIndex() < 0) {
throw new ValidationFailedException(
"Starting index must be >= 0");
}
if (getStartingIndex() >= getNumberOfResults()) {
throw new ValidationFailedException(
"Starting index must be < the number of results"
+ " (" + getNumberOfResults() + ")");
}
}
}
/*The model stores a String array of simulated results, and a starting index that indicates which result the view should start showing. The Random instance is used later to generate simulated search results.
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*/package com.startups.testmvc;
import java.util.Random;
import com.startups.validation.ValidatableObject;
import com.startups.validation.ValidationFailedException;/**
* The model for displaying the results of a simulated
* search query. These results may be displayed across
* several jsp pages. A single jsp view may only display
* 10 results at a time. Because of this, the model keeps
* track of the position that is being displayed by the
* current view (via getStartingIndex()).
**/
public class DisplayingResultsModel extends ValidatableObject {
private static final Random random = new Random();
private String[] resultsArray;
private int startingIndex;
The next part of the code contains the accessors--methods that allow the controller servlet to get and set properties of the model. An important method is setResultsNumber(), which generates the random results via a helper method called fillResultsArray():
//The next part of the code continues defining accessors:
// Accessors
//
/**
* Set the number of results in this model.
* Calling this method will generate a bunch
* of random results. In principle, this is
* similar to what would happen if we did a
* search. The parameter wouldn't be a number,
* but a string of search terms. Instead of
* generating random results, we'd actually
* hit the database and come up with some
* sensible results, which would probably
* be more complicated than simple strings.
**/
public void setResultsNumber(int number) {
this.resultsArray = new String[number];
fillResultsArray();
}/**
* Fill the results array with random gibberish.
**/
private void fillResultsArray() {
for (int i = 0; i < this.resultsArray.length; i++) {
this.resultsArray[i] = String.valueOf(
DisplayingResultsModel.random.nextInt());
}
}
/**The getEndingIndex() method returns a calculated value. This method will be used to determine the last result to display on the page:
* Sets the starting index of the results to display.
**/
public void setStartingIndex(int startingIndex) {
this.startingIndex = startingIndex;
}/**
* Return the starting index of the results to display.
* A single jsp view may only view a fraction
**/
public int getStartingIndex() {
return this.startingIndex;
}/**
* Return the total number of results that could be displayed.
**/
public int getNumberOfResults() {
return this.resultsArray.length;
}/**
* Return the result at the given index. The result
* is a simple string.
**/
public String getResultAt(int index) {
return this.resultsArray[index];
}
/**Finally, there is a validation method:
* Returns the ending index of the results to display.
* This is either the starting index + 10 or the
* index of the last result.
**/
public int getEndingIndex() {
if (this.resultsArray.length > getStartingIndex() + 10) {
return getStartingIndex() + 10;
} else {
return resultsArray.length;
}
}
//
// Validation
//
/**
* Make sure that the starting index is >= 0 and is
* not greater than the total number of results.
**/
public void validateStartingIndex() throws ValidationFailedException {
if (getStartingIndex() < 0) {
throw new ValidationFailedException(
"Starting index must be >= 0");
}
if (getStartingIndex() >= getNumberOfResults()) {
throw new ValidationFailedException(
"Starting index must be < the number of results"
+ " (" + getNumberOfResults() + ")");
}
}
}
<HTML>
<HEAD>
<TITLE> Multipage test </TITLE>
</HEAD>
<BODY>
<H1>Multipage test</H1>
<FORM ACTION="#" METHOD="POST">
<B>Number Of Results to Return:</B>
<INPUT TYPE="TEXT"
NAME="#"
VALUE="#"
SIZE="20"
MAXLENGTH="20">
<INPUT TYPE="SUBMIT">
</FORM>
</BODY>
</HTML>
<HTML>
<HEAD>
<TITLE> Multipage test </TITLE>
</HEAD>
<BODY>
<H1>Multipage test results</H1>
<A HREF="#">Previous </A> |
<A HREF="#">Next </A> |
<A HREF="#">1 - 10<P>
<TABLE>
<TR>
<TD> Result number: # </TD>
<TD> # </TD>
</TR></TABLE>
</FORM><HR>
<A HREF="/testmvc/ChoosingNumberOfResults.jsp">Another Search</A>
</BODY>
</HTML>
<%@ page import="com.startups.testmvc.ChoosingNumberOfResultsModel" %>
<jsp:useBean id="choosingNumberOfResultsModel" class="com.startups.testmvc.ChoosingNumberOfResultsModel" scope="session" create="yes"/><HTML>
<HEAD>
<TITLE> Multipage test </TITLE>
</HEAD>
<BODY>
<H1>Multipage test</H1>
<FORM ACTION="/servlet/com.startups.testmvc.ChoosingNumberOfResultsServlet" METHOD="POST">
<B>Number Of Results to Return:</B>
<INPUT TYPE="TEXT"
NAME="DesiredNumberOfResults"
VALUE="<%= choosingNumberOfResultsModel.getDesiredNumberOfResults() %>"
SIZE="20"
MAXLENGTH="20">
<INPUT TYPE="SUBMIT">
</FORM>
</BODY>
</HTML>
<%@ page import="com.startups.testmvc.ChoosingNumberOfResultsModel" %>
<%@ page import="com.startups.validation.ValidatableObject" %>
<%@ page import="com.startups.validation.ValidationFailedException" %>
<%!
//
// Returns a red font color tag if the given fieldName in the given
// model is invalid, otherwise returns a black font color tag.
// This method could be broken out into a separate jsp
// include.
//
public String fontColorTagFor(String fieldName, ValidatableObject model) {
if (model.isFieldValid(fieldName)) {
return "<FONT COLOR=\"black\">";
} else {
return "<FONT COLOR=\"red\">";
}
}
%>
<jsp:useBean id="choosingNumberOfResultsModel" class="com.startups.testmvc.ChoosingNumberOfResultsModel" scope="session" create="yes"/><HTML>
<HEAD>
<TITLE> Multipage test </TITLE>
</HEAD>
<BODY>
<H1>Multipage test</H1>
<%
if (! choosingNumberOfResultsModel.isValid()) {
//
// print out the fields that need to be changed
//
%>
<FONT COLOR="red">
Please review the indicated fields:<BR>
<%
ValidationFailedException[] theExceptions =
choosingNumberOfResultsModel.getValidationErrorList();
for (int i = 0; i < theExceptions.length; i++) {
%>
<%= theExceptions[i].getMessage() %> <br>
<%
}
%>
</FONT>
<%
}
%>
<FORM ACTION="/servlet/com.startups.testmvc.ChoosingNumberOfResultsServlet" METHOD="POST">
<%= fontColorTagFor("DesiredNumberOfResults", choosingNumberOfResultsModel) %>
<B>Number Of Results to Return:</B>
</FONT>
<INPUT TYPE="TEXT"
NAME="DesiredNumberOfResults"
VALUE="<%= choosingNumberOfResultsModel.getDesiredNumberOfResults() %>"
SIZE="20"
MAXLENGTH="20">
<INPUT TYPE="SUBMIT">
</FORM>
</BODY>
</HTML>
<%@ page import="com.startups.testmvc.DisplayingResultsModel" %>Let's look at the thing piece by piece so we can understand the changes. The first addition should be familiar:
<jsp:useBean id="displayingResultsModel" class="com.startups.testmvc.DisplayingResultsModel" scope="session" create="yes"/><HTML>
<HEAD>
<TITLE> Multipage test </TITLE>
</HEAD>
<BODY>
<H1>Multipage test results</H1><%
// Generate Previous | Next links, if necessary
int theNumberOfResults = displayingResultsModel.getNumberOfResults();
int startingIndex = displayingResultsModel.getStartingIndex();
if (startingIndex != 0) {
%>
<A HREF="/servlet/com.startups.testmvc.DisplayingResultsServlet?startIndex=<%= startingIndex - 10 %>">
Previous </A>
<%
} else {
%>
Previous
<%
}int endingIndex = displayingResultsModel.getEndingIndex();
if (endingIndex < theNumberOfResults) {
%>
| <A HREF="/servlet/com.startups.testmvc.DisplayingResultsServlet?startIndex=<%= startingIndex + 10 %>">
Next </A>
<%
} else {
%>
| Next
<%
}// Generate numbered links 1 - 10 | 11 - 20 | etc.
for (int i = 0;
i < theNumberOfResults;
i += 10) {
int theEndIndex = i + 10 < theNumberOfResults ? i + 10 : theNumberOfResults;
if (i != startingIndex) {
%>
|
<A HREF="/servlet/com.startups.testmvc.DisplayingResultsServlet?startIndex=<%= i %>">
<%= i + 1 %> - <%= theEndIndex %>
</A>
<%
} else {
%>
| <B>
<%= i + 1 %> - <%= theEndIndex %> </B>
<%
}
}%>
<P>
<TABLE>
<%
for (int i = displayingResultsModel.getStartingIndex();
i < displayingResultsModel.getEndingIndex();
i++) {
%>
<TR>
<TD> Result number: <%= i + 1 %> </TD>
<TD> <%= displayingResultsModel.getResultAt(i) %> </TD>
</TR>
<%
}
%>
</TABLE>
</FORM><HR>
<A HREF="/testmvc/ChoosingNumberOfResults.jsp">Another Search</A>
</BODY>
</HTML>
<%@ page import="com.startups.testmvc.DisplayingResultsModel" %>The next part creates the dynamically generated "Previous" navigation link. It checks if there is a Previous link, and if there is it generates the HREF. If there isn't a previous link (e.g. it's the first page), it just prints out "Previous" without making it an HREF.
<jsp:useBean id="displayingResultsModel" class="com.startups.testmvc.DisplayingResultsModel" scope="session" create="yes"/><HTML>
<HEAD>
<TITLE> Multipage test </TITLE>
</HEAD>
<BODY>
<H1>Multipage test results</H1>
<%Notice the HREF (in red). Note that we are referring to the controller servlet, even though this isn't a form. The HREF issues an HTTP GET request rather than an HTTP POST request. For GET requests, we don't have form elements to hold our parameters, so we must pass them in using a parameter string (the part that begins with a ? character). The parameter "startIndex" will get passed to the controller servlet as if it were entered in a form. The actual value of the parameter is the current model value of the startingIndex minus 10, since there are 10 results per page.
// Generate Previous | Next links, if necessary
int theNumberOfResults = displayingResultsModel.getNumberOfResults();
int startingIndex = displayingResultsModel.getStartingIndex();
if (startingIndex != 0) {
%>
<A HREF="/servlet/com.startups.testmvc.DisplayingResultsServlet?startIndex=<%= startingIndex - 10 %>">
Previous </A>
<%
} else {
%>
Previous
<%
}
The next part is similar and generates the "Next" link:
int endingIndex = displayingResultsModel.getEndingIndex();The next part generates the numbered results page navigation links (e.g. 1-10 | 11-20 | 21-28). The current page is not hyperlinked and is displayed in bold:
if (endingIndex < theNumberOfResults) {
%>
| <A HREF="/servlet/com.startups.testmvc.DisplayingResultsServlet?startIndex=<%= startingIndex + 10 %>">
Next </A>
<%
} else {
%>
| Next
<%
}
// Generate numbered links 1 - 10 | 11 - 20 | etc.The last part of the jsp simply loops through the currently displayed indices and prints out up to 10 results on the page:
for (int i = 0;
i < theNumberOfResults;
i += 10) {
int theEndIndex = i + 10 < theNumberOfResults ? i + 10 : theNumberOfResults;
if (i != startingIndex) {
%>
|
<A HREF="/servlet/com.startups.testmvc.DisplayingResultsServlet?startIndex=<%= i %>">
<%= i + 1 %> - <%= theEndIndex %>
</A>
<%
} else {
%>
| <B>
<%= i + 1 %> - <%= theEndIndex %> </B>
<%
}
}%>
<P><TABLE>
<%
for (int i = displayingResultsModel.getStartingIndex();
i < displayingResultsModel.getEndingIndex();
i++) {
%>
<TR>
<TD> Result number: <%= i + 1 %> </TD>
<TD> <%= displayingResultsModel.getResultAt(i) %> </TD>
</TR>
<%
}
%>
</TABLE>
</FORM><HR>
<A HREF="/testmvc/ChoosingNumberOfResults.jsp">Another Search</A>
</BODY>
</HTML>
/*The servlet is quite similar to the servlet in the previous example. It updates the model, checks for validity, and forwards the request to either an error page or the results page.
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*/package com.startups.testmvc;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.ServletException;
import java.io.IOException;
import com.startups.webui.ControllerServlet;/**
*
**/
public class ChoosingNumberOfResultsServlet extends ControllerServlet {
/**
* Handle an Http post. The request is expected to
* contain the parameter "DesiredNumberOfResults" from the form
* that points to this servlet.
**/
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
updateModel(request);
if (getModel(request).isValid()) {
createDisplayingResultsModel(request, response);
enterState("/testmvc/DisplayingResults.jsp", request, response);
} else {
enterState("/testmvc/ChoosingNumberOfResultsError.jsp", request, response);
}
}/**
* Retrieve the model for this state from the session
* and update it using values retrieved from the request.
* If the model doesn't already exist, create it.
**/
private void updateModel(HttpServletRequest request) {
HttpSession theSession = request.getSession();
ChoosingNumberOfResultsModel theModel = getModel(request);
if (theModel == null) {
theModel = new ChoosingNumberOfResultsModel();
theSession.putValue("choosingNumberOfResultsModel", theModel);
}
//
// Set model properties with values from form
//
theModel.setDesiredNumberOfResults(
request.getParameter("DesiredNumberOfResults"));
}/**
* Creates the model used for the next state and adds it to
* the session.
**/
private void createDisplayingResultsModel(HttpServletRequest request,
HttpServletResponse response) {
DisplayingResultsModel theModel = new DisplayingResultsModel();
theModel.setResultsNumber(Integer.parseInt(
getModel(request).getDesiredNumberOfResults()));
HttpSession theSession = request.getSession();
theSession.putValue("displayingResultsModel", theModel);
}/**
* Return the model for this state given a request.
**/
private ChoosingNumberOfResultsModel getModel(HttpServletRequest request) {
HttpSession theSession = request.getSession();
return (ChoosingNumberOfResultsModel)
theSession.getValue("choosingNumberOfResultsModel");
}/**
* See doPost().
**/
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
}
/*The first thing to notice is that the servlet extends SessionValidControllerServlet instead of ControllerServlet:
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*/package com.startups.testmvc;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.ServletException;
import java.io.IOException;
import com.startups.webui.SessionValidControllerServlet;/**
* Controller for the DisplayingResults state.
**/
public class DisplayingResultsServlet extends SessionValidControllerServlet {
/**
* Handle an Http get. The request may contain the parameter
* "startIndex" from the jsp that points to this servlet,
* where the value of "startIndex" should be a non-negative
* integer.
**/
protected void performGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
updateModel(request);
if (getModel(request).isValid()) {
enterState("/testmvc/DisplayingResults.jsp", request, response);
} else {
enterState("/testmvc/DisplayingResultsError.jsp", request, response);
}
}/**
* Retrieve the model for this state from the session
* and update it using values retrieved from the request.
* If the model doesn't already exist, create it.
**/
private void updateModel(HttpServletRequest request) {
HttpSession theSession = request.getSession();
DisplayingResultsModel theModel = getModel(request);
if (theModel == null) {
theModel = new DisplayingResultsModel();
theSession.putValue("displayingResultsModel", theModel);
}
int theStartIndex = Integer.parseInt(
request.getParameter("startIndex"));theModel.setStartingIndex(theStartIndex);
}
/**
* Return the model for this state given a request.
**/
private DisplayingResultsModel getModel(HttpServletRequest request) {
HttpSession theSession = request.getSession();
return (DisplayingResultsModel)
theSession.getValue("displayingResultsModel");
}/**
* See doGet().
**/
protected void performPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
performGet(request, response);
}
}
/*SessionValidControllerServlet extends ControllerServlet and adds session expiration checking--it checks that the session has not expired before letting subclasses do their work (see the javadoc for SessionValidControllerServlet for more info). The reason why we need this is because the DisplayingResults state spans multiple pages. Since a user can leave his or her browser after seeing any page, the session may expire. Since the session contains the model, subsequent requests will fail with a nasty error. SessionValidControllerServlet checks for this condition and redirects the user to an error page if the session has expired. The use of SessionValidControllerServlet leads to another difference from our previous example in the next few lines of code:
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*/package com.startups.testmvc;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.ServletException;
import java.io.IOException;
import com.startups.webui.SessionValidControllerServlet;/**
* Controller for the DisplayingResults state.
**/
public class DisplayingResultsServlet extends SessionValidControllerServlet {
/**Note that we now no longer define doGet(), but rather a method called performGet(). SessionValidControllerServlet defines doGet() to check for session expiration, then calls an abstract method called performGet(), which can be interepreted as "do whatever you would do if the session is found to be valid." In other respects, the method is similar to our previous example, with one subtle point. Note that there is an error page associated with DisplayingResults. At first glance, it appears as though there is no way to get the model into an invalid state, since the model variables seem to all be set programmatically. However, the reason this state exists is because when we hit the "next", "previous", or "1-10" links, we are issuing HTTP GET requests instead of POST requests. HTTP GET requests pass parameters via a URL query string (recall in our DisplayingResults.jsp page we used this property to pass the startIndex parameter), so the urls are visible in the browser url field. This means someone could simply type in a URL that looks like "http://ted.startups.com:8082/servlet/com.startups.testmvc.DisplayingResultsServlet?startIndex=-1" and pass in an invalid startIndex. The validation we perform will catch this.
* Handle an Http get. The request may contain the parameter
* "startIndex" from the jsp that points to this servlet,
* where the value of "startIndex" should be a non-negative
* integer.
**/
protected void performGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
updateModel(request);
if (getModel(request).isValid()) {
enterState("/testmvc/DisplayingResults.jsp", request, response);
} else {
enterState("/testmvc/DisplayingResultsError.jsp", request, response);
}
}
DisplayingResultsError.jsp doesn't need to highlight any form input, since there was no form involved. It doesn't have to be as polished because the only way to reach it is if a user does some clearly unexpected behavior such as editing the url field in the browser:
<HTML>That's it, we're done! The working pages look like this:
<HEAD>
<TITLE> Internal Error </TITLE>
</HEAD>
<BODY>
<H1>Internal Error</H1>Sorry, an internal error has occurred.
<HR>
<A HREF="/testmvc/ChoosingNumberOfResults.jsp">Another Search</A>
</BODY>
</HTML>