| CONTENTS | PREV | NEXT | INDEX | J2EE BluePrints |
The sample application must reflect the state of a user's interaction with the application and the current values of persistent data in the user interface. Following the MVC architecture, this functionality is implemented within the controller. In the sample application, the controller is split between the Web tier and the EJB tier. In this section we will discuss the implementation of the controller for the shopping interaction in the sample application.
The controller is responsible for coordinating the model and view. As described in Section 10.2, the view depends on the controller for screen selection. The model depends on the controller for making state changes to the model. The controller must accept user gestures from the view, translate them into business events based on the behavior of the application, and process these events. The processing of an event involves invoking methods of the model to cause the desired state changes. Finally, the controller selects the screen shown in response to the request that was processed.
Since the controller must coordinate both the view and the data, it straddles the Web and EJB tiers. Some components of the controller are hosted by the Web tier and facilitate communication with the view, while others are hosted by the EJB tier and control the model.
In the Web tier, the controller consists of several components:
Main.jspreceives and processes HTTP requests.Main.jspcallsScreenFlowManager, which is responsible for selecting the next screen to be shown to the client after the completion of the current request.RequestProcessorprovides the glue in the Web tier for holding the application components together. It contains logic that needs to be executed for each request.RequestProcessorcollaborates with two classes:
| - | RequestToEventProcessor translates HTTP requests into business events that the rest of the application can operate on. Events are represented by the class eStoreEvent and its subclasses CatalogEvent, LoginEvent, AccountEvent, CartEvent, and OrderEvent. |
| - | ShoppingClientControllerWebImpl (SCCWI) provides a Web-tier proxy for ShoppingClientController. It delegates all methods to its EJB tier counterpart. |
In the EJB tier, the controller is ShoppingClientController (SCC), which provides the view with read-only access to the model and handles business events. ShoppingClientController collaborates with StateMachine, an object that controls the creation and removal of enterprise beans and handles events to modify those objects passed to it by the controller in the Web tier.
Figure 10.10 illustrates the interactions
that occur between the collaborating controller objects when an HTTP request
is handled. The servlet generated from Main.jsp receives all HTTP
requests. It passes the request to RequestProcessor, which coordinates
all handling of the request. RequestProcessor uses RequestToEventTranslator
to translate the HTTP request into a business event. RequestProcessor
then passes the event to ShoppingClientControllerWebImpl, which
forwards the event to ShoppingClientController, the controller
in the EJB tier. ShoppingClientController delegates the handling
of the business event to StateMachine. StateMachine
changes the state of the model in response to the business event or command
and then retrieves a list of model objects that have changed as a result of
handling the business event from ModelUpdateManager (MUM).
Finally, RequestProcessor notifies all registered views of model
changes.
Figure 10.11 shows what happens
after RequestProcessor.processRequest returns. Main.jsp
forwards the initial request to template.jsp. The template includes
ScreenDefinitions.jsp, which uses ScreenFlowManager
to map the screen to a JSP page.
In the following sections, we will discuss the implementation of each of these components in more detail.
A front component is a component to which all requests for application URLs are delivered. The front component Main.jsp, processes these requests and delegates the generation of the response to the template page.
Code Example 10.11 shows Main.jsp.
The highlighted lines in the example indicate these two steps. Main.jsp
delegates all of the request processing tasks to RequestProcessor.
The response is generated by forwarding to template.jsp. An interesting
detail to note here is that Main.jsp stores references to the request
processor and other session-specific beans in the HTTP session object.
<jsp:useBean id="modelManager"
class="com.sun.estore.control.Web.ModelManager"
scope="session">
<% modelManager.init(config.getServletContext(), session); %>
</jsp:useBean>
<jsp:useBean id="rp"
class="com.sun.estore.control.Web.RequestProcessor"
scope="session">
<% rp.init(config.getServletContext(), session); %>
</jsp:useBean>
<%
try {
rp.processRequest(request);
request.setAttribute("selectedURL" , request.getPathInfo());
} catch (MissingFormDataException mi){
request.setAttribute("missingformdata", mi);
request.setAttribute("selectedURL", "/missingformdata");
} catch (DuplicateAccountException du){
request.setAttribute("selectedURL", "/duplicateaccount");
}
getServletConfig().getServletContext()
.getRequestDispatcher("/template.jsp")
.forward(request, response);
%>
Code Example 10.11 Main.jsp
|
RequestProcessor contains logic that gets executed for each request. For example, when a customer tries to access a feature that requires signin, RequestProcessor checks to detect whether the customer is logged in.
Code Example 10.12 presents an excerpt
from RequestProcessor, simplified to illustrate the key aspects
of its behavior.
public class RequestProcessor {
private ShoppingClientControllerWebImpl scc;
private ModelManager mm;
private ModelUpdateNotifier mun;
private RequestToEventTranslator eventTranslator;
private SecurityAdapter securityAdapter;
public void init(...) {
mm = (ModelManager)session.getAttribute("modelManager");
mun = mm;
scc = new ShoppingClientControllerWebImpl(session);
eventTranslator =
new RequestToEventTranslator(this, mm);
...
}
public void processRequest(HttpServletRequest req) {
checkForWebServerLogin(req);
EStoreEvent event = eventTranslator.processRequest(req);
if (event != null) {
Collection updatedModelList = scc.handleEvent(event);
mun.notifyListeners(updatedModelList);
}
...
}
}
Code Example 10.12 RequestProcessor
|
This excerpt demonstrates the core responsibilities of RequestProcessor including:
- Initializing the client session.
RequestProcessorinstantiates an object that implementsShoppingClientControllerand related application objects when a new session is initiated.- Detecting when the user logs into the server using form-based authentication and generating a login business event when this happens.
- Computing the business event to generate based on the
HttpRequestthat came in, with the help of theRequestToEventTranslator.- Raising a business event by invoking
handleEventon theShoppingClientController's Web implementation.- Gathering the outcome of the event processing. In particular,
RequestProcessorpasses the business event and its outcome to theModelManagerso the model change notifications can be processed by the view components (see Section 10.6.8).
RequestToEventTranslator is responsible for taking an HTTP-specific request and converting it into a business event that is not tied to the specifics of the HTTP protocol.
Application objects that include HTTP-specific functionality are not easy to reuse. By removing HTTP protocol-specific details from the request as early as possible, by turning it into a business event, the sample application ensures that all components that deal with business events would be completely reusable with non-HTTP clients. For example, the StateMachine that implements command processing logic for the sample application could be easily used as-is by a stand-alone Java client.
The two standard HTTP requests that can be processed by the translator are GET and POST. It is relatively straightforward to map GET requests to business events. However, POST requests, which represent form submission in the sample application, require the request processor to validate the form data as part of generating the business event. The processor needs to keep track of the values entered in the form so that the presentation screen can show where the error occurred when the form data is invalid. When the form data is valid, the processor must encapsulate the form parameters in an application-specific business event.
Code Example 10.13 presents excerpts
from RequestToEventTranslator. The highlighted lines indicate where
the translator parses HTTP request parameters and converts them to objects to
be used in business events.
public class RequestToEventTranslator {
private ModelManager mm;
public EStoreEvent processRequest(HttpServletRequest req)
throws EStoreEventException, MissingFormDataException {
String selectedUrl = req.getPathInfo();
EStoreEvent event = null;
if (selectedUrl.equals(ScreenNames.CATALOG_URL)) {
event = createCatalogEvent(req);
} else if (selectedUrl.equals(ScreenNames.CART_URL)) {
mm.getCartModel();
event = createCartEvent(req);
} else if ...
return event;
}
private EStoreEvent createCatalogEvent(HttpServletRequest req) {
CatalogEvent event = null;
String[] category = req.getParameterValues(CATEGORY_ID );
if (category != null) {
event = new CatalogEvent(
CatalogEvent.BROWSING_EVENT, category[0]);
}
return event;
}
private CartEvent createCartEvent(HttpServletRequest request) {
String action = request.getParameter("action");
if (action.equals("purchaseItem")) {
return createPurchaseItemEvent(request);
} else if (action.equals("removeItem")) {
return createRemoveItemEvent(request);
} else if (action.equals("updateCart")) {
return createUpdateCartEvent(request);
}
}
}
Code Example 10.13 RequestToEventTranslator
|
ShoppingClientControllerWebImpl is a proxy object that calls methods on the EJB tier controller ShoppingClientController. ShoppingClientControllerWebImpl exposes a read-only interface to the model, so that the view can render the model as needed. Keeping this interface read-only minimizes dependencies between the view and the model, to prevent inadvertent modification of the model by the view outside the scope of the business rules encapsulated in the application.
Code Example 10.14 contains an excerpt
from ShoppingClientControllerWebImpl. Notice that all the methods
of ShoppingClientController are synchronized so that concurrent
requests to ShoppingClientController are serialized. This is done
because an EJB container will throw an exception if a request is made to a session
bean while it is servicing another request.
public class ShoppingClientControllerWebImpl
{
private com....ejb.ShoppingClientController sccEjb;
private HttpSession session;
public ShoppingClientControllerWebImpl(HttpSession session) {
this.session = session;
ModelManager mm =
(ModelManager)session.getAttribute("modelManager");
sccEjb = mm.getSCCEJB();
}
public synchronized AccountModel getAccount() {
return sccEjb.getAccount().getDetails();
}
...
public synchronized Collection handleEvent(EStoreEvent ese) {
return sccEjb.handleEvent(ese);
}
public synchronized void remove() {
sccEjb.remove();
}
}
Code Example 10.14 ShoppingClientControllerWebImpl
|
ShoppingClientController manages the life cycle of model objects
such as the shopping cart and account enterprise beans and processes business
events. It delegates the processing of business events in the handleEvent
method to StateMachine. ShoppingClientController is
also responsible for the life cycle of StateMachine. ShoppingClientController
is implemented by ShoppingClientControllerEJB, illustrated in Code
Example 10.15.
public class ShoppingClientControllerEJB implements SessionBean {
private StateMachine sm;
private ShoppingCart cart;
String userId;
Account acct;
public Account getAccount() {
if (acct == null) {
createAccountEJB();
}
return acct;
}
public ShoppingCart getShoppingCart() {
if (cart == null) {
try {
ShoppingCartHome cartHome =
EJBUtil.getShoppingCartHome();
cart = cartHome.create();
} catch (CreateException ce) {
throw new EJBException(ce);
}
}
return cart;
}
public void ejbCreate() {
sm = new StateMachine(this);
}
public Collection getOrders() throws FinderException {
Collection orders = null;
if (userId != null) {
OrderHome home = EJBUtil.getOrderHome();
orders = home.findUserOrders(userId);
}
return orders;
}
public Collection handleEvent(EStoreEvent ese)
throws EStoreEventException {
try {
return (sm.handleEvent(ese));
} catch (RemoteException re) {
throw new EJBException (re);
}
}
}
Code Example 10.15 ShoppingClientControllerEJB
|
StateMachine implements the core command processing business logic
of the application. It is responsible for changing the state of the models in
response to a business event or command. StateMachine consists
of methods that handle each of the different business events that the sample
application can respond to. One such method is highlighted in Code
Example 10.16.
public class StateMachine {
private ShoppingClientControllerEJB sccejb;
private ModelUpdateManager mum;
private HashMap orderTable;
public StateMachine(ShoppingClientControllerEJB sccejb) {
this.sccejb = sccejb;
this.mum = new ModelUpdateManager();
}
public Collection handleEvent(EStoreEvent ese)
throws RemoteException, EStoreEventException {
if (ese instanceof CartEvent) {
handleCartEvent((CartEvent)ese);
} else if (ese instanceof AccountEvent) {
handleAccountEvent((AccountEvent)ese);
} else if (ese instanceof OrderEvent) {
handleOrderEvent((OrderEvent)ese);
} else if (ese instanceof LoginEvent) {
login((LoginEvent)ese);
} else if (ese instanceof LogoutEvent) {
logout();
}
return (mum.getUpdatedModels(ese));
}
private void handleCartEvent(CartEvent ce)
throws RemoteException {
ShoppingCart cart = sccejb.getShoppingCart();
switch (ce.getActionType()) {
...
case CartEvent.UPDATE_ITEM :{
Collection itemIds = ce.getItemIds();
Iterator it = itemIds.iterator();
while (it.hasNext()){
String itemId = (String)it.next();
int quantity = ce.getItemQty(itemId);
if (quantity > 0){
cart.updateItemQty(itemId, quantity);
} else {
cart.deleteItem(itemId);
}
}
}
break;
}
}
...
}
Code Example 10.16 StateMachine
|
StateMachine has both read and write access to all of the model
objects so that it can coordinate event processing across multiple model objects.
For example, when StateMachine handles an order event, it interacts
with the inventory bean to debit the quantity of the purchased item, the order
bean to insert the order details, and the mailer bean to send confirmation email
to the user. These functions are performed by the method illustrated in Code
Example 10.17. Highlighted lines indicate where enterprise beans are retrieved
or created.
private Order createOrder(OrderEvent oe) throws RemoteException {
ShoppingCart cart = sccejb.getShoppingCart();
Order order = null;
String userId = sccejb.getAccount().getDetails().getUserId();
try {
InventoryHome inventHome = EJBUtil.getInventoryHome();
Iterator ci = ((ShoppingCartModel)cart.getDetails()).
getItems();
ArrayList lineItems = new ArrayList();
int lineNo = 0;
double total = 0;
while (ci.hasNext()) {
lineNo++;
CartItem cartItem = (CartItem) ci.next();
LineItem li = new LineItem(cartItem.getItemId(),
cartItem.getQuantity(),cartItem.getUnitCost(),
lineNo);
lineItems.add(li);
total += cartItem.getUnitCost() * cartItem.getQuantity();
}
for (Iterator it = lineItems.iterator(); it.hasNext();){
LineItem LI = (LineItem)it.next();
Inventory inventRef =
inventHome.findByPrimaryKey(LI.getItemNo());
inventRef.updateQuantity(LI.getQty());
}
OrderHome home = EJBUtil.getOrderHome();
order = home.create(lineItems,
oe.getShippingAddress(),
oe.getBillingAddress(),
...
total);
// put the requestId and the orderId in a table to match up later
if (orderTable == null) orderTable = new HashMap();
orderTable.put(oe.getRequestId() + "",
order.getDetails().getOrderId() +"");
// empty shopping cart
cart.empty();
if (JNDIUtil.sendConfirmationMail()) {
// send order confirmation mail.
Mailer mailer = EJBUtil.createMailerEJB();
mailer.sendOrderConfirmationMail(order.getDetails().
getOrderId());
}
} catch (DuplicateKeyException dke) {
...
} catch (CreateException ce) {
throw new EJBException(ce);
} catch (FinderException fe) {
throw new EJBException(fe);
}
return order;
}
Code Example 10.17 StateMachine.createOrder
|
ScreenFlowManager is responsible for selecting a screen to present to the user as the outcome of their request. The mapping from a requested URL to a response screen is not one to one. In fact, the response that depends not only on the request itself, but also on the state of the application data model and the outcome of request processing within the application. In other words, the flow manager keeps a state machine that captures the flow of screens in the application. ScreenFlowManager looks at the request and the state of the model and computes the screen to be returned.
Code Example 10.18 shows how ScreenFlowManager
maps many of the request URLs directly into response screens. For some of the
requests, such as the VALIDATE_BILLING_INFO_URL, the code inspects
the model to decide which of two possible screens to present.
public class ScreenFlowManager {
public int getCurrentScreen(HttpServletRequest req) {
String selectedUrl =
(String)req.getAttribute("selectedURL");
int nextScreen = ScreenNames.DEFAULT_SCREEN;
if (selectedUrl == null) {
// do nothing. show the default screen.
} else if (selectedUrl.equals(ScreenNames.CATALOG_URL)) {
nextScreen = ScreenNames.CATALOG_SCREEN;
...
} else if (selectedUrl.equals(
ScreenNames.VALIDATE_BILLING_INFORMATION_URL)) {
if (req.getSession()
.getAttribute("shippingAddressRequired") != null) {
boolean addrReqd = req.getSession()
.getAttribute("addrReqd").equals("true");
if (addrReqd)
nextScreen = ScreenNames.ENTER_SHIPPING_INFO;
else
nextScreen = ScreenNames.CONFIRM_SHIPPING_INFO;
}
}
return nextScreen;
}
}
Code Example 10.18 ScreenFlowManager
|
Following the MVC architecture, views implemented by JSP pages and JavaBeans components present data owned by their associated models implemented as enterprise beans. In the sample application, each Web-tier JavaBeans component serves as the view, with corresponding EJB-tier classes representing the model. Whenever a model changes, it notifies interested views so that the views can update its presentation of the model.
In the sample application, the notification process is managed by ModelUpdateManager and ModelManager. ModelUpdateManager is responsible for converting a business event, such as AccountEvent, to a list of names of models that have changed due to this event. ModelManager uses this list to notify all views that have registered interest in the changed models to fetch the models' data.
The functions of ModelManager and ModelUpdateManager and their interactions with controller objects are described in the following sections.
10.6.8.1 Model Manager
ModelManager extends ModelUpdateNotifier, which provides methods for adding listeners of model change events and causing listeners (that is, views) to perform an update when a change event is received. ModelManager adds methods that create and return instances of view classes.
Code Example 10.19 presents excerpts
from ModelManager. Note that ModelManager maintains
references to both a ServletContext and an HttpSession.
These objects in turn contain references to view objects (highlighted in the
example). View objects specific to a client (for example, AccountModel)
are maintained by an HTTP session object. View objects that can be shared by
all clients (for example, CatalogModel) are maintained as an attribute
of the servlet generated from Main.jsp.
public class ModelManager extends ModelUpdateNotifier {
private ServletContext context;
private HttpSession session;
private ShoppingClientController sccEjb = null;
private ShoppingCart cartEjb = null;
private Account acctEjb = null;
public void init(ServletContext context,
HttpSession session) {
this.session = session;
this.context = context;
getAccountModel();
}
public CatalogModel getCatalogModel() {
CatalogModel catalog = (CatalogModel)
context.getAttribute(WebKeys.CatalogModelKey);
if (catalog == null) {
catalog = new CatalogWebImpl();
context.setAttribute(WebKeys.CatalogModelKey, catalog);
}
return catalog;
}
public AccountModel getAccountModel() {
AccountModel acct = (AccountModel)
session.getAttribute(WebKeys.AccountModelKey);
if (acct == null) {
acct = new AccountWebImpl(this);
session.setAttribute(WebKeys.AccountModelKey, acct);
}
return acct;
}
...
}
Code Example 10.19 ModelManager
|
10.6.8.2 ModelUpdateManager
ModelUpdateManager is responsible for converting an EStore
event to a list of names of models that have changed due to this event. Code
Example 10.20 presents excerpts from ModelUpdateManager.
public class ModelUpdateManager {
...
public Collection getUpdatedModels(EStoreEvent ese)
throws RemoteException {
ArrayList modelList = new ArrayList();
if (ese instanceof CartEvent) {
modelList.add(JNDINames.CART_EJBHOME);
} else if (ese instanceof AccountEvent) {
modelList.add(JNDINames.ACCOUNT_EJBHOME);
} else if (ese instanceof OrderEvent) {
modelList.add(JNDINames.ORDER_EJBHOME);
modelList.add(JNDINames.INVENTORY_EJBHOME);
modelList.add(JNDINames.CART_EJBHOME);
} else if (ese instanceof LoginEvent) {
modelList.add(JNDINames.ACCOUNT_EJBHOME);
}
return modelList;
}
}
Code Example 10.20 ModelUpdateManager
|