The first step when creating a new Web application is the construction of the application skeleton. In the
bigrez.com
Web application, this skeleton consists of the minimum
The skeleton Web applications for both sites consist of the following components:
The Master.jsp template page for the site, defining the overall page layout and included components
Supporting view components such as Header.jsp , TopNav.jsp , Footer.jsp , and placeholder versions of view components such as RezInfo.jsp and Offers.jsp
The PageDisplayServlet , used in page assembly
The web.xml and weblogic.xml descriptor files, including JSP configuration elements and the servlet mappings from *.do to the Struts ActionServlet and *.page to the PageDisplayServlet
A placeholder struts-config.xml file with sufficient configuration information, to start the ActionServlet on Web application boot
Placeholder Home.jsp page, to use as a test page to validate skeleton configuration
These skeleton components, once deployed to the appropriate Web applications in the domain, are sufficient to present the
Home.jsp
page in the display area of the layout when the
Home.page
URL is accessed from a browser. On a typical development project, the skeleton components are then placed in source code control to form the starting point for the Web application construction
| Best Practice |
Begin construction by building an application skeleton containing the minimum number of components necessary to configure and start the Web application. |
Once the application skeleton is in place, additional view components are added in a piecewise fashion to flesh out the application. The construction of the user site in bigrez.com was broken down into three primary sections:
Reservation information components responsible for the display of the reservation information area on the page and for the handling of user actions in that area
In the following sections, we ll examine each of these sections of the user site in some detail, highlighting key components and techniques in each section.
Creating a reservation requires a multiple-step process. Intermediate results must be stored in the
HttpSession
on
private String lastSearchCity; private String lastSearchState; private int propertyId; private String propertyDescription; private int roomTypeId; private String roomTypeDescription; private int guestProfileId; private String firstName; private String lastName; private String phone; private String email; private String cardType; private String cardExp; private String cardNum; private Date arriveDate; private Date departDate; private Collection rezRates;
The ReservationRateInfo class has these attributes:
private Date startDate; private int numNights; private float rate;
The current reservation information is displayed on the left side of the screen on every page in the user site in a small reservation information area generated by the RezInfo.jsp display JSP page. Figure 3.2 in the previous chapter showed this reservation information area in the context of the overall display. As the user selects a property, selects dates, or completes additional steps in the process, the reservation information area changes to reflect these selections.
The RezInfo.jsp page generates this area using a jsp:useBean tag to declare the existence of the ReservationInfo object in the HttpSession and provide a local page variable, rezinfo , for accessing the attributes of the object:
<jsp:useBean id=rezinfo scope=session
class=com.bigrez.val.user.ReservationInfo />
The rezinfo variable may now be used in simple jsp:getProperty tags to retrieve specific attributes from the object and display them on the page:
<jsp:getProperty name="rezinfo" property="propertyDescription"/>
Collections contained in the rezinfo variable may be examined using Struts logic:iterate tags:
<jsp:useBean id=rezrates class=java.util.ArrayList scope=page>
<% rezrates = (ArrayList) rezinfo.getRezRates(); %>
</jsp:useBean>
...
<tr>
<td>
<span class=sidebar-title>Rate:</span>
<logic:iterate id=rezrate
type=com.bigrez.val.user.ReservationRateInfo
collection=<%= rezrates %>>
<br> <span class=sidebar-data>
<jsp:getProperty name=rezrate property=numNights/>
nts @ $
<jsp:getProperty name=rezrate property=rate/>/nt</span>
</logic:iterate>
</td>
</tr>
The rezinfo variable may also be used in logic:equal or logic:notEqual tags to display information conditionally. For example, we want to display the string Choose Property for the property description if the user has not yet selected a property for this reservation. Rather than performing this logic in the ReservationInfo value object or creating a separate Helper class, we ve made use of these logic tags to control the display:
<a class=sidebar-link href=/RezInfoAction.do?action=property>
<logic:equal name=rezinfo property=propertyId value=0>
Choose Property
</logic:equal>
<logic:notEqual name=rezinfo property=propertyId value=0>
<jsp:getProperty name=rezinfo property=propertyDescription/>
</logic:notEqual>
</a>
| Best Practice |
Do not place conditional display logic, such as replacing empty values with default messages, in value objects. Use custom tags or other view components to create conditional displays. |
The displayed values in the reservation information area are also used as navigation links, allowing the user to jump back to a previous decision or log in early. As the code snippet shows, the target URL for the property description is
RezInfoAction.do
. All
The
execute()
method in
RezInfoAction
interrogates the request to determine the proper action and
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
String action = request.getParameter(action);
ReservationInfo rezinfo = getRezInfo(request);
...
if (action.equals(dates)) {
if (rezinfo.getPropertyId() == 0) {
action = property; // cant do dates before property
ActionErrors errors = new ActionErrors();
errors.add(ActionErrors.GLOBAL_ERROR,
new ActionError(error.rezinfo.propertybeforedates));
saveErrors(request, errors);
}
else {
// prepare the request with a SelectDatesForm object
// to populate fields
ActionUserHelper.loadSelectDatesForm(request, rezinfo);
}
return mapping.findForward(action);
}
...
}
This simple controller behavior of the RezInfoAction class is depicted in Figure 4.2.
Figure 4.2:
The RezInfoAction controller determines the display page.
As the code snippet indicates, there are many business rules in each if (action_.equals(...)) block in the class that modify this simple controller behavior. For example, if the user has not yet selected a property, the property search page must be displayed before selecting dates or room types. The key is that none of these business rules are contained in the RezInfo.jsp display page. Consistent with the model-view-controller approach, the controller component RezInfoAction is responsible for both navigation and the application of presentation-related business rules.
The
RezInfoAction
class also illustrates an important implementation detail. As discussed earlier, controller components are responsible for placing the required beans, forms, or value objects in the proper scope before forwarding control to the display JSP page. The
RezInfoAction
class forwards control to many different pages in the action-specific branching logic, so it must be capable of preparing many different types of objects before forwarding. The class uses a series of helper methods in the
ActionUserHelper
class to perform this preparation, thereby encapsulating the required business-
ActionUserHelper.loadPropertyBean(request, rezinfo.getPropertyId());
The loadPropertyBean() method in the helper class performs the key operations:
...
PropertyLocal prop =
(PropertyLocal) Locator.getBean(PropertyLocal, id);
request.setAttribute(prop, prop);
...
We ll discuss specific display pages, their preparation requirements, and the contents of the
ActionUserHelper
class in more detail in the
| Best Practice |
Preparation of the request or session context prior to forwarding to a display JSP page should be performed in a common helper method. There may be multiple paths to the same display page, and all controllers leading to that page should use the same helper method to prepare the request or session. |
One other interesting technique demonstrated in
RezInfoAction
is the use of the standard Struts error-handling functions to place messages on
if (action.equals("dates")) {
if (rezinfo.getPropertyId() == 0) {
action = "property"; // cant do dates before property
ActionErrors errors = new ActionErrors();
errors.add(ActionErrors.GLOBAL_ERROR,
new ActionError("error.rezinfo.propertybeforedates"));
saveErrors(request, errors);
}
else {
// prepare the request with a SelectDatesForm to populate fields
ActionUserHelper.loadSelectDatesForm(request, rezinfo);
}
return (mapping.findForward(action));
}
The target page displays these errors by including the standard < html:errors/ > tag in the page definition.
In summary, the reservation information area of the bigrez.com user site is designed to display the current status of the reservation process and allow the user to jump directly to certain steps in the process, subject to business rules enforced in the RezInfoAction controller component. The display area is created by the RezInfo.jsp display JSP page using data stored in the HttpSession in the ReservationInfo and ReservationRateInfo value objects. All hyperlinks invoke the RezInfoAction controller class through the normal Struts action-mapping facilities, and this controller class is responsible for determining the proper JSP page to display.
The reservation information area design and the
The core reservation process,
The reservation process involves a fair number of separate display JSP pages, controller Action classes, and related form beans and value objects. Table 4.1 provides a list of the primary components in the process. Note that not all display pages have form beans and that all pages, except the final ReservationThankYou page, have their own controller Action class for processing user actions on the page.
|
Display Component |
Related Controller Component |
Form Bean |
|---|---|---|
|
PropertySearch.jsp |
PropertySearchAction.java |
PropertySearchForm.java |
|
PropertyList.jsp |
PropertyListAction.java |
|
|
SelectDates.jsp |
SelectDatesAction.java |
SelectDatesForm.java |
|
SelectRoomType.jsp |
SelectRoomTypeAction.java |
|
|
GuestInformation.jsp |
GuestInformationAction.java |
GuestInformationForm.java |
|
ReviewReservation.jsp |
ReviesReservationAction.java |
|
|
ReservationThankYou.jsp |
| Best Practice |
Use a standard naming convention for display pages, Action controller classes, and form beans to make relationships between components clear without inspecting the configuration file. The chosen convention of appending Action and Form to the display page name is a reasonable choice. |
As discussed in Chapter 2, the servlet-centric Web application architecture dictates certain rules and principles related to presentation-tier components and their relationships. One key principle is the separation of roles between display components, controller components, and navigational control facilities. The bigrez.com application meets the requirements by adopting the following rules:
Display JSP pages must use < PageName > Action.do controller invocations for all hyperlinks and form-posting targets. The controller is always responsible for determining the next page in the process based on the user s action, the current state, and navigation control information.
Controller components do not refer directly to display JSP page names when specifying the next page for display. Logical page names are used in controller code, and these logical names are mapped to actual page names in the Struts configuration files.
A critical aspect of this design is the Struts configuration file,
struts-config.xml
. This file defines the mapping between logical and actual JSP page names as well as the relationships between pages, form beans, and their controllers. Please download the
struts-config.xml
file for
bigrez.com
from the companion web site (http://www.
We ve also used the
struts-config.xml
file to define the basic course, or
happy
<action path="/SelectDatesAction"
type="com.bigrez.ui.user.SelectDatesAction"
name="SelectDatesForm"
scope="request"
validate="true"
input="/SelectDates.page">
<
forward name="success" path="/SelectRoomType.page" redirect="false"/
>
</action>
As we ll examine in a moment, the controller component may now
return (mapping.findForward(success));
In theory, you can change the order of the reservation process by simply modifying the entries in
struts-config.xml
to indicate the new
success
pages for each page in the process. Unfortunately, modifying the page flow also requires some changes in related controller classes to prepare the request properly, given the new
success
page. These kinds of controller changes
Controller components that do not follow this simple success chain or that need to indicate a different target page also use logical page mappings defined in the configuration file. The RezInfoAction class already discussed had many examples of this technique.
Now that we ve discussed the basic approach to navigation in the user site, let s examine selected pages and controller components in the site to learn more about its construction.
As indicated in Table 4.1, the first page in the reservation process is PropertySearch.jsp , a simple search page allowing the user to pick the desired city or state to use in finding a property. This page uses a form bean and the standard Struts tags for creating HTML forms and input elements, as you can see in Listing 4.1.
Listing 4.1: PropertySearch.jsp.
|
|
<%@ page extends="com.bigrez.ui.MyJspBase" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<html:form action="/PropertySearchAction" method="get">
<table width="100%" cellspacing="5" cellpadding="0">
<tr>
<td class="page-header" align="right">Find a Property</td>
</tr>
<tr><td><html:errors/></td></tr>
<tr>
<td class="page-text">
Please enter the state or city you plan to visit:
</td>
</tr>
<tr><td> </td></tr>
<tr>
<td>
<table width="100%" cellspacing="6" cellpadding="0">
<tr>
<td width="25%" class="page-label" nowrap>State Code:</td>
<td width="75%">
<html:select property="stateCode">
<html:option value="">Choose...</html:option>
<html:options collection="stateCodeList"
property="value" labelProperty="label"/>
</html:select>
</td>
</tr>
<tr>
<td class="page-label" nowrap>City:</td>
<td>
<html:select property="city">
<html:option value="">Choose...</html:option>
<html:options collection="cityList"
property="value" labelProperty="label"/>
</html:select>
</td>
</tr>
<tr><td> </td></tr>
<tr>
<td colspan="2" align="left">
<input type="submit" value="Find Properties">
</td>
</tr>
</table>
</td>
</tr>
</table>
</html:form>
|
|
The drop-down list elements of valid state codes and city codes are generated using the html:select and html:option Struts tags. These tags look for the specified collections, either stateCodeList or cityList , in all of the scopes available to the page. In this case, we ve preloaded these collections in the application scope using an InitializationServlet loaded during application startup.
The PropertySearch.jsp page submits the contents of the HTML form to the standard ActionServlet defined in the Struts framework. The ActionServlet then forwards the contents to the PropertySearchAction controller class using the form bean defined for this page, PropertySearchForm . Note that the html:form tag in the page defines the method attribute to be a GET rather than a POST . We ve used the GET method on a number of the forms in the user site to allow more natural browser navigation without warning messages caused by POST actions.
| Best Practice |
Use
GET
rather than
POST
when possible in form pages. Users will be able to navigate more
|
As before, the PropertySearchForm form bean is first given a chance to validate the HTML form contents using its validate() method:
public ActionErrors validate(ActionMapping mapping,
HttpServletRequest request)
{
ActionErrors errors = new ActionErrors();
if (isEmpty(city) && isEmpty(stateCode))
errors.add(ActionErrors.GLOBAL_ERROR,
new ActionError(error.propertysearch.nocriteria));
return errors;
}
If the form-bean validation returns no errors, the execute() method in the controller class is invoked to complete the processing of this form submission. As shown in Listing 4.2, the execute() method uses the selected values of city and stateCode to execute a finder method directly on the PropertyHomeLocal interface:
PropertyHomeLocal propertyhome =
(PropertyHomeLocal) Locator.getHome("PropertyHomeLocal");
Collection props = null;
...
LOG.debug("Finding properties using city " + city);
props = propertyhome.findByCity(city);
...
This direct invocation of the entity bean finder method to retrieve a list of matching properties is consistent with the direct interaction approach we ve adopted for this application. The returned collection is actually a collection of local references, implementing the PropertyLocal interface, representing the related PropertyBean objects. Chapter 7 describes the declaration and definition of finder methods, such as this one, using EJB generation tools.
Listing 4.2: PropertySearchAction.java.
|
|
package com.bigrez.ui.user;
import java.io.IOException;
import java.util.Collection;
import javax.ejb.FinderException;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import com.bigrez.ejb.PropertyHomeLocal;
import com.bigrez.form.user.PropertySearchForm;
import com.bigrez.utils.Locator;
import com.bigrez.val.user.ReservationInfo;
public final class PropertySearchAction extends BigRezUserAction
{
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
LOG.info(">>> PropertySearchAction::execute()");
PropertySearchForm pform = (PropertySearchForm)form;
try {
String city = pform.getCity();
String stateCode = pform.getStateCode();
ReservationInfo rezinfo = getRezInfo(request);
rezinfo.setLastSearchCity(city);
rezinfo.setLastSearchState(stateCode);
PropertyHomeLocal propertyhome = (PropertyHomeLocal)
Locator.getHome("PropertyHomeLocal");
Collection props = null;
if (!isEmpty(city)) {
if (!isEmpty(stateCode)) {
LOG.debug("Finding properties using city " + city +
" and state " + stateCode);
props =
propertyhome.findByCityState(city, stateCode);
}
else {
LOG.debug("Finding properties using city " + city);
props = propertyhome.findByCity(city);
}
}
else {
LOG.debug("Finding properties using state " +
stateCode);
props = propertyhome.findByState(stateCode);
}
ActionUserHelper.loadPropertyBeans(request, props);
}
catch (NamingException ne) {
LOG.error("NamingException searching for properties", ne);
ActionUserHelper.loadJSPException(request, ne);
return (mapping.findForward("error"));
}
catch (FinderException fe) {
LOG.error("FinderException searching for properties", fe);
ActionUserHelper.loadJSPException(request, fe);
return (mapping.findForward("error"));
}
return mapping.findForward("success");
}
}
|
|
The final step in handling the search request is placing the necessary objects in the HttpServletRequest context to prepare for the display JSP page showing the results. The display page, PropertyList.jsp , expects the matching properties to be located in the request using the props identifier, so we could easily place our collection of matching properties in the request manually using:
request.setAttribute(props, props);
The best practice presented earlier indicated that helper methods should be used for this purpose, so we ll delegate this preparation step to the ActionUserHelper class using the following invocation:
ActionUserHelper.loadPropertyBeans(request, props);
If you examine the code in ActionUserHelper you ll notice that loadPropertyBeans() performs the same request.setAttribute(...) statement in this simple case:
public static void loadPropertyBeans(HttpServletRequest request,
Collection props)
throws FinderException, NamingException
{
request.setAttribute(props, props);
return;
}
Many other preparation methods in ActionUserHelper are more involved, as evidenced by the length and complexity of the code in the class. We make heavy use of these helper methods in other controller classes.
The
PropertySearchAction
controller forwards the request to the
success
mapping for this page, which according to the
struts-config.xml
file is
PropertyList.page
. WebLogic Server s servlet container intercepts this
*.page
request and
Figure 4.3:
Property list page.
The
PropertyList.jsp
page is
First, the page gains access to the collection of matching property references using the jsp:useBean element:
<jsp:useBean id=props type=java.util.Collection scope=request/>
Note that the collection must be present under the ID
props
, or this tag will fail attempting to instantiate a
Collection
object. This fact emphasizes again the importance of preparing data in the controllers before forwarding
The collection is iterated through using the logic:iterate tag in Struts:
<logic:iterate id="prop" type="com.bigrez.ejb.PropertyLocal"
collection="<%= props %>">
...
</logic:iterate>
In this loop, the attributes of each PropertyBean are displayed using jsp:_getProperty elements and the prop reference created by the iteration tag. Because we are interacting directly with the entity beans, all of this activity must be performed in the context of a transaction. The BeginTrans.jspf and EndTrans.jspf files are included at the top and bottom of the page, respectively, to ensure that we are in a transaction.
Consistent with our servlet-centric approach, all of the links on this page use
PropertyListAction.do
as the primary target, with different links specifying different
action
values and any required parameters. For example, the Select button for a property is defined in the page as
<a href="PropertyListAction.do?action=select&id=<jsp:getProperty
name="prop" property="id"/>">
<img src="/images/selectbutton.gif" alt="" border="0">
</a>
Clicking Select for a given property invokes the PropertyListAction class, passing in the parameter action with a value of select and the parameter id with a value equal to the property primary key. The PropertyListAction class saves this property information in the ReservationInfo object in the session, prepares the necessary beans or forms, and forwards to the next page in the process:
beginTrans(request);
String propertyId = request.getParameter(id);
int id = Integer.parseInt(propertyId);
PropertyLocal prop = ActionUserHelper.loadPropertyBean(request, id);
if (action.equals(select)) {
ReservationInfo rezinfo = getRezInfo(request);
if (id != rezinfo.getPropertyId()) {
rezinfo.setPropertyId(id);
rezinfo.setPropertyDescription(prop.getDescription());
rezinfo.clearRoomType();
rezinfo.clearRates();
}
ActionUserHelper.loadSelectDatesForm(request, rezinfo);
action = success; // next step in process
setRezInfo(request, rezinfo);
}
commitTrans(request);
Note the use of helper classes and utility methods, such as
getRezInfo()
and
setRezInfo()
, rather than accessing attributes directly in the
HttpServletRequest
and
HttpSession
. It is always a good idea to reduce the number of places in your code that access these objects using key values to avoid unnecessary
The call to setRezInfo() (defined in the BigRezUserAction base class) performs a setAttribute() in the HttpSession to place the ReservationInfo object in the session:
protected void setRezInfo(HttpServletRequest request, Object rezinfo)
{
// set the information in the session again to ensure replication
request.getSession(true).setAttribute(rezinfo, rezinfo);
}
Why do we have to do this again when the
ReservationInfo
object is already in the session and the only activity in this method was changing some of the attributes in the object? Calling
setAttribute()
is important when using session persistence because it
| Best Practice |
Make sure to call
setAttribute()
on any session
|
As shown in this partial source listing for ActionUserHelper.java , the load_SelectDatesForm() method prepares for the SelectDates.jsp page by creating a SelectDatesForm form bean and placing it in the request:
public static void loadSelectDatesForm(HttpServletRequest request,
ReservationInfo rezinfo)
{
LOG.info(">>> loadSelectDatesForm");
// prepare the request with a SelectDatesForm to populate fields
SelectDatesForm sdform = new SelectDatesForm();
if (rezinfo.getArriveDate() != null) {
sdform.setArriveDate(DateHelper.format1(rezinfo.getArriveDate()));
sdform.setDepartDate(DateHelper.format1(rezinfo.getDepartDate()));
}
else {
Calendar now = Calendar.getInstance();
now.set(Calendar.HOUR, 0);// get midnight date/time
now.set(Calendar.MINUTE, 0);
now.set(Calendar.SECOND, 0);
sdform.setArriveDate(DateHelper.format1(now.getTime()));
now.add(Calendar.DAY_OF_MONTH, 1);
sdform.setDepartDate(DateHelper.format1(now.getTime()));
}
request.setAttribute("SelectDatesForm", sdform);
}
Note that the
loadSelectDatesForm()
method prepopulates the form bean with dates based on the current date if no values are present in the
ReservationInfo
value object. We are now ready to proceed to the
SelectDates
page and obtain the user s desired arrival and
The
SelectDates.jsp
page
Figure 4.4:
Select dates page.
The user chooses the desired dates and submits the form to the
SelectDates_Action
controller class. This form again uses the
GET
method rather than
POST
to improve browser navigation. Consistent with the servlet-centric architecture, all field validation takes place in the form bean
validate()
method or in the controller class. The
validate()
method in
SelectDatesForm
checks for empty or invalid dates, as well as
public ActionErrors validate(ActionMapping mapping,
HttpServletRequest request)
{
LOG.info(>>> SelectDatesForm::validate());
ActionErrors errors = new ActionErrors();
if (assertNonEmpty(errors, arriveDate,
error.selectdates.arriveempty)) {
assertValidDate(errors, arriveDate,
error.selectdates.arriveinvalid);
}
if (assertNonEmpty(errors, departDate,
error.selectdates.departempty)) {
assertValidDate(errors, departDate,
error.selectdates.departinvalid);
}
try {
Date arrive = DateHelper.parse(arriveDate);
Date depart = DateHelper.parse(departDate);
if (arrive.equals(depart) arrive.after(depart)) {
errors.add(ActionErrors.GLOBAL_ERROR, new ActionError(error.selectdates.arriveafterdepart));
}
}
catch (ParseException e) {
LOG.error(ParseException validating SelectDatesForm, e);
errors.add(ActionErrors.GLOBAL_ERROR,
new ActionError(error.validationproblem));
}
return errors;
}
If the validate() method succeeds, the dates must be present and properly formatted. Processing can then continue in the execute() method of the SelectDates_Action class:
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
LOG.info(">>> SelectDatesAction::execute()");
SelectDatesForm sdform = (SelectDatesForm)form;
try {
// save the selected dates in the ReservationInfo object
ReservationInfo rezinfo = getRezInfo(request);
// safety checks that we have property selection already
if (rezinfo.getPropertyId() == 0) {
return (mapping.findForward("property"));
}
rezinfo.setArriveDate(DateHelper.parse(sdform.getArriveDate()));
rezinfo.setDepartDate(DateHelper.parse(sdform.getDepartDate()));
rezinfo.setRezRates(new ArrayList());
setRezInfo(request, rezinfo);
// prepare the information required for next page
ActionUserHelper.loadPropertyBean(request, rezinfo.getPropertyId());
ActionUserHelper.loadRateAvailabilityInfos(request, rezinfo);
} catch (ParseException e) {
LOG.error("ParseException setting dates", e);
ActionUserHelper.loadJSPException(request, e);
return (mapping.findForward("error"));
} catch (NamingException e) {
LOG.error("NamingException setting dates", e);
ActionUserHelper.loadJSPException(request, e);
return (mapping.findForward("error"));
}
return mapping.findForward("success");
}
The execute() method has two primary tasks: save the selected dates in the ReservationInfo object in the session and prepare the necessary beans in the request for the next page, SelectRoomType.jsp .
Saving the selected dates is a task made easy by the DateHelper helper class used to format the form attributes and by the getRezInfo() and setRezInfo() methods used to retrieve and set the information in the session.
Preparing for the next display page looks like another simple task, involving only two calls to the
ActionUserHelper
class to perform the preparation. But looks can be deceiving. The call to
loadPropertyBean()
simply places a local reference to the currently selected property bean in the request, but the call to
loadRateAvailabilityInfos()
does something we haven t
public static void loadRateAvailabilityInfos(HttpServletRequest request, ReservationInfo rezinfo)
throws NamingException
{
LOG.info(">>> loadRateAvailabilityInfos");
ReservationSessionLocal rezsession = (ReservationSessionLocal)
Locator.getSessionBean("ReservationSessionLocal");
PropertyLocal prop = (PropertyLocal)
Locator.getBean("PropertyLocal", rezinfo.getPropertyId());
Collection rainfos =
rezsession.calculateAllRateAvailabilityInfo(
prop, rezinfo.getArriveDate(), rezinfo.getDepartDate());
request.setAttribute("rainfos", rainfos);
}
This method makes use of a session bean called
ReservationSession
to retrieve a collection of objects for the given property and dates. The returned collection contains
RateAvailabilityInfo
objects, a class encapsulating information about the price and availability of a given room type in the selected hotel for the dates
private RoomTypeLocal roomType; private boolean availableFlag; private Collection rates; private Collection blockingControls;
Each attribute of the RateAvailabilityInfo object plays a different role:
The roomType attribute is a reference to the RoomType entity bean this object represents.
The availableFlag attribute is a simple Boolean value indicating whether the room is available for the requested dates.
The rates collection contains RaeservationRateInfo objects representing date ranges and rates for this room type during the requested dates (recognize that rates could change during the length of the stay).
The blockingControls collection contains a list of Inventory entity bean references for days that cannot be booked at this hotel during the date range.
The creation of these objects and collections is covered in Chapter 7 when we examine the ReservationSession bean and the calculateAllRateAvailabilityInfo() method.
Why did we use a session bean here, and does this argue that the direct interaction approach is inappropriate for our application because it cannot be used for all business-tier interaction? We believe a session bean makes sense in this case because the work required to determine the rates and availability is complex enough to
| Best Practice |
Favor encapsulating complex business logic in session beans, even when using the direct interaction approach, to improve efficiency and maintainability. |
Notice that the RateAvailabilityInfo objects returned by the session bean are hybrids rather than pure value objects because they contain entity-bean references rather than additional value objects where it is convenient to do so. This collection of objects is placed in the HttpServletRequest , just like any other entity bean or form bean, to make it available for use in the next page in the process, the SelectRoomType page.
As shown in Figure 4.5, the SelectRoomType page presents the user with a list of room types, rates, and availability information to assist them in choosing the desired room for their stay. Rooms that are not available for the entire duration of the stay are not available for selection and indicate the specific nights they are unavailable below their normal rates. We ll talk more about rates and availability when we walk through some pages in the administration site, so for now let s concentrate on how this content is built by the display JSP page.
Figure 4.5:
Select room type page.
The SelectRoomType.jsp page is listed in Listing 4.3 in its entirety because it contains many interesting features and highlights some limitations of the JSP tag library when dealing with complex nested data structures.
Listing 4.3: SelectRoomType.jsp.
|
|
<%@ page import="com.bigrez.ejb.*" %>
<%@ page extends="com.bigrez.ui.MyJspBase" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<%@ include file="include/BeginTrans.jspf" %>
<jsp:useBean id="prop" scope="request"
type="com.bigrez.ejb.PropertyLocal" />
<jsp:useBean id="rainfos" scope="request"
type="java.util.Collection" />
<table width="100%" cellspacing="5" cellpadding="0">
<tr>
<td class="page-header" align="right">Room Types</td>
</tr>
<tr>
<td class="page-text">The <jsp:getProperty name="prop"
property="description"/> has the following room types:
</td>
</tr>
<tr>
<td>
<table width="100%" cellspacing="0" cellpadding="3" border="0">
<logic:iterate id="rainfo" collection="<%= rainfos %>"
type="com.bigrez.val.user.RateAvailabilityInfo">
<tr>
<td width="30%" align="left">
<a class="table-link"
href="SelectRoomTypeAction.do?action=select&id=<bean:write
name="rainfo" property="roomType.id"/>">
<bean:write name="rainfo" property="roomType.description"/>
</a>
</td>
<td width="25%" class="table-data">
<bean:message key="<%= \"smoking\" +
rainfo.getRoomType().getSmokingFlag() %>"/>
</td>
<td width="25%" class="table-data">
<bean:write name="rainfo"
property="roomType.maxAdults"/> Adults Max
</td>
<logic:equal name="rainfo" property="availableFlag" value="true">
<td width="20%" align="center">
<a href="SelectRoomTypeAction.do?action=select&id=<bean:write
name="rainfo" property="roomType.id"/>">
<img src="/images/selectbutton.gif" alt="" border="0">
</a>
</td>
</logic:equal>
<logic:equal name="rainfo"
property="availableFlag" value="false">
<td class="table-header" width="20%" align="center">
Unavailable
</td>
</logic:equal>
</tr>
<tr>
<td colspan="3" class="table-data">
<bean:write name="rainfo" property="roomType.features"/>
</td>
</tr>
<logic:iterate id="rate" collection="<%= rainfo.getRates() %>"
type="com.bigrez.val.user.ReservationRateInfo" >
<tr>
<td colspan="3" class="table-data">
Rate: $<bean:write name="rate"
property="rate" formatKey="float.price"/>/night
for
<jsp:getProperty name="rate" property="numNights"/> nts
</td>
</tr>
</logic:iterate>
<logic:iterate id="blocker" type="com.bigrez.ejb.InventoryLocal"
collection="<%= rainfo.getBlockingControls() %>">
<tr>
<td colspan="3" class="table-data">
Not Available on <bean:write name="blocker"
property="day" formatKey="date.format1"/>
</td>
</tr>
</logic:iterate>
<tr><td> </td></tr>
</logic:iterate>
</table>
</td>
</tr>
</table>
<%@ include file="include/EndTrans.jspf" %>
|
|
The SelectRoomType page first declares the existence of prop and rainfos beans using jsp:useBean tags:
<jsp:useBean id="
prop
" scope="request"
type="com.bigrez.ejb.PropertyLocal" />
<jsp:useBean id="
rainfos
" scope="request" type="java.util.Collection"/>
These variables are now available for use in either jsp:getProperty tags or Struts custom tags. The page next uses the logic:iterate tag to iterate through the rainfos collection, defining a new page bean called rainfo in the loop:
<logic:iterate id="
rainfo
" collection="<%= rainfos %>"
type="com.bigrez.val.user.RateAvailabilityInfo">
...
</logic:iterate>
Recall that each object in the rainfos collection is a RateAvailabilityInfo value object containing a reference to the related RoomType entity bean as well as rate and availability information. These other objects are nested in the RateAvailabilityInfo object, in a sense, so the page must traverse these internal nesting links to display the data related to the nested objects.
In the main loop, the page displays basic room type information by traversing the nested link in the RateAvailabilityInfo object to the RoomType bean using the bean:write tag provided by Struts:
<bean:write name=rainfo property=roomType.description/>
The
bean:write
tag, unlike the normal
jsp:getProperty
tag, allows nested attribute names in the
property
attribute of the tag. If a standard
jsp:getProperty
tag was used to display the nested data, the simple syntax you just saw would be
<% loadPageObject(pageContext, roomType, rainfo.getRoomType()); %> <jsp:useBean id=roomType type=com.bigrez.ejb.RoomTypeLocal/> <jsp:getProperty name=roomType property=description/>
The scriptlet code calls the loadPageObject() method in the JSP page base class, MyJspBase , in order to place the RoomType reference from the current rainfo object in the page context. This step is required because the jsp:useBean tag must find the variable already located in the desired scope, a limitation discussed earlier in this chapter. The jsp:useBean tag then declares the roomType variable, making it available for use by the subsequent jsp:getProperty tag. Finally, the jsp:getProperty tag displays the description value. This three-step process for accessing nested attributes in page beans argues strongly for the use of a custom-tag library, such as the bean or nested libraries in Struts, when accessing nested attributes.
| Best Practice |
Use Struts bean:write or nested tags to access nested attributes and components in display JSP pages rather than standard jsp:getProperty tags to avoid complexity caused by the limitations of the standard jsp:useBean and jsp:getProperty tags. |
Unlike some Struts applications, the bigrez.com application does not make heavy use of the bean:message tag to parameterize display strings and retrieve them from the ApplicationResources.properties file. This was a conscious decision to help maintain clarity by retaining actual display messages in the example JSP pages. So far, the only messages placed in the ApplicationResources.properties file have been error messages used by server-side form validation and other controller logic.
The SelectRoomType page uses the bean:message tag to demonstrate an interesting technique for converting Boolean bean attributes to a corresponding message in the JSP page. The RoomType bean in the rainfo object includes a smokingFlag Boolean attribute, and this page must display either Smoking or Non-Smoking depending on the value of this attribute. There are many ways to accomplish this, of course, including the ternary operator in JSP scriptlet code:
<%= rainfo.getRoomType().getSmokingFlag().booleanValue() ? Smoking:Non-Smoking %>
A verbose set of logic tags would also work:
<logic:equal name=rainfo property=roomType.smokingFlag value=true> Smoking </logic:equal> <logic:equal name=rainfo property=roomType.smokingFlag value=false> Non-Smoking </logic:equal>
We ve chosen to
smokingtrue=Smoking smokingfalse=Non-Smoking
We then provide the bean:message tag a key containing the value of the smoking flag:
<bean:message key=<%= \smoking\+rainfo.getRoomType().getSmokingFlag() %>/>
This solution is not much better than the JSP scriptlet technique, but it does illustrate the potential for displaying conditional messages using the bean:message tag. Note that the run-time expression defining the value for the key attribute includes a literal, smoking , requiring escaped double quotes to avoid JSP parsing errors.
The
SelectRoomType
page also
date.format1=MM/dd/yyyy date.format2=MMM dd, yyyy float.price=#.00
Dates and prices are then displayed on the page using these formats in the bean:write tags:
<bean:write name="rate" property="rate" formatKey="float.price" /> ... <bean:write name="blocker" property="day" formatKey="date.format1" />
Alternative techniques using ViewHelper objects are possible, but they require scriptlet code embedded in the HTML and appropriate < %@ page import ... % > directives at the top of the page, while the bean:write mechanism provides a cleaner and more flexible solution.
| Best Practice |
Use bean:write tags with display formats defined in the application properties file to format display values such as dates and amounts. |
The user examines the room types and rates displayed on this page and selects the desired room by clicking one of the Select buttons on the right side of the page. These buttons, like every other hyperlink in bigrez.com , are mapped to an Action controller class:
<a href=SelectRoomTypeAction.do?action=select&id=
<bean:write name=rainfo property=roomType.id/>>
<img src=/images/selectbutton.gif alt= border=0>
</a>
A partial listing of the target SelectRoomTypeAction controller class is shown below:
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
LOG.info(">>> SelectRoomTypeAction::execute()");
try {
int roomTypeId = Integer.parseInt(request.getParameter("id"));
ReservationInfo rezinfo = getRezInfo(request);
// safety checks that we have property and dates already
if (rezinfo.getPropertyId() == 0) {
return mapping.findForward("property");
}
else if (rezinfo.getArriveDate() == null) {
ActionUserHelper.loadSelectDatesForm(request, rezinfo);
return mapping.findForward("dates");
}
beginTrans(request);
ReservationSessionHomeLocal home =
(ReservationSessionHomeLocal)
Locator.getHome("ReservationSessionHomeLocal");
ReservationSessionLocal rezsession = home.create();
RoomTypeLocal roomtype = (RoomTypeLocal)
Locator.getBean("RoomTypeLocal", roomTypeId);
Collection rezrates =
rezsession.calculateRates(roomtype,
rezinfo.getArriveDate(),
rezinfo.getDepartDate());
rezinfo.setRoomTypeId(roomTypeId);
rezinfo.setRoomTypeDescription(roomtype.getDescription());
rezinfo.setRezRates(rezrates);
setRezInfo(request, rezinfo);
// prepare the request with a GuestInformationForm object
ActionUserHelper.loadGuestInformationForm(request, rezinfo);
commitTrans(request);
}
catch (Exception e) {
...
}
return mapping.findForward("success");
}
SelectRoomTypeAction processes the room type selection by calling the ReservationSession bean to retrieve rate information, placing the selected room and rate information in the ReservationInfo session variable, preparing the required form bean, and forwarding to the next page in the reservation process.
The next page in the reservation process, GuestInformation , is a fairly straightforward HTML form used to collect guest information and credit-card information for the reservation. It is a typical Struts form using the GuestInformationForm form bean and submitting the data to the GuestInformationAction controller class, a pattern we ve already covered in some detail. You may examine these components in the downloaded example code if desired, but we will not discuss the GuestInformation page in this text.
The final step in the reservation process begins with a confirmation page, ReviewReservation , shown in Figure 4.6. The ReviewReservation.jsp display page simply displays information from the ReservationInfo object located in the HttpSession along with the standard property information from a PropertyLocal reference placed in the request by the controller responsible for forwarding to this page. The user examines the contents and clicks on the confirmation button to officially make the reservation.
The only wrinkle introduced on this page is the use of an HTML form, created using Struts form tags, even though there are no apparent input fields in the form. We will make use of the token-based form-posting logic built in to Struts to ensure that this form is submitted for processing once and only once. Rather than building this logic
saveToken(request)
The controller component this page submits to, ReviewReservationAction , checks for the token during form processing:
// Check for the transaction token set in previous action class
if (!checkToken(request, "error.transaction.token")) {
return mapping.findForward("systemproblem");
}
Figure 4.6:
Review reservation page.
We ve used a helper method, checkToken , defined in the BigRezAction base class for all Action classes:
protected boolean checkToken(HttpServletRequest request,
String errorkey) {
if (!isTokenValid(request)) {
ActionErrors errors = new ActionErrors();
errors.add(ActionErrors.GLOBAL_ERROR,
new ActionError(errorkey));
saveErrors(request, errors);
return false;
}
else {
resetToken(request);
return true;
}
}
If the token is not present or is not valid, the form submission is not accepted and the user is presented with an error message
After validating and clearing the token, the ReviewReservationAction controller class is responsible for invoking the proper business components to create the final, persistent reservation. It passes a copy of the ReservationInformation value object to the ReservationSession session bean responsible for this task:
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
LOG.info(">>> ReviewReservationAction::execute()");
try {
...
ReservationSessionLocal rezsession = (ReservationSessionLocal)
Locator.getSessionBean("ReservationSessionLocal");
ReservationLocal reservation =
rezsession.createReservation(rezinfo);
// prepare for the thankyou page before clearing rezinfo
HttpSession session = request.getSession(true);
session.setAttribute("rezid", reservation.getId());
rezinfo.clearAllButProfile();
setRezInfo(request, rezinfo);
}
catch (BigRezBusinessException e) {
...
}
catch (NamingException e) {
...
}
catch (EJBException e) {
...
}
return mapping.findForward("success");
}
We have adopted the value object and session fa §ade pattern for the complex reservation creation operation for reasons we ve discussed previously. Note that the primary key of
Reservation
entity bean created and returned by the
createReservation
method is placed in the
HttpSession
rather than the
HttpServletRequest
in preparation for the
ReservationThankYou
page. This unusual choice was made to allow a
redirect= true
in the
success
mapping from this action to the
ReservationThankYou
page, thereby
Before redirecting to the next page, all previous information in the session-based ReservationInfo object is cleared to indicate that the reservation process is complete. The user is now presented with the ReservationThankYou page presenting the data contained in the final Reservation bean. The process is complete!
To round out our discussion of the user site in
bigrez.com
, we will
There are two interesting techniques demonstrated by the offers area:
Determining the offers to be displayed using a stateless session bean with method invocations placed directly on the display JSP page
Caching the displayed offers using WebLogic Server wl:cache custom tags
The specific offers presented to the user depend on the last city and state searched by the user and the current selected property, if any, in the ReservationInfo object. The business logic for selecting and ordering the offers is complex and is therefore encapsulated in a session bean, the OfferSessionBean , in a method called getOffersForDisplay() . The business-logic contained in this session bean is discussed in Chapter 7. The source code for Offers.jsp is shown in Listing 4.4.
Listing 4.4: Offers.jsp.
|
|
<%@ page import="com.bigrez.utils.*, com.bigrez.ejb.*" %>
<%@ page extends="com.bigrez.ui.MyJspBase" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/weblogic-tags.tld" prefix="wl" %>
<jsp:useBean id="rezinfo" scope="session"
class="com.bigrez.val.user.ReservationInfo" />
<% loadPageObject(pageContext, "offerhash", rezinfo.getOfferHash());%>
<wl:cache name="Offers" key="offerhash" timeout="30s" scope="session">
<% LOG.info("Creating Offers.jsp display for hash "+offerhash);%>
<%@ include file="include/BeginTrans.jspf" %>
<% loadSessionBean(pageContext, "offersession", "OfferSessionLocal");%>
<jsp:useBean id="offersession" scope="page"
class="com.bigrez.ejb.OfferSessionLocal"/>
<table width="100%" cellpadding="0" cellspacing="0" bgcolor="#EEEEEE">
<tr valign="top">
<!-- force offer block to be at least 200 high -->
<td width="1" bgcolor="#EEEEEE">
<img src="images/space.gif" width="1" height="200"></td>
<td>
<table width="100%" align="center" cellpadding="0"
cellspacing="5" bgcolor="#EEEEEE">
<logic:iterate id="offer" type="com.bigrez.ejb.OfferLocal"
collection="<%=
offersession.getOffersForDisplay(rezinfo, 2) %>">
<tr><td>
<img src="images/space.gif" width="1" height="5">
</td></tr>
<tr align="center">
<td>
<img src="/images/<jsp:getProperty name="offer"
property="imageFile"/>"
alt="<jsp:getProperty name="offer"
property="description"/>"
width="70" height="70">
</td>
</tr>
<tr align="center">
<td>
<a class="sidebar-link"
HREF="/OffersAction.do?id=<jsp:getProperty
name="offer" property="id"/>">
<jsp:getProperty name="offer" property="caption"/><br>
<bean:write name="offer"
property="property.description"/>
</a>
</td>
</tr>
</logic:iterate>
</table>
</td>
</tr>
</table>
<%@ include file="include/EndTrans.jspf" %>
</wl:cache>
|
|
Unlike most display JSP pages in the site, there is no controller responsible for preparing the required Offer objects in the HttpServletRequest prior to forwarding to this page. The Offers.jsp display page is actually included by the Master_.jsp page-assembly template on every page request, so every controller would be responsible for creating the necessary data in the request if we adopted the normal design pattern. Instead, we allow the Offer.jsp page to call the stateless session bean directly to retrieve the collection of Offer objects to display based on the current ReservationInformation contents:
<% loadSessionBean(pageContext, "offersession", "OfferSessionLocal");%>
<jsp:useBean id="offersession" scope="page"
class="com.bigrez.ejb.OfferSessionLocal" />
...
<logic:iterate id="offer" type="com.bigrez.ejb.OfferLocal"
collection="<%=
offersession.getOffersForDisplay(rezinfo,2)
%>">
...
</logic:iterate>
Note that we could have used a business delegate pattern at this point by encapsulating the session bean invocation in a page bean or other helper, but in the spirit of direct interaction, we will avoid introducing intermediate objects and helpers unless they encapsulate significant complexity or foster reuse. The code required to access the bean directly is fairly straightforward, the only trick being the use of a helper method defined on the JSP base page to create the stateless session bean and make it available in the PageContext before declaring it with a jsp:useBean element. The loadSessionBean() helper method is defined on MyJspBase.java, as shown here:
protected void loadSessionBean(PageContext context,
String name, String localname)
throws NamingException
{
Object _temp = Locator.getSessionBean(localname);
context.setAttribute(name, _temp);
}
Once the JSP page has the offersession bean reference, the logic:iterate tag is employed to loop through the list returned by the getOffersForDisplay() method and display the contents of each Offer in the list. The objects returned in this list are actually local references to OfferBean entity beans, so the access must be within the context of a transaction. Displaying the offer data is performed with straightforward jsp:getProperty elements in the loop as well as a single bean:write tag used to access the description (name) of the property related to this offer:
<bean:write name=offer property=property.description/>
The second interesting technique demonstrated by Offers.jsp is the use of the WebLogic Server wl:cache tag to improve the performance of this page. We discussed the capabilities and limitations of the wl:cache tag in some detail in Chapter 1. The tag basically caches the HTML response created within the body of the tag for a specific duration, using the cached version of the response rather than evaluating the body on subsequent page requests. The goal in Offers.jsp is to reduce the number of hits to the session bean and the related Offer beans to improve performance.
If the offers displayed to the user were completely random and unrelated to the current ReservationInformation context, we might be tempted simply to surround the bulk of the Offer.jsp page with caching tags similar to the following:
<wl:cache name=Offers timeout=30s scope=application> ... // Generate all HTML output ... </wl:cache>
In this scenario, the cached content would expire after 30 seconds. The next page request would cause a
We could flush the cache every time the user makes a selection that affects the displayed offers. This technique was described in Chapter 1 and might be required in certain circumstances. There is, however, a better way: cache the response based on the current selection data.
As described in Chapter 1, the wl:cache tag includes a key attribute that can be used to specify the variables whose values should be used as the key for the cached contents. Essentially, the cached contents should depend on the values stored in the variables defined in the key attribute. In our case, the cached content depends on the recent city, state, and property selections made by the user. Rather than expose all of this complexity in the key definition, we ve introduced a hash function on the ReservationInfo class and used this hash as the key for the caching tag:
<% loadPageObject(pageContext, "offerhash", rezinfo.getOfferHash()); %>
<wl:cache name="Offers" key="
offerhash
"
timeout="30s" scope="application">
The getOfferHash() method in ReservationInfo simply appends the selection values together to form a String value representing the current user selections:
public String getOfferHash()
{
// return a hash string of data used to fetch offers
return "[" + lastSearchCity + "," + lastSearchState + "," +
propertyId + "]";
}
This string is placed in the page context by loadPageObject() using the name offerhash , and the wl:cache tag is then configured to control caching using this variable. If the application already has a response cached for that particular hash of selection information, the tag will use the previous response. If the cache does not contain a response for that selection information, the body of the tag will be evaluated and the response cached using the key. The response will therefore be generated each time the user changes a selection, but as long as that selection remains in effect the response will be retrieved from the cache, subject to the timeout value, of course.
Note that we ve defined scope= application in the tag, so response data will be cached at the application level. The application will therefore present the same set of selection-specific offers to every user of the site for the entire 30 seconds. In other words, every user who does a search for properties in a particular city or chooses a particular hotel will see the same offers during that 30-second period of time. Offers could also be cached on a per-user basis using the session scope, but placing cached information in the session impacts performance if session persistence is employed and should be avoided if possible.
| Best Practice |
Avoid using
session
scope for response data cached using
wl:cache
tags. If session persistence is being employed in the application, the cached data will be treated like other session data and
|
Finally, the hyperlinks in the offers area invoke the controller object for this page, OffersAction , just like any other JSP page in the application. The controller retrieves the offer identifier from the request parameter, acquires a reference to the OfferBean entity bean, and uses the relationship get method on the OfferBean to determine the property to load in the request before forwarding to the property display page:
String offerId = request.getParameter(id); int id = Integer.parseInt(offerId); OfferLocal offer = (OfferLocal)Locator.getBean(OfferLocal, id); PropertyLocal prop = offer.getProperty(); ActionUserHelper.loadPropertyBean(request, prop);
That s it! We are now ready to proceed to the construction of the administration site components including pages for entering, updating, and deleting all of the information used by the bigrez.com user site.