Programming Jakarta Struts, 2nd Edition [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Programming Jakarta Struts, 2nd Edition [Electronic resources] - نسخه متنی

Chuck Cavaness

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید








6.4 What Does Struts Offer for the Model?


To be honest, the Struts framework doesn't offer
much in the way of building model components, but this probably is as
it should be. Many frameworks and component models are already
available for dealing with the business domain of an application,
including Enterprise JavaBeans and Java Data Objects (JDO), or you
can use regular JavaBeans and an ORM. The good news is that the
Struts framework does not limit you to one particular model
implementation. This chapter will present one approach. In Chapter 13, we'll take a completely
different approach and see how the framework is affected by this
change.


6.4.1 Building the Storefront Model


After all this discussion of what constitutes a model for a Struts
application, it's finally time to apply the
previously discussed concepts using the Storefront application as the
business domain. Obviously, the Storefront is a fictitious example
and doesn't represent a complete model for what a
"real" e-commerce application would
need to support. However, it does provide enough of an object model
for you to understand the semantics of this chapter.


6.4.2 Accessing a Relational Database


The state of the Storefront application will be persisted using a
relational database. This is, in fact, how it would be done if
Storefront were a real application. Of course, an ERP system often is
used in conjunction with the relational database, but many e-commerce
applications use a relational database closer to the frontend for
performance and ease of development. When both are deployed in an
enterprise, there's usually a middleware service to
keep the data between the two synchronized, either in real time or
using batch mode.

As you probably are aware, there are many relational databases to
choose from. You can choose one of several major database vendors or,
if your requirements don't call for such a large and
expensive implementation, you can choose one of the cheaper or free
products on the market. Because we will not be building out every
aspect of the application and our intended user load is small, our
requirements for a database are not very stringent. That said, the
database-specific examples in this chapter should be fine for most
database platforms. If you understand the SQL Data Definition
Language (DDL), you can tweak the DDL for the database
that's available to you.

We have quite a bit of work to do before we can start using the
Storefront model. The following tasks need to be completed before we
are even ready to involve the Struts framework:

  • Create the business objects for the Storefront application

  • Create the database for the Storefront application

  • Map the business objects to the database

  • Test that the business objects can be persisted in the database


As you can see, none of these tasks mentions the Struts framework.
You should approach this part of the development phase without a
particular client in mind. The Struts Storefront web application is
just one potential type of client to the business objects. If
designed and coded properly, many different types may be used. The
business objects are used to query and persist information regarding
the Storefront business. They should not be coupled to a presentation
client.

To help insulate the Struts framework from changes that may occur in
the business objects, we also will look at using the
Business Delegate design pattern within the
Storefront application. The business delegate acts as a client-side
business abstraction. It hides the implementation of the actual
business service, which helps to reduce the coupling between the
client and the business objects.


6.4.3 Creating the Storefront Business Objects


Business objects contain data
and behavior. They are a virtual representation of one or more
records within a database. In the Storefront application, for
example, an OrderBO object represents a physical
purchase order placed by a customer. It also contains the business
logic that helps to ensure that the data is valid and remains valid.


Where Does Business Validation Belong?


Deciding where to put your validation
logic in a Struts application can be frustrating. On the one hand, it
seems to belong within the framework itself, as this is the first
place that the user data can be obtained and validated. The problem
with placing business-logic validation within the
Action or ActionForm class is
that the validation then becomes coupled to the Struts framework,
which prevents the validation logic from being reused by any other
clients.

There is a different type of validation, called
presentation
validation
, that can and should occur within the
framework. Presentation validation, or "input
validation," as it's sometime
called, can be grouped into three distinct categories:

  • Lexical

  • Syntactic

  • Semantic


Lexical
validation
checks to make sure data is well formed. For
example, is the quantity value an integer?
Syntactic
validation
goes one step further and makes sure that
values made from a composite are valid and well formed. For example,
date fields in a browser typically are accepted as month/day/year
values. Syntactic validation ensures that the value entered is in the
proper format. However, it doesn't ensure that the
values make a valid date. Ensuring that the date entered is valid and
meaningful is the job of semantic
validation
, which ensures that the values entered have
meaning for the application. For example, putting a quantity value of
-3 in the order quantity field for an item is
lexically and syntactically valid but not semantically valid.

Presentation validation belongs within the Struts framework, but
business validation does not. The business objects have the final
responsibility of ensuring that any data inserted into the database
is valid, and therefore it should have the rules necessary to perform
this duty.

The first step is to create the business objects with which
we'll need to interact. For this implementation,
they will just be regular JavaBean objects. Many component models are
specific to a single implementation. Entity beans, for example, will
work only within an EJB container. For this example, the Storefront
business objects will not be specific to a particular implementation.
If later we want to use these same business objects with an EJB
container, we can wrap them with entity beans or just delegate the
call from a session bean method to one of these objects. In Chapter 13, we'll show how this can
be done without impacting the Storefront application.

Because all the business objects share several common properties, we
are going to create an abstract superclass for the business objects.
Every business object will be a subclass of the
BaseBusinessObject
class shown in Example 6-1.


Example 6-1. BaseBusinessObject is the superclass for all business objects

package com.oreilly.struts.storefront.businessobjects;
/**
* An abstract superclass that many business objects will extend.
*/
abstract public class BaseBusinessObject implements java.io.Serializable {
private Integer id;
private String displayLabel;
private String description;
public Integer getId( ) {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setDescription(String description) {
this.description = description;
}
public String getDescription( ) {
return description;
}
public void setDisplayLabel(String displayLabel) {
this.displayLabel = displayLabel;
}
public String getDisplayLabel( ) {
return displayLabel;
}
}

The BaseBusinessObject prevents each business
object from needing to declare these common properties. We also can
put common business logic here if the opportunity presents itself.

Example 6-2 shows the OrderBO business object
that represents a customer purchase order in the Storefront
application. There's nothing that special about the
OrderBO class; it's an ordinary
JavaBean object. Other than the recalculatePrice(
)
method, the class just provides setter and getter methods
for the order properties.


Example 6-2. The OrderBO object represents an order placed by a customer

package com.oreilly.struts.storefront.businessobjects;
import java.sql.Timestamp;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
/**
* The OrderBO, which represents a purchase order that a customer
* has placed or is about to place.
*/
public class OrderBO extends BaseBusinessObject{
// A list of line items for the order
private List lineItems = new LinkedList( );
// The customer who placed the order
private CustomerBO customer;
// The current price of the order
private double totalPrice;
// The id of the customer
private Integer customerId;
// Whether the order is in process, shipped, canceled, etc.
private String orderStatus;
// The date and time that the order was received
private Timestamp submittedDate;
public OrderBO( Integer id, Integer custId, String orderStatus,
Timestamp submittedDate, double totalPrice ){
this.setId(id);
this.setCustomerId(custId);
this.setOrderStatus(orderStatus);
this.setSubmittedDate(submittedDate);
this.setTotalPrice(totalPrice);
}
public void setCustomer( CustomerBO owner ){
customer = owner;
}
public CustomerBO getCustomer( ){
return customer;
}
public double getTotalPrice( ){
return this.totalPrice;
}
private void setTotalPrice( double price ){
this.totalPrice = price;
}
public void setLineItems( List lineItems ){
this.lineItems = lineItems;
}
public List getLineItems( ){
return lineItems;
}
public void addLineItem( LineItemBO lineItem ){
lineItems.add( lineItem );
}
public void removeLineItem( LineItemBO lineItem ){
lineItems.remove( lineItem );
}
public void setCustomerId(Integer customerId) {
this.customerId = customerId;
}
public Integer getCustomerId( ) {
return customerId;
}
public void setOrderStatus(String orderStatus) {
this.orderStatus = orderStatus;
}
public String getOrderStatus( ) {
return orderStatus;
}
public void setSubmittedDate(Timestamp submittedDate) {
this.submittedDate = submittedDate;
}
public Timestamp getSubmittedDate( ) {
return submittedDate;
}
private void recalculatePrice( ){
double totalPrice = 0.0;
if ( getLineItems( ) != null ){
Iterator iter = getLineItems( ).iterator( );
while( iter.hasNext( ) ){
// Get the price for the next line item and make sure it's not null
Double lineItemPrice = ((LineItemBO)iter.next( )).getUnitPrice( );
// Check for an invalid lineItem. If found, return null right here.
if (lineItemPrice != null){
totalPrice += lineItemPrice.doubleValue( );
}
}
// Set the price for the order from the calcualted value
setTotalPrice( totalPrice );
}
}
}

We won't show all of the business objects here; they
all have similar implementations to the OrderBO
class.


When designing your business objects, don't worry
about how they will be mapped to the database. There will be plenty
of time for that. Don't be afraid to use
object-oriented techniques such as inheritance and polymorphism, just
as you would with any other object model. The
BaseBusinessObject in Example 6-1
will not actually be mapped to a table in the database, but its
properties will get mapped with the respective subclasses. Although
most persistence mapping frameworks support multiple approaches to
mapping inheritance in the database, adding the properties to each
table allows fewer SQL joins to occur, which may have a positive
impact on performance.


6.4.4 The Storefront Data Model


Once all the business objects have been created for the Storefront
application, we need to create a database model
and schema. The details of creating a database schema for the
Storefront application are beyond the scope of this book.
It's seemingly easy to throw a bunch of tables into
a database and add columns to them. However, it's
quite another thing to understand the trade-offs between database
normalization and issues that surface due to the object-relational
mismatch discussed earlier.

If the application is small enough, almost anyone can create a
database schema, especially with the tools available from database
vendors and third-party sources. If your schema is more than just a
few tables, or if the complexity of foreign keys, triggers, and
indexes is high, it's best to leave creating a
schema to the experts. The Storefront schema is quite small, mainly
because we've chosen to implement only a portion of
what normally would be required. Figure 6-4 shows
the data model that will be implemented for the Storefront
application.


Figure 6-4. The Storefront data model

The table definitions in Figure 6-4 are fairly
self-explanatory. There are several items of interest that should be
pointed out, however. The first is that every table, except for the
many-to-many link table CATALOGITEM_LNK, has been
assigned an object identifier
(OID). OIDs simplify the navigation between objects, and should have
no business meaning at all. Values that are based on business
semantics will sooner or later change, and basing your keys on values
that change is very problematic. In the database world, using the OID
strategy is known as using surrogate keys.

To generate the schema for the data model shown in Figure 6-4, we need to create the DDL. The SQL DDL is used to create the physical
entities in the database. The Storefront SQL DDL that will create the
set of tables in Figure 6-4 is shown in Example 6-3.


Example 6-3. The Storefront SQL DDL

DROP DATABASE storefront;
CREATE DATABASE storefront;
use storefront;
CREATE TABLE CATALOG(
id int NOT NULL,
displaylabel varchar(50) NOT NULL,
featuredcatalog char(1) NULL,
description varchar(255) NULL
);
ALTER TABLE CATALOG ADD
CONSTRAINT PK_CATALOG PRIMARY KEY(id);
CREATE TABLE CUSTOMER (
id int NOT NULL,
firstname varchar(50) NOT NULL,
lastname varchar(50) NOT NULL,
email varchar(50) NOT NULL,
password varchar(15) NOT NULL,
description varchar(255) NULL,
creditStatus char(1) NULL,
accountstatus char(1) NULL,
accountnumber varchar(15) NOT NULL
);
ALTER TABLE CUSTOMER ADD
CONSTRAINT PK_CUSTOMER PRIMARY KEY(id);
CREATE TABLE ITEM (
id int NOT NULL,
itemnumber varchar (255) NOT NULL,
displaylabel varchar(50) NOT NULL,
description varchar (255) NULL,
baseprice decimal(9,2) NOT NULL,
manufacturer varchar (255) NOT NULL,
sku varchar (255) NOT NULL,
upc varchar (255) NOT NULL,
minsellingunits int NOT NULL,
sellinguom varchar (255) NOT NULL,
onhandquantity int NOT NULL,
featuredesc1 varchar (255) NULL,
featuredesc2 varchar (255) NULL,
featuredesc3 varchar (255) NULL,
smallimageurl varchar (255) NULL,
largeimageurl varchar (255) NULL
)
ALTER TABLE ITEM ADD
CONSTRAINT PK_ITEM PRIMARY KEY(id);
CREATE TABLE CATALOGITEM_LNK(
catalogid int NOT NULL,
itemid int NOT NULL
)
ALTER TABLE CATALOGITEM_LNK ADD
CONSTRAINT PK_CATALOGITEM_LNK PRIMARY KEY(catalogid, itemid);
ALTER TABLE CATALOGITEM_LNK ADD
CONSTRAINT FK_CATALOGITEM_LNK_CATALOG FOREIGN KEY
(catalogid) REFERENCES CATALOG(id);
ALTER TABLE CATALOGITEM_LNK ADD
CONSTRAINT FK_CATALOGITEM_LNK_ITEM FOREIGN KEY
(itemid) REFERENCES ITEM(id);
CREATE TABLE PURCHASEORDER (
id int NOT NULL,
customerid int NOT NULL,
submitdttm timestamp NOT NULL,
status varchar (15) NOT NULL,
totalprice decimal(9,2) NOT NULL,
)
ALTER TABLE PURCHASEORDER ADD
CONSTRAINT PK_PURCHASEORDER PRIMARY KEY(id);
ALTER TABLE PURCHASEORDER ADD
CONSTRAINT FK_PURCHASEORDER_CUSTOMER FOREIGN KEY
(customerid) REFERENCES CUSTOMER(id);
CREATE TABLE LINEITEM (
id int NOT NULL,
orderid int NOT NULL,
itemid int NOT NULL,
lineitemnumber int NULL,
extendedprice decimal(9, 2) NOT NULL,
baseprice decimal(9, 2) NOT NULL,
quantity int NOT NULL
)
ALTER TABLE LINEITEM ADD
CONSTRAINT PK_LINEITEM PRIMARY KEY(id);
ALTER TABLE LINEITEM ADD
CONSTRAINT FK_LINEITEM_ORDER FOREIGN KEY
(orderid) REFERENCES PURCHASEORDER(id);
ALTER TABLE LINEITEM ADD
CONSTRAINT FK_LINEITEM_ITEM FOREIGN KEY
(itemid) REFERENCES ITEM(id);


The DDL shown in Example 6-3 has been tested on
Microsoft SQL Server 2000. If you plan to use it with other database
platforms, it might be necessary to modify the
ALTER statements. For example, due to the
limitations of foreign keys with MySQL, you may have to eliminate the
FOREIGN KEY statements
entirely. The only parts that are absolutely necessary to run the
example are the CREATE TABLE
sections and the primary keys, which all databases should support. It
might also be necessary to execute the first couple of statements one
at a time until the database is created and then execute the
CREATE TABLE
statements.

Once you have executed the DDL from Example 6-3, you
will need to insert some data for the tables. The data must be in the
database for the Storefront application to work properly. You can
either use the administrative tools to enter data for the application
or execute INSERT statements in the same manner as you did when
creating the tables. As an example, to insert catalog data, you can
execute the following statements:

INSERT INTO CATALOG(id, displaylabel) VALUES(1,'Import');
INSERT INTO CATALOG(id, displaylabel) VALUES(2,'Domestic');
INSERT INTO CATALOG(id, displaylabel) VALUES(3,'Lager');
INSERT INTO CATALOG(id, displaylabel) VALUES(4,'Light');
INSERT INTO CATALOG(id, displaylabel) VALUES(5,'Import');
INSERT INTO CATALOG(id, displaylabel) VALUES(6,'Malt Liquor');


6.4.5 Mapping the Business Objects to the Database


When
it comes time to connect or map the business objects to the database,
there are a variety of approaches from which you can choose. Your
choice depends on several factors that may change from application to
application and situation to situation. A few of the approaches are:

  • Use straight JDBC calls

  • Use a "home-grown" ORM approach
    (a.k.a. the "roll-your-own"
    approach)

  • Use a proprietary ORM framework

  • Use a nonintrusive, nonproprietary ORM framework

  • Use an object database


Keeping in mind that some tasks are better done in-house and others
are better left to the experts, building a Java persistence mechanism
is one that typically you should avoid doing. Remember that the point
of building an application is to solve a business problem. You
generally are better off acquiring a
persistence
solution from a third party.

There are many issues that must be dealt with that are more
complicated than just issuing a SQL select statement through JDBC,
including transactions, support for the various associations, virtual
proxies or indirection, locking strategies, primary-key increments,
caching, and connection pooling, just to name a few. Building a
persistence framework is an entire project in and of itself. You
shouldn't be spending valuable time and resources on
something that isn't the core business. The next
section lists several solutions that are available.


6.4.6 Object-to-Relational Mapping Frameworks


There are a large number of ORM
products available for you to choose from. Some of them are
commercially available and have a cost that is near to or exceeds
that of most application servers. Others are free and open source.
Table 6-1 presents several commercial and
noncommercial solutions that you can choose from.

Table 6-1. Object-to-relational mapping frameworks

Product


URL


TopLink


http://otn.oracle.com/products/ias/toplink/indexl


CocoBase


http://www.cocobase.com


Torque


http://db.apache.org/torque/indexl


Hibernate


http://www.hibernate.org


ObJectRelationalBridge


http://db.apache.org/ojb


FrontierSuite


http://www.objectfrontier.com


Castor


http://castor.exolab.org


FreeFORM


http://chimu.com/projects/form


Expresso


http://www.jcorporate.com


JRelationalFramework


http://jrf.sourceforge.net


VBSF


http://www.objectmatter.com


JGrinder


http://sourceforge.net/projects/jgrinder

Although Table 6-1 is not an exhaustive list of
available products, it does present many solutions to choose from.
Regardless of whether you select a commercial or noncommercial
product, you should make sure that the mapping framework
implementation does not "creep"
into your application. Recall from Figure 6-1 that
dependencies always should go down the layers, and there should not
be a top layer depending on the persistence framework.
It's even advantageous to keep the business objects
ignorant about how they are being persisted. Some persistence
frameworks force you to import their classes and interfaces, but this
is problematic if you ever need to change your persistence mechanism.
Later in this chapter, you'll see how you can use
the Business Delegate pattern and the
Data Access Object (DAO) pattern
to limit the intrusion of the persistence framework.


You can find a complete list of ORM product vendors at http://www.service-architecture.com/products/object-relational_mappingl.

Another thing to be careful of is that a few of the persistence
frameworks need to alter the Java bytecode of the business objects
after they are compiled. Depending on how you feel about this, it
could introduce some issues. Just make sure you fully understand how
a persistence framework needs to interact with your application
before investing time and resources into using it.


6.4.7 The Storefront Persistence Framework


We could have
chosen almost any solution from Table 6-1 and
successfully mapped the Storefront business objects to the database.
Our requirements are not that stringent, and the model
isn't that complicated. We evaluated several
options, but our selection process was very informal and quick, an
approach you should not follow for any serious project. The criteria
that the frameworks were judged against were:

  • The cost of the solution

  • The amount of intrusion the persistence mechanism needed

  • How good the available documentation was


Cost was a big factor. We needed a solution that you could use to
follow along with the examples in this book without incurring any
monetary cost. All of the solutions evaluated for this example
performed pretty well and were relatively easy to use, but we finally
selected the open source Hibernate product to use for the Storefront
example. We could have just as easy used ObJectRelationalBridge
(OJB), which is another very popular open source ORM framework.


Just because this solution was chosen for this example,
don't assume that it will be the best solution for
your application. Take the necessary time and evaluate the products
based on your specific criteria.

The documentation for Hibernate is pretty good, considering that
documentation for open source projects tends to be one of the last
tasks completed. Essentially, the mapping of the business objects to the
database tables takes place within XML files.
The files are parsed by the mapping framework at runtime and used to
execute SQL to the database. The portion of the mapping file that
maps the customer business object is shown in Example 6-4.


At the time that this source was written, the newest version of
Hibernate was 2.1.2. For the latest Hibernate changes, go to
http://www.hibernate.org.


Example 6-4. The mapping XML for the CustomerBO class

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping package="com.oreilly.struts.storefront.businessobjects">
<class name="CustomerBO" table="CUSTOMER">
<id name="id">
<generator class="native"/>
</id>
<property name="firstName"/>
<property name="lastName"/>
<property name="password"/>
<property name="email"/>
<property name="accountStatus"/>
<property name="creditStatus"/>
<set name="submittedOrders" lazy="true">
<key column="customerid"/>
<one-to-many class="OrderBO"/>
</set>
</class>
</hibernate-mapping>

The rest of the mappings are mapped in a similar manner. Once all of
the mappings are specified, you must configure the database
connection information to allow the JDBC driver to connect to the
correct database. With Hibernate, you configure the connection
information in the hibernate.cfg.xml
file. This is shown in Example 6-5.


Example 6-5. The respository.xml file contains the database connection information

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
<!-- a SessionFactory instance listed as /jndi/name -->
<session-factory name="java:comp/env/hibernate/SessionFactory">
<!-- properties -->
<property name="connection.datasource">datasource/storefront</property>
<property name="dialect">net.sf.hibernate.dialect.SQLServerDialect</property>
<property name="show_sql">false</property>
<property name="use_outer_join">true</property>
<property name="transaction.factory_class">
net.sf.hibernate.transaction.JTATransactionFactory
</property>
<property name="jta.UserTransaction">java:comp/UserTransaction</property>
<!-- mapping files -->
<mapping resource="customer.hbm.xml"/>
</session-factory>
</hibernate-configuration>

You need to configure the settings in this file for your specific
environment and all add all of the mappings for your
application
Example 6-5
adds only the mapping for the Customer
business object. That's really all there is to
configuring the persistence framework for your application. To
initialize the framework within your application, you simply call a
few initialization methods, as shown later in this section.


Hibernate offers a rich and user-friendly API that you can use to
create, query, update and delete your objects. You can read the
documentation and tutorials to get a complete understanding of the
API. The Storefront application will not need to scale for multiple
servers. This means that the persistence framework is running within
the same JVM as the Storefront application itself. This makes our
design much simplier.

There's not enough room in this chapter for a better
explanation of the Hibernate framework. For more detailed
information, review the documentation for the product at http://www.hibernate.org.
Don't forget that you will need to have an
appropriate database and a JDBC driver in your web
application's classpath.


6.4.8 The Business Delegate and DAO Patterns in Action


The final piece of the puzzle is to create a service interface that
the Storefront Action classes can use instead of
interacting with the persistence framework directly. Again, the idea
is to decouple the persistence from as much of the application as
possible. Before we show the details of how we are going to
accomplish this for the Storefront example, we need to briefly
discuss the Data Access Object (DAO) pattern.

The purpose of the DAO pattern is to decouple the business logic of
an application from the data access logic. When a persistence
framework is being used, the pattern should help to decouple the
business objects from that framework. A secondary goal is to allow
the persistence implementation to easily be replaced with another,
without negatively affecting the business objects.

There are actually two independent design patterns contained within
the DAOthe Bridge and the Adaptorboth of which are
structural design patterns explained in the Gang of
Four's Design Patterns: Elements of
Reusable Object-Oriented Software
(Addison Wesley).

For the Storefront application, we are going to combine the DAO and
Business Delegate patterns to insulate the Action
and business object classes from the persistence implementation. The
abstract approach is shown in Figure 6-5.


Figure 6-5. The Business Delegate and DAO patterns combined

The client object in Figure 6-5 represents the
Struts Action classes. They will acquire a
reference to a service interface, which is referred to in the diagram
as the Business Delegate Interface. The Storefront business interface is
shown in Example 6-6.


Example 6-6. The Storefront business interface

package com.oreilly.struts.storefront.service;
import java.util.List;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.catalog.view.ItemSummaryView;
import com.oreilly.struts.storefront.framework.exceptions.DatastoreException;
import com.oreilly.struts.storefront.framework.security.IAuthentication;
/**
* The business interface for the Storefront application. It defines all
* of the methods that a client may call on the Storefront application.
* This interface extends the IAuthentication interface to provide a
* single cohesive interface for the Storefront application.
*/
public interface IStorefrontService extends IAuthentication {
public List getFeaturedItems( ) throws DatastoreException;
public ItemDetailView getItemDetailView( String itemId )
throws DatastoreException;
}

The IStorefrontService interface in Example 6-6 defines all of the methods a client may call
on the Storefront application. In our case, the client will be the
set of Action classes in the Storefront
application. The IStorefrontService is designed so
that there is no web dependency. It's feasible that
other types of clients could use this same service.

The IStorefrontService
extends the IAuthentication class to encapsulate
the security methods. The IAuthentication
class, shown in Example 6-7, contains only two
methods for this simple example.


Example 6-7. The IAuthentication interface

package com.oreilly.struts.storefront.framework.security;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.exceptions.InvalidLoginException;
import com.oreilly.struts.storefront.framework.exceptions.ExpiredPasswordException;
import com.oreilly.struts.storefront.framework.exceptions.AccountLockedException;
import com.oreilly.struts.storefront.framework.exceptions.DatastoreException;
/**
* Defines the security methods for the system.
*/
public interface IAuthentication {
/**
* Log the user out of the system.
*/
public void logout(String email);
/**
* Authenticate the user's credentials and either return a UserView for the
* user or throw one of the security exceptions.
*/
public UserView authenticate(String email, String password)
throws InvalidLoginException, ExpiredPasswordException,
AccountLockedException, DatastoreException;
}

One implementation for the IStorefrontService
interface is shown in Example 6-8. The
implementation could be swapped out with other implementations, as
long as the new implementations also implement the
IStorefrontService interface. No clients would be
affected because they are programmed against the interface, not the
implementation.


You'll see an example of switching the
implementation of the IStorefrontService interface
in Chapter 13, when we substitute an EJB tier
into the Storefront application.


Example 6-8. The Storefront service implementation class

public class StorefrontServiceImpl implements IStorefrontService{
// The SessionFactory
SessionFactory sessionFactory = null;
/**
* Create the service, which includes initializing the persistence
* framework.
*/
public StorefrontServiceImpl( ) throws DatastoreException {
super( );
init( );
}
/**
* Return a list of items that are featured.
*/
public List getFeaturedItems( ) throws DatastoreException {
List items = null;
Session session = null;
try{
session = sessionFactory.openSession( );
Query q = session.createQuery("from ItemBO item");
List results = q.list( );
int size = results.size( );
items = new ArrayList( );
for( int i = 0; i < size; i++ ){
ItemBO itemBO = (ItemBO)results.get(i);
ItemSummaryView newView = new ItemSummaryView( );
newView.setId( itemBO.getId( ).toString( ) );
newView.setName( itemBO.getDisplayLabel( ) );
newView.setUnitPrice( itemBO.getBasePrice( ) );
newView.setSmallImageURL( itemBO.getSmallImageURL( ) );
newView.setProductFeature( itemBO.getFeature1( ) );
items.add( newView );
}
session.close( );
}catch( Exception ex ){
ex.printStackTrace( );
throw DatastoreException.datastoreError(ex);
}
return items;
}
/**
* Return an detailed view of an item based on the itemId argument.
*/
public ItemDetailView getItemDetailView( String itemId )
throws DatastoreException{
ItemBO itemBO = null;
Session session = null;
try{
session = sessionFactory.openSession( );
itemBO = (ItemBO) session.get(ItemBO.class, itemId);
session.close( );
}catch( Exception ex ){
ex.printStackTrace( );
throw DatastoreException.datastoreError(ex);
}
//
if (itemBO == null ){
throw DatastoreException.objectNotFound( );
}
// Build a ValueObject for the Item
ItemDetailView view = new ItemDetailView( );
view.setId( itemBO.getId( ).toString( ) );
view.setDescription( itemBO.getDescription( ) );
view.setLargeImageURL( itemBO.getLargeImageURL( ) );
view.setName( itemBO.getDisplayLabel( ) );
view.setProductFeature( itemBO.getFeature1( ) );
view.setUnitPrice( itemBO.getBasePrice( ) );
view.setTimeCreated( new Timestamp(System.currentTimeMillis( ) ));
view.setModelNumber( itemBO.getModelNumber( ) );
return view;
}
/**
* Authenticate the user's credentials and either return a UserView for the
* user or throw one of the security exceptions.
*/
public UserView authenticate(String email, String password) throws
InvalidLoginException,ExpiredPasswordException,AccountLockedException,
DatastoreException {
List results = null;
try{
Session session = sessionFactory.openSession( );
results =
session.find(
"from CustomerBO as cust where cust.email = ? and cust.password = ?",
new Object[] { email, password },
new Type[] { Hibernate.STRING, Hibernate.STRING } );
}catch( Exception ex ){
ex.printStackTrace( );
throw DatastoreException.datastoreError(ex);
}
// If no results were found, must be an invalid login attempt
if ( results.isEmpty( ) ){
throw new InvalidLoginException( );
}
// Should only be a single customer that matches the parameters
CustomerBO customer = (CustomerBO)results.get(0);
// Make sure the account is not locked
String accountStatusCode = customer.getAccountStatus( );
if ( accountStatusCode != null && accountStatusCode.equals( "L" ) ){
throw new AccountLockedException( );
}
// Populate the Value Object from the Customer business object
UserView userView = new UserView( );
userView.setId( customer.getId( ).toString( ) );
userView.setFirstName( customer.getFirstName( ) );
userView.setLastName( customer.getLastName( ) );
userView.setEmailAddress( customer.getEmail( ) );
userView.setCreditStatus( customer.getCreditStatus( ) );
return userView;
}
/**
* Log the user out of the system.
*/
public void logout(String email){
// Do nothing with right now, but might want to log it for auditing reasons
}
public void destroy( ){
// Do nothing for this example
}
private void init( ) throws DatastoreException {
try{
sessionFactory = new Configuration( ).configure( ).buildSessionFactory( );
}catch( Exception ex ){
throw DatastoreException.datastoreError(ex);
}
}
}

The service implementation provides all of the required methods of
the IStorefrontService interface. Because the
IStorefrontService interface extends the
IAuthentication interface, the
StorefrontServiceImpl class also must implement
the security methods. Again, notice that the implementation knows
nothing about the Struts framework or web containers in general. This
allows it to be reused across many different types of applications.
This was our goal when we set out at the beginning of this chapter.

We mentioned earlier that we have to call a few methods of the
Hibernate framework so that the mapping XML can be parsed and the
connections to the database can be made ready. This initialization is
shown in the init() method in Example 6-8. When the constructor of this implementation
is called, the XML file is loaded and parsed. Upon successful
completion of the constructor, the persistence framework is ready to
be called.

The constructor needs to be called by the client. In the case of the
Storefront application, we'll use a factory class,
which will also be a Struts PlugIn. The factory is
shown in Example 6-9.


Example 6-9. The StorefrontServiceFactory class

package com.oreilly.struts.storefront.service;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.struts.action.PlugIn;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.config.ModuleConfig;
import com.oreilly.struts.storefront.framework.util.IConstants;
/**
* A factory for creating Storefront Service Implementations. The specific
* service to instantiate is determined from the initialization parameter
* of the ServiceContext. Otherwise, a default implementation is used.
* @see com.oreilly.struts.storefront.service.StorefrontDebugServiceImpl
*/
public class StorefrontServiceFactory implements IStorefrontServiceFactory, PlugIn{
// Hold onto the servlet for the destroy method
private ActionServlet servlet = null;
// The default is to use the debug implementation
String serviceClassname =
"com.oreilly.struts.storefront.service.StorefrontDebugServiceImpl";
public IStorefrontService createService( ) throws
ClassNotFoundException, IllegalAccessException, InstantiationException {
String className = servlet.getInitParameter( IConstants.SERVICE_CLASS_KEY );
if (className != null ){
serviceClassname = className;
}
IStorefrontService instance =
(IStorefrontService)Class.forName(serviceClassname).newInstance( );
return instance;
}
public void init(ActionServlet servlet, ModuleConfig config)
throws ServletException{
// Store the servlet for later
this.servlet = servlet;
/* Store the factory for the application. Any Storefront service factory
* must either store itself in the ServletContext at this key, or extend
* this class and don't override this method. The Storefront application
* assumes that a factory class that implements the IStorefrtonServiceFactory
* is stored at the proper key in the ServletContext.
*/
servlet.getServletContext( ).setAttribute( IConstants.SERVICE_FACTORY_KEY, this );
}
public void destroy( ){
// Do nothing for now
}
}

The StorefrontServiceFactory class in Example 6-9 reads an initialization parameter from the
struts-config.xml file, which tells it the name
of the IStorefrontService implementation class to
instantiate. If it doesn't have an
property for this value, a default implementation
class (in this case, the debug implementation) is created.

Because the factory class implements the PlugIn
interface, it will be instantiated at startup, and the init(
)
method will be called. The init()
method stores an instance of the factory into the application scope,
where it can be retrieved later. To create an instance of the
Storefront service, a client just needs to retrieve the factory from
the ServletContext and call the
createService() method. The
createService() method calls the no-argument
constructor on whichever implementation class has been configured.

The final step that needs to be shown is how we invoke the Storefront
service interface from an Action class. The relevant methods are
highlighted in Example 6-10.


Example 6-10. The LoginAction from the Storefront application

package com.oreilly.struts.storefront.security;
import java.util.Locale;
import javax.servlet.http.*;
import javax.servlet.ServletContext;
import org.apache.struts.action.*;
import org.apache.struts.util.MessageResources;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.exceptions.*;
import com.oreilly.struts.storefront.framework.UserContainer;
import com.oreilly.struts.storefront.framework.StorefrontBaseAction;
import com.oreilly.struts.storefront.framework.util.IConstants;
import com.oreilly.struts.storefront.service.IStorefrontService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Implements the logic to authenticate a user for the storefront application.
*/
public class LoginAction extends StorefrontBaseAction {
protected static Log log = LogFactory.getLog( StorefrontBaseAction.class );
/**
* Called by the controller when the a user attempts to login to the
* storefront application.
*/
public ActionForward execute( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response )
throws Exception{
// Get the user's login name and password. They should have already
// validated by the ActionForm.
String email = ((LoginForm)form).getEmail( );
String password = ((LoginForm)form).getPassword( );
// Get the StorefrontServiceFactory
IStorefrontServiceFactory factory = (IStorefrontServiceFactory)
servlet.getServletContext( ).getAttribute( IConstants.SERVICE_FACTORY_KEY );
// Create the Service
try{
service = factory.createService( );
}catch( Exception ex ){
log.error( "Problem creating the Storefront Service", ex );
}
return service;
// Attempt to authenticate the user
UserView userView = service.authenticate(email, password);
// Store the user object into the HttpSession
UserContainer existingContainer = getUserContainer(request);
existingContainer.setUserView(userView);
return mapping.findForward(IConstants.SUCCESS_KEY);
}
}

The first step in the LogoutAction is to acquire
an instance of the service on which it will authenticate the user. As
you have seen in the previous examples, an instance of the Storefront
service can be obtained through the factory, as Example 6-10 shows.

Once you have written the code to acquire the factory and the service
in several Action classes, you my be tempted to move this code up to
an abstract base Action class from which all others can extend. Once
implemented, concrete Action classes would need only to call a
getStorefrontService() method. This method is
located in the superclass called StorefrontBaseAction.

The implementation for the getStorefrontService()
method retrieves the factory and calls the createService(
)
method. The
StorefrontBaseAction class, which includes the
getStorefrontService() method, is shown in Example 6-11.


Example 6-11. The Storefront Base Action class

package com.oreilly.struts.storefront.framework;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Iterator;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.oreilly.struts.storefront.framework.util.IConstants;
import com.oreilly.struts.storefront.framework.exceptions.*;
import com.oreilly.struts.storefront.service.IStorefrontService;
import com.oreilly.struts.storefront.service.IStorefrontServiceFactory;
/**
* An abstract Action class that all Storefront action classes should
* extend.
*/
abstract public class StorefrontBaseAction extends Action {
Log log = LogFactory.getLog( this.getClass( ) );
protected IStorefrontService getStorefrontService( ){
IStorefrontServiceFactory factory = (IStorefrontServiceFactory)getApplicationObject
( IConstants.SERVICE_FACTORY_KEY );
IStorefrontService service = null;
try{
service = factory.createService( );
}catch( Exception ex ){
log.error( "Problem creating the Storefront Service", ex );
}
return service;
}
/**
* Retrieve a session object based on the request and the attribute name.
*/
protected Object getSessionObject(HttpServletRequest req,
String attrName) {
Object sessionObj = null;
HttpSession session = req.getSession(false);
if ( session != null ){
sessionObj = session.getAttribute(attrName);
}
return sessionObj;
}
/**
* Return the instance of the ApplicationContainer object.
*/
protected ApplicationContainer getApplicationContainer( ) {
return (ApplicationContainer)getApplicationObject
(IConstants.APPLICATION_CONTAINER_KEY);
}
/**
* Retrieve the UserContainer for the user tier to the request.
*/
protected UserContainer getUserContainer(HttpServletRequest request) {
UserContainer userContainer = (UserContainer)getSessionObject(request, IConstants.
USER_CONTAINER_KEY);
// Create a UserContainer for the user if it doesn't exist already
if(userContainer == null) {
userContainer = new UserContainer( );
userContainer.setLocale(request.getLocale( ));
HttpSession session = request.getSession(true);
session.setAttribute(IConstants.USER_CONTAINER_KEY, userContainer);
}
return userContainer;
}
/**
* Retrieve an object from the application scope by its name. This is
* a convience method.
*/
protected Object getApplicationObject(String attrName) {
return servlet.getServletContext( ).getAttribute(attrName);
}
public boolean isLoggedIn( HttpServletRequest request ){
UserContainer container = getUserContainer(request);
if ( container.getUserView( ) != null ){
return true;
}else{
return false;
}
}
}

The getApplicationObject() method is just a
convenience method for the Storefront Action
classes; it calls the getAttribute() method on
the ServletContext object.

Once the service is obtained in Example 6-10, the
authenticate() method is called and a value
object called UserView is returned back to the
Action:

    UserView userView = serviceImpl.authenticate(email, password);

This object is placed inside a session object, and the
Action returns. If no user with a matching set of
credentials is found, the authenticate() method
will throw an InvalidLoginException.

Notice that the Action class is using the
IStorefrontService interface, not the
implementation object. As we said, this is important to prevent
alternate implementations from having a ripple effect on the
Action
classes.


6.4.9 Conclusion


We covered a lot of ground in this chapter, and it may be little
overwhelming if you are new to the concepts of models and
persistence. Some of the future chapters will assume that these
topics are familiar to you and will not spend any time discussing
them, so make sure you understand this material before moving on.

Obviously much of this chapter dealt with issues not directly related
to Struts. As mentioned earlier, the Struts framework
doesn't offer much for the model. Most of the work
is spent integrating an existing model architecture into Struts, as
we did throughout this chapter. And although this material may seem
irrelevant to the topic of the book, it's just as
important as anything else you'll have to do with
Struts.


    / 181