"We want a page with a form where we can enter the name of an existing user in a textfield and a legal structure from a pulldown. We will retrieve the legal structure pulldown entries from the database (we can't just put them into html). It should look something like this."
"Also, if the username is invalid and the user submits, it should come back to this page and highlight Username in red, along with a message that says what the problem was."
"When the username is really valid, hitting submit should take them to a success page which shows the userID and the email address of that user."
The first thing to do is to name all the pages. Try to pick
something which is descriptive of the user task, or the state that the
user is in when they are looking at the page. For example, you might
pick:
The balloons indicate states, and the arrows indicate transitions between states. Note that the EnteringUserNameError state has a state transition that leads back to the same state--if the username isn't valid, it's still an error. Please make sure that everyone (you, the Java developer, the spec writer) agree on the state model!
Arrows indicate dependencies. The reason why we break up each
state into three components is that each component has a very specific
responsibility, which makes for easier maintenance. In addition,
there is a fair amount of separation between the presentation component
and the logic, which allows a Web developer to be less dependent on the
Java developer if he or she wants to make purely visual changes.
In our situation, the View is owned by the web developer, while the model
and controller components are owned by the Java developer. For more
information, see the references in the design
discussion section.
Concept | Implementation | Responsibility |
Model | Java object named <StateName>Model.java | Business logic. Includes dynamic state, validation. May make use of data access and persistent objects. |
View | JSP page named <StateName>.jsp | Present dynamic state of application to end user. |
Controller | Java servlet named <StateName>Servlet.java | Handle control flow between pages. |
/*
* 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;
import com.vivid.startups.users.User;
import com.vivid.exception.BaseException;/**
* The model object for the EnteringUserName state.
* It is currently a facade for a User object.
**/
public class EnteringUserNameModel extends ValidatableObject {
private static final String[] LEGAL_STRUCTURES = new String[] {
"C Corporation",
"S Corporation",
"LLC",
"Partnership",
"Sole Proprietorship",
"No Formal Entity",
"Other"};private User user = new User();
//
// Accessors
//
public void setUserID(String userID) {
this.user.setUserID(userID);
}public User getUser() {
return this.user;
}public String getUserID() {
return this.user.getUserID();
}public String[] getLegalStructures() {
return LEGAL_STRUCTURES;
}//
// Validation
//
public void validateUserID() throws ValidationFailedException {
if (getUserID() == null || getUserID().trim().equals("")
|| getUserID().indexOf("'") != -1) {
throw new ValidationFailedException("Invalid user name");
}
try {
getUser().load();
} catch (BaseException e) {
throw new ValidationFailedException("User does not exist");
}
}}
Let's go through this listing piece by piece. The model is
declared to extend a ValidatableObject:
public class EnteringUserNameModel extends ValidatableObject {
ValidatableObject is a class which contains some useful methods to help in validation, so by extending ValidatableObject we get some functionality that we will need later. Since we know the model contains some user information, it makes sense to have an instance variable representing the current user, hence the User object instance variable declaration:
private static final String[] LEGAL_STRUCTURES = new String[] {
"C Corporation",
"S Corporation",
"LLC",
"Partnership",
"Sole Proprietorship",
"No Formal Entity",
"Other"};private User user = new User();
Normally, there would be an actual instance variable for the legal
structures, which would probably be read out of a database somewhere.
For the purposes of this toy example, we'll just pretend that these values
came out of the database and just declare them as a static String
array to fake it.
The next bit of code contains method definitions to allow the view or controller components to inspect or modify the state of the model:
//
// Accessors
//
public void setUserID(String userID) {
this.user.setUserID(userID);
}public User getUser() {
return this.user;
}public String getUserID() {
return this.user.getUserID();
}public String[] getLegalStructures() {
return LEGAL_STRUCTURES;
}
Finally, the last bit of code contains any validations that should
be done on the model. Recalling our state diagram, we note that in
order to make certain state transitions we need to have a valid user.
Because the model object extends ValidatableObject, it inherits
the isValid()
method. This method invokes all methods on the object with a method
signature void validate<something>() throws ValidationFailedException,
and saves the validation errors for later retrieval (see the javadoc
for ValidatableObject for more info).
//
// Validation
//
public void validateUserID() throws ValidationFailedException {
if (getUserID() == null || getUserID().trim().equals("")
|| getUserID().indexOf("'") != -1) {
throw new ValidationFailedException("Invalid user name");
}
try {
getUser().load();
} catch (BaseException e) {
throw new ValidationFailedException("User does not exist");
}
}
The method defined above validates that the userID is legal, and
tries to load up the user. If for some reason these fail, it throws
a ValidationFailedException.
We can define as many validation methods as we need, as long as they have
the proper method signature. It's actually a good idea at this point
to write some test code to ensure that the validations do the right thing.
Since your model is view-independent, there's no reason why you can't write
test code that doesn't require a web server or servlet engine. For
example, you might write the following automated test suite using JUnit.
/*
* 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 junit.framework.*;
/**
* Unit test for EnteringUserNameModel class.
**/
public class EnteringUserNameModelTest extends TestCase {
private EnteringUserNameModel model;//
// Constructors
//
public EnteringUserNameModelTest(String name) {
super(name);
}//
// Test Fixtures
//
protected void setUp() {
this.model = new EnteringUserNameModel();
}protected void tearDown() {
this.model = null;
}//
// Tests
//
public void testNullUserID() {
this.model.setUserID(null);
assert(this.model.isValid() == false);
}public void testBlankSpaceUserID() {
this.model.setUserID(" ");
assert(this.model.isValid() == false);
}public void testUserIDWithQuotes() {
this.model.setUserID("Harry''Potter");
assert(this.model.isValid() == false);
}// etc.
//
// Suite
//
public static Test suite() {
return new TestSuite(EnteringUserNameModelTest.class);
}
}
There is another state which contains a model. It's the SuccessModel
corresponding to the Success state, where the user's ID and email
address are displayed:
/*This model object doesn't extend ValidatableObject, because no validations are performed in the Success state.
* 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;
import com.vivid.startups.users.User;
import com.vivid.exception.BaseException;/**
* The model for the Success state, where a user has been
* found to exist.
**/
public class SuccessModel {
private User user;//
// Constructors
//
public SuccessModel(User user) {
setUser(user);
}//
// Accessors
//
public void setUser(User user) {
this.user = user;
}public String getUserID() {
return this.user.getUserID();
}public String getEmailAddress() {
return this.user.getEmailAddress();
}
}
<HTML>And Success.jsp might look like:
<HEAD>
<TITLE> Test MVC Page </TITLE>
</HEAD>
<BODY><H1> Test MVC Model </H1>
<FORM ACTION="#" METHOD="POST">
<B>Username:</B>
<INPUT TYPE="TEXT"
NAME="#"
VALUE="#"
SIZE="20"
MAXLENGTH="40"><P>
<B>Legal Structure</B>
<SELECT NAME="#">
<OPTION VALUE="#">#
</SELECT><INPUT TYPE="SUBMIT">
</FORM>
</BODY>
</HTML>
<HTML>
<HEAD>
<TITLE> Test MVC Page Success</TITLE>
</HEAD>
<BODY><H1> Success! </H1>
<B>UserID:</B> # <BR>
<B>Email Address:</B> # <BR></BODY>
</HTML>
<%--The first significant addition is a usebean tag that indicates the model that this jsp depends on to get its dynamic data. Also note that the hash marks have been replaced by jsp evals calling methods defined on the SuccessModel. Since the success page does not itself include a form, there isn't any reference to any servlet code. The EnteringUserName.jsp additions are a bit more complex. We'll first list the changed file, then discuss each change piece by piece:
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*
--%>
<jsp:useBean id="successModel" class="com.startups.testmvc.SuccessModel" scope="session" /><HTML>
<HEAD>
<TITLE> Test MVC Page Success</TITLE>
</HEAD>
<BODY><H1> Success! </H1>
<B>UserID:</B> <%= successModel.getUserID() %> <BR>
<B>Email Address:</B> <%= successModel.getEmailAddress() %> <BR></BODY>
</HTML>
EnteringUserName.jsp:
<%--Let's go through the jsp piece by piece. The first part of the scripting code simply declares that the jsp depends on the EnteringUserNameModel:
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*
--%>
<%@ page import="com.startups.testmvc.EnteringUserNameModel" %>
<jsp:useBean id="enteringUserNameModel" class="com.startups.testmvc.EnteringUserNameModel" scope="session" create="yes"/><HTML>
<HEAD>
<TITLE> Test MVC Page </TITLE>
</HEAD>
<BODY><H1> Test MVC Model </H1>
<FORM ACTION="/servlet/com.startups.testmvc.EnteringUserNameServlet" METHOD="POST"><B>Username:</B>
<INPUT TYPE="TEXT"
NAME="UserID"
VALUE="<%= enteringUserNameModel.getUserID() %>"
SIZE="20"
MAXLENGTH="40"><P>
<B>Legal Structure</B>
<SELECT NAME="LegalTypes">
<%
String[] theLegalTypes = enteringUserNameModel.getLegalStructures();
for (int i = 0; i < theLegalTypes.length; i++) {
%>
<OPTION VALUE="<%= theLegalTypes[i] %>"><%= theLegalTypes[i] %>
<%
}
%>
</SELECT>
<INPUT TYPE="SUBMIT">
</FORM>
</BODY>
</HTML>
<%@ page import="com.startups.testmvc.EnteringUserNameModel" %>The next part contains the form tag, which must point to a controller servlet. Controller servlets should be named according to the convention <state name>Servlet, so you always know the name of the servlet corresponding to a given state. Since you know what package your servlet is in, and you know the name of the servlet corresponding to this state, you can easily fill this in.
<jsp:useBean id="enteringUserNameModel" class="com.startups.testmvc.EnteringUserNameModel" scope="session" create="yes"/>
<HTML>The next part of the jsp is a form element. Since you're building the servlet to process the form, you get to name the individual form elements. Pick names corresponding to the method names on your model. Since you have a method called getUserID() on the model object that corresponds to the value of the input field, you should name the form variable "UserID".
<HEAD>
<TITLE> Test MVC Page </TITLE>
</HEAD>
<BODY><H1> Test MVC Model </H1>
<FORM ACTION="/servlet/com.startups.testmvc.EnteringUserNameServlet" METHOD="POST"><B>Username:</B>
<B>Username:</B>The jsp eval simply fills in the form with the userID of the user in the model. In most cases this will be blank. Also make sure that the maxlength attributes of text fields make sense. Most likely there are database limits that should not be exceeded! Since you are in a better position to know what these limits are than the web developer, it is your responsibility to ensure that the field entries don't overflow the database columns. The next bit of the jsp shows what you need to do to create an pulldown with values obtained from the model:
<INPUT TYPE="TEXT"
NAME="UserID"
VALUE="<%= enteringUserNameModel.getUserID() %>"
SIZE="20"
MAXLENGTH="40"><P>
<B>Legal Structure</B>In this situation, you need to write some scriptlet code to pull the legal types out of the model and generate a bunch of option values. This completes your modification of EnteringUserName.jsp.
<SELECT NAME="LegalTypes">
<%
String[] theLegalTypes = enteringUserNameModel.getLegalStructures();
for (int i = 0; i < theLegalTypes.length; i++) {
%>
<OPTION VALUE="<%= theLegalTypes[i] %>"><%= theLegalTypes[i] %>
<%
}
%>
</SELECT>
You are free to make changes to these jsp files. Just remember to commit your changes to CVS, and the web developer will get them. It's a good idea to inform your web developer when you make changes, so he or she will remember to do cvs updates. Likewise, you should expect the web developer to occassionally modify the jsp file's HTML layout. You'll get these changes via cvs updates.
<%--Let's go through this code step by step. The first difference between the error page and the non-error page is the inclusion of a method definition:
* Copyright 2000 Startups.com. All Rights Reserved.
*
* This software is the proprietary information of Startups.com.
* Use is subject to license terms.
*
--%>
<%@ page import="com.startups.validation.ValidatableObject" %>
<%@ page import="com.startups.validation.ValidationFailedException" %>
<%@ page import="com.startups.testmvc.EnteringUserNameModel" %>
<%!
//
// Returns <FONT COLOR="red"> if the given fieldName in the given
// model is invalid, otherwise returns <FONT COLOR="black">.
// Since this method is pretty generic, it could be included
// as a separate jsp include in many jsp views.
//
public String fontColorTagFor(String fieldName, ValidatableObject model) {
if (model.isFieldValid(fieldName)) {
return "<FONT COLOR=\"black\">";
} else {
return "<FONT COLOR=\"red\">";
}
}
%>
<jsp:useBean id="enteringUserNameModel" class="com.startups.testmvc.EnteringUserNameModel" scope=
"session" create="yes"/><HTML>
<HEAD>
<TITLE> Test MVC Page </TITLE>
</HEAD>
<BODY><H1> Test MVC Model </H1>
<%
if (! enteringUserNameModel.isValid()) {
//
// print out the fields that need to be changed
//
%>
<FONT COLOR="red">
Please review the indicated fields:<BR>
<%
ValidationFailedException[] theExceptions =
enteringUserNameModel.getValidationErrorList();
for (int i = 0; i < theExceptions.length; i++) {
%>
<%= theExceptions[i].getMessage() %> <br>
<%
}
%>
</FONT>
<%
}
%>
<FORM ACTION="/servlet/com.startups.testmvc.EnteringUserNameServlet" METHOD="POST">
<%= fontColorTagFor("UserID", enteringUserNameModel) %>
<B>Username:</B>
</FONT>
<INPUT TYPE="TEXT"
NAME="UserID"
VALUE="<%= enteringUserNameModel.getUserID() %>"
SIZE="20"
MAXLENGTH="40"><P>
<B>Legal Structure</B>
<SELECT NAME="LegalTypes">
<%
String[] theLegalTypes = enteringUserNameModel.getLegalStructures();
for (int i = 0; i < theLegalTypes.length; i++) {
%>
<OPTION VALUE="<%= theLegalTypes[i] %>"><%= theLegalTypes[i] %>
<%
}
%>
</SELECT><INPUT TYPE="SUBMIT">
</FORM>
</BODY>
</HTML>
<%@ page import="com.startups.validation.ValidatableObject" %>This definition saves us the trouble of putting in lots of scriptlets later. It is used later to color invalid fields red in the form. One thing to notice is that the method makes use of a method called isFieldValid() on EnteringUserNameModel. This is one of the methods that is inherited from ValidatableObject. The next piece of EnteringUserNameError.jsp contains a scriptlet that implements the "please review the indicated fields" functionality:
<%@ page import="com.startups.validation.ValidationFailedException" %>
<%@ page import="com.startups.testmvc.EnteringUserNameModel" %>
<%!
//
// Returns <FONT COLOR="red"> if the given fieldName in the given
// model is invalid, otherwise returns <FONT COLOR="black">
//
public String fontColorTagFor(String fieldName, EnteringUserNameModel model) {
if (model.isFieldValid(fieldName)) {
return "<FONT COLOR=\"black\">";
} else {
return "<FONT COLOR=\"red\">";
}
}
%>
<HTML>Again, the scriptlet takes advantage of a method inherited from ValidatableObject. This method is called getValidationErrorList(), and returns an array of all the failed validations. The scriptlet simply prints them out one by one.
<HEAD>
<TITLE> Test MVC Page </TITLE>
</HEAD>
<BODY><H1> Test MVC Model </H1>
<%
if (! enteringUserNameModel.isValid()) {
//
// print out the fields that need to be changed
//
%>
<FONT COLOR="red">
Please review the indicated fields:<BR>
<%
ValidationFailedException[] theExceptions =
enteringUserNameModel.getValidationErrorList();
for (int i = 0; i < theExceptions.length; i++) {
%>
<%= theExceptions[i].getMessage() %> <br>
<%
}
%>
</FONT>
<%
}
%>
<FORM ACTION="/servlet/com.startups.testmvc.EnteringUserNameServlet" METHOD="POST">
You'll have to do a cvs add and commit on this page, since you created it.
/*The servlet extends ControllerServlet, which provides the useful method enterState() which is used later:
* 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.webui.ControllerServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import java.io.IOException;/**
* The controller for two views: EnteringUserName.jsp and
* EnteringUserNameError.jsp. It may forward the request
* to either "/testmvc/Success.jsp" or "/testmvc/EnteringUserNameError.jsp".
*
* @see com.startups.testmvc.EnteringUserNameModel
**/
public class EnteringUserNameServlet extends ControllerServlet {/**
* Handle an Http post. The request is expected to
* contain the parameter "UserID" from the form
* that points to this servlet.
**/
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
updateModel(request);
if (getModel(request).isValid()) {
createSuccessModel(request, response);
enterState("/testmvc/Success.jsp", request, response);
} else {
enterState("/testmvc/EnteringUserNameError.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();
EnteringUserNameModel theModel = getModel(request);
if (theModel == null) {
theModel = new EnteringUserNameModel();
theSession.putValue("enteringUserNameModel", theModel);
}
theModel.setUserID(request.getParameter("UserID"));
}/**
* Creates the model used for the next state and adds it to
* the session.
**/
private void createSuccessModel(HttpServletRequest request,
HttpServletResponse response) {
SuccessModel theModel = new SuccessModel(getModel(request).getUser());
HttpSession theSession = request.getSession();
theSession.putValue("successModel", theModel);
}/**
* Return the model for this state given a request.
**/
private EnteringUserNameModel getModel(HttpServletRequest request) {
HttpSession theSession = request.getSession();
return (EnteringUserNameModel)
theSession.getValue("enteringUserNameModel");
}/**
* See doPost().
**/
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
}
/**The meat of the servlet is in the doPost() method. This method contains the actual conditional logic where the servlet performs its controller duties by forwarding the user the appropriate page depending on the state of the model:
* The controller for two views: EnteringUserName.jsp and
* EnteringUserNameError.jsp. It may forward the request
* to either "/testmvc/Success.jsp" or "/testmvc/EnteringUserNameError.jsp".
*
* @see com.startups.testmvc.EnteringUserNameModel
**/
public class EnteringUserNameServlet extends ControllerServlet {
/**The first thing doPost() does is to update the model based on request parameters. This usually means pulling data out of forms and using setter methods on the model object. Now, based on the state of the model, the servlet decides which state to transition into. If the model is valid, the servlet creates the SuccessModel and forwards the request to the Success state. Otherwise, the servlet forwards the request to the EnteringUserNameError state. It is important that the argument to enterState() be an absolute path from the web root (it must start with a slash). The actual path should be obvious, since when you obtained the jsp files from CVS they will be in a particular directory.
* Handle an Http post. The request is expected to
* contain the parameter "UserID" from the form
* that points to this servlet.
**/
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
updateModel(request);
if (getModel(request).isValid()) {
createSuccessModel(request, response);
enterState("/testmvc/Success.jsp", request, response);
} else {
enterState("/testmvc/EnteringUserNameError.jsp", request, response);
}
}
The rest of the servlet just handles the details of model creation and updating based on parameters gathered from the HTTP requests.
/**The updateModel() method checks to see if the session already contains a model object. If not, it creates a new one. The actual updating of the model occurs in the last line of the method--this is the most important part of the method, since it actually changes the model object! The modification to the model is that the userID is updated based on whatever the end user typed into the form. The parameter name "UserID" is the name you put into the jsp form variable when you were replacing # marks.
* 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();
EnteringUserNameModel theModel = getModel(request);
if (theModel == null) {
theModel = new EnteringUserNameModel();
theSession.putValue("enteringUserNameModel", theModel);
}
theModel.setUserID(request.getParameter("UserID"));
}
/**The createSuccessModel() method just instantiates a new SuccessModel and puts it into the user's session, so it can be used later by Success.jsp.
* Creates the model used for the next state and adds it to
* the session.
**/
private void createSuccessModel(HttpServletRequest request,
HttpServletResponse response) {
SuccessModel theModel = new SuccessModel(getModel(request).getUser());
HttpSession theSession = request.getSession();
theSession.putValue("successModel", theModel);
}
/**The getModel() method is just a convenience method that handles the details of getting the current model out of the session.
* Return the model for this state given a request.
**/
private EnteringUserNameModel getModel(HttpServletRequest request) {
HttpSession theSession = request.getSession();
return (EnteringUserNameModel)
theSession.getValue("enteringUserNameModel");
}
After you've built, tested, and deployed all of your components and you've gotten a signoff from your site producer, you're done! Hopefully these steps have allowed you to concentrate on and own the part that you do best--the back end logic--while allowing the web developer to do what he or she does best without a lot of back and forth.
Item to be named | Name to Use | Example |
Model | <StateName>Model.java | EnteringUserNameModel.java |
View | <StateName>.jsp | EnteringUserName.jsp |
Controller | <StateName>Servlet.java | EnteringUserNameServlet.jsp |
JSP bean id | <stateName>Model | enteringUserNameModel |
Session key for model | <stateName>Model | enteringUserNameModel |
Model property accessors | get<Property>, is<Property> | getUserID() |
Form variable names | <Property> | UserID |
Validate property method | validate<Property> | validateUserID() |
A single state is represented by Model-View-Controller components, using
the following naming convention: <state>.jsp, <state>Servlet,
and <state>Model.
Goals:
The main consequence of using the MVC system is that there will be more files than if we didn't use MVC. For every state, there are at least three files that are involved. However, each file should be much simpler and easier to understand than if these things were in fewer files, so it seems to be an acceptable tradeoff.
if (! request.isRequestedSessionIdValid()) { enterState("login.jsp", request, response); }