"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 EnteringUserName state has a state transition that leads back to the same state--if the username isn't valid, it sends you back into the same state (with appropriate error messages). 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 using Model properties. |
Controller | Java servlet named <StateName>Servlet.java | Handle control flow between pages. Updates the Model with form input. |
/*
* 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.Model;
import com.startups.validation.ValidationFailedException;
import com.vivid.startups.users.User;
import com.vivid.exception.BaseException;
import com.vivid.ServiceFactory;
import com.vivid.dataaccess.DBAccess;/**
* The model object for the EnteringUserName state.
* It is currently a facade for a User object.
**/
public class EnteringUserNameModel extends Model {
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 {
DBAccess db = ServiceFactory.getInstance().getDBAccess();
getUser().load(db);
} 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 Model object:
public class EnteringUserNameModel extends Model {
Model is the superclass of all model objects and provides two types of functionality to its subclasses. First, it provides a validation framework. Second, it provides a few convenience methods for use from within JSP views (please see the javadoc for Model for more information). 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 our model object extends Model,
which in turn 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 {
DBAccess db = ServiceFactory.getInstance().getDBAccess();
getUser().load(db);
} 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 from persistent storage. 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:
/*
* 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.Model;
import com.vivid.startups.users.User;/**
* The model for the Success state, where a user has been
* found to exist.
**/
public class SuccessModel extends Model {
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>
<%= enteringUserNameModel.displayFailedValidations() %><FORM ACTION="/servlet/com.startups.testmvc.EnteringUserNameServlet" METHOD="POST">
<%= enteringUserNameModel.fontColorTagFor("UserID") %>
<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.testmvc.EnteringUserNameModel" %>The next part contains an addition for validation purposes. This jsp will double as an error page. In other words, it's possible that the model could fail validation and the user could get sent back to this page. In these cases, the page will display a list of failed validations at the top of the page in red. The code to do this is defined in the Model object, which EnteringUserNameModel extends, and all that is needed in the jsp is a simple jsp eval:
<jsp:useBean id="enteringUserNameModel" class="com.startups.testmvc.EnteringUserNameModel" scope="session" create="yes"/>
<HTML>For more information on displayFailedValidations(), see the javadoc for Model. 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.
<HEAD>
<TITLE> Test MVC Page </TITLE>
</HEAD>
<BODY><H1> Test MVC Model </H1>
<%= enteringUserNameModel.displayFailedValidations() %>
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".
<FORM ACTION="/servlet/com.startups.testmvc.EnteringUserNameServlet" METHOD="POST">
<%= enteringUserNameModel.fontColorTagFor("UserID") %>The first jsp eval produces an HTML font tag for the "UserID" field using the method fontColorTagFor() defined on Model. This tag will evaluate to either black or red color, depending on whether the UserID field has passed validation or not. The jsp eval of the value parameter simply fills in the form with the userID of the user in the model. 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:
<B>Username:</B>
</FONT>
<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.
/*The servlet extends ControllerServlet, which provides a number of useful methods and some validation logic common to all controller servlets. The main concept to understand about ControllerServlet is that it defines doPost() to do some framework bookkeeping and then calls performPost(), which as a subclass author is your responsibility to implement. A similar concept exists for doGet().
* 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.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/EnteringUserName.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 performPost(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/EnteringUserName.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) {
EnteringUserNameModel theModel = (EnteringUserNameModel)
getModel(request);
if (theModel == null) {
theModel = new EnteringUserNameModel();
addModelToSession(theModel, request);
}//
// Update the model object using parameters gathered
// from the form.
//
theModel.setUserID(request.getParameter("UserID"));
}/**
* Creates the model used for the next state and adds it to
* the session. The reason we do this here instead of
* in the usebean declaration in the jsp is that this
* model takes a parameter in its constructor.
**/
private void createSuccessModel(HttpServletRequest request,
HttpServletResponse response) {
SuccessModel theModel = new SuccessModel(
((EnteringUserNameModel) getModel(request)).getUser());
addModelToSession(theModel, request);
}
/**
* See performPost().
**/
protected void performGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
performPost(request, response);
}
}
/**The meat of the servlet is in the performPost() 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/EnteringUserName.jsp".
*
* @see com.startups.testmvc.EnteringUserNameModel
**/
public class EnteringUserNameServlet extends ControllerServlet {
/**The first thing performPost() 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 using the enterState() method inherited from ControllerServlet. Otherwise, the servlet forwards the request back to the EnteringUserName state to show the validation errors. 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 performPost(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/EnteringUserName.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 and adds it to the session using the addModelToSession() method inherited from ControllerServlet. 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) {
EnteringUserNameModel theModel = (EnteringUserNameModel)
getModel(request);
if (theModel == null) {
theModel = new EnteringUserNameModel();
addModelToSession(theModel, request);
}//
// Update the model object using parameters gathered
// from the form.
//
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. The reason we do this here instead of
* in the usebean declaration in the jsp is that this
* model takes a parameter in its constructor.
**/
private void createSuccessModel(HttpServletRequest request,
HttpServletResponse response) {
SuccessModel theModel = new SuccessModel(
((EnteringUserNameModel) getModel(request)).getUser());
addModelToSession(theModel, request);
}
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 tag id | <stateName>Model | enteringUserNameModel |
Model property accessors | get<Property>, is<Property> | getUserID() |
Form variable names | <Property> | UserID |
Validate property method | validate<Property> | validateUserID() |
<!-- usebean tag for previously defined model -->If you ever need to access a previously created model from within a ControllerServlet, use the getModel() method that takes a class object as a parameter:
<jsp:useBean id="enteringUserNameModel"
class="com.startups.testmvc.EnteringUserNameModel"
scope="session" /><!-- usebean tag for model corresponding to current state -->
<jsp:useBean id="thankYouModel"
class="com.startups.testmvc.ThankYouModel"
scope="session"
create="yes"/>
// some code in ThankYouServletIf you ever use this, you need to take special care that the session doesn't expire between calls to the servlets. In order to handle this, see the next question.
EnteringUserNameModel thePreviouslyCreatedModel = (EnteringUserNameModel)
getModel(EnteringUserNameModel.class, request);
Q: Do I need to do anything special if I have a multipage form?
A: The framework stores Model objects in the user's http
session. In a multipage form, there is no guarantee that the user
will move between pages in a timely fashion, so it's possible that the
user's session may expire between pages. If this were to happen,
Model objects created in earlier pages will not be available. In
order to handle this, you may have your controller servlet extend SessionValidControllerServlet
instead of ControllerServlet. SessionValidControllerServlet
checks that the session is valid before attempting the performPost()
or performGet() methods. If the session is found to be invalid,
which would happen if the session timed out, the SessionValidControllerServlet
forwards the user to a session expired error page.
Q: I am not using a form, can I still use the MVC method?
A: Yes. In the above examples you had a form with
the post action set to the servlet (e.g. <form action="/servlet/com.startups.testmvc.EnteringUserNameServlet"
method="POST">). You can just as easily put this into an href
(e.g. <a href="/servlet/com.startups.testmvc.EnteringUserNameServlet">).
In fact, it's preferred that you go between states through a controller
servlet, rather than just moving from jsp to jsp.
Q: Do I need to do anything special if I am going to read or
write to persistent storage?
A: Yes. The Model class has a method defined on
it called getSession(), which returns a TOPLink Session object
(not to be confused with an HTTP session). This method will only
return a valid session if the model is used in conjunction with a ControllerDataServlet,
which contains the logic to assign the proper TOPLink session to the Model.
So for states requiring either reads from persistent storage or writes
to persistent storage, have your controller servlet extend ControllerDataServlet.
More detail on persistence can be found in another cookbook (not yet completed).
Q: I have a bunch of business and persistence logic.
Where do I put it, in the Model or in the Servlet?
A: Put it in the Model, or have the Model delegate to
some other object. The only logic that should be in the Servlet is
deciding which state to go to next depending on the state of the model,
and updating the model objects. Updating the model objects typically
involves pulling data out of forms and using setters on the model, but
may also involve higher level operations. For example, if you had
some method which saves data to persistent storage, it might need to be
invoked by the servlet prior to transitioning to a TransactionConfirmed
state. In this example, the model might have a saveData()
method on it which is invoked by the servlet as part of the model update
step.
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.