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.
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.
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.
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.
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.
package com.oreilly.struts.storefront.businessobjects;
import java.math.BigDecimal;
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.
|
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.
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.
# The
SQL DDL for the Storefront Application
# (Programming Jakarta Struts by O'Reilly)
# Chuck Cavaness
# Execute the next line if you need to clear the storefront database
DROP DATABASE storefront;
# Creates the initial database
CREATE DATABASE storefront;
# Make sure you are creating the tables in the storefront tablespace
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);
|
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.
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 (also known as 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.
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 |
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 product comparison between these and other mapping products at http://www.object-relational.com/object-relational.html. There is a cost for the compiled information, but if your application is large, it may be money well spent.
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.
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 ObJectRelationalBridge (OJB) product to use for the Storefront example.
|
The documentation for OJB is pretty good, considering that documentation for open source projects tends to be one of the last tasks completed. Essentially, the entire mapping of the business objects to the database tables takes place in a single XML file, called repository_user.xml. The file is 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.
|
<ClassDescriptor id="120">
<class.name>com.oreilly.struts.storefront.businessobjects.CustomerBO</class.name>
<table.name>CUSTOMER</table.name>
<FieldDescriptor id="1">
<field.name>id</field.name>
<column.name>id</column.name>
<jdbc_type>INTEGER</jdbc_type>
<PrimaryKey>true</PrimaryKey>
<autoincrement>true</autoincrement>
</FieldDescriptor>
<FieldDescriptor id="2">
<field.name>firstName</field.name>
<column.name>firstname</column.name>
<jdbc_type>VARCHAR</jdbc_type>
</FieldDescriptor>
<FieldDescriptor id="3">
<field.name>lastName</field.name>
<column.name>lastname</column.name>
<jdbc_type>VARCHAR</jdbc_type>
</FieldDescriptor>
<FieldDescriptor id="4">
<field.name>email</field.name>
<column.name>email</column.name>
<jdbc_type>VARCHAR</jdbc_type>
</FieldDescriptor>
<FieldDescriptor id="5">
<field.name>password</field.name>
<column.name>password</column.name>
<jdbc_type>VARCHAR</jdbc_type>
</FieldDescriptor>
<FieldDescriptor id="6">
<field.name>accountStatus</field.name>
<column.name>accountstatus</column.name>
<jdbc_type>CHAR</jdbc_type>
</FieldDescriptor>
<FieldDescriptor id="7">
<field.name>creditStatus</field.name>
<column.name>creditstatus</column.name>
<jdbc_type>CHAR</jdbc_type>
</FieldDescriptor>
<CollectionDescriptor id="1">
<cdfield.name>submittedOrders</cdfield.name>
<items.class>com.oreilly.struts.storefront.businessobjects.OrderBO</items.class>
<inverse_fk_descriptor_ids>2</inverse_fk_descriptor_ids>
</CollectionDescriptor>
</ClassDescriptor>
The rest of the mappings are mapped in a similar manner. Once all of the mappings are specified in the XML file, you must configure the database connection information to allow the JDBC driver to connect to the correct database. With the OJB product, you configure the connection information in the repository.xml file. This is shown in Example 6-5.
<?xml version="1.0" encoding="UTF-8"?>
<!-- defining entities for include-files -->
<!DOCTYPE MappingRepository SYSTEM "repository.dtd" [
<!ENTITY user SYSTEM "repository_user.xml">
<!ENTITY junit SYSTEM "repository_junit.xml">
<!ENTITY internal SYSTEM "repository_internal.xml">
]>
<MappingRepository>
<JdbcConnectionDescriptor id="default">
<dbms.name>MsSQLServer2000</dbms.name>
<jdbc.level>1.0</jdbc.level>
<driver.name>com.microsoft.jdbc.sqlserver.SQLServerDriver</driver.name>
<url.protocol>jdbc</url.protocol>
<url.subprotocol>microsoft:sqlserver</url.subprotocol>
<url.dbalias>
//localhost:1433;DatabaseName=storefront;user=sa;SelectMethod=cursor
</url.dbalias>
<user.name>sa</user.name>
<user.passwd></user.passwd>
</JdbcConnectionDescriptor>
<!-- include user-defined mappings here -->
&user;
<!-- include ojb internal mappings here -->
&internal;
</MappingRepository>
You need to configure the settings in this file for your specific environment. That's really all there is to configuring the persistence framework for your application. To initialize the framework within your application, you simply call three methods, as shown later in this section.
There's not enough room in this chapter for a better explanation of the OJB framework. For more detailed information, review the documentation for the product at http://jakarta.apache.org/ojb/. Don't forget that you will need to have an appropriate database and a JDBC driver in your web application's classpath.
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 actually are two independent design patterns contained within the DAO—the Bridge and the Adaptor—both 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.
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.
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.
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.
|
package com.oreilly.struts.storefront.service;
import java.sql.Timestamp;
import java.util.List;
import java.util.ArrayList;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.catalog.view.ItemSummaryView;
import com.oreilly.struts.storefront.framework.security.IAuthentication;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.businessobjects.*;
// Import the exceptions used
import com.oreilly.struts.storefront.framework.exceptions.DatastoreException;
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 the implementation-specific packages
import org.odmg.*;
import ojb.odmg.*;
public class StorefrontServiceImpl implements IStorefrontService{
// Implementation-specific references
Implementation odmg = null;
Database db = 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 {
// Start a transaction
Transaction tx = odmg.newTransaction( );
tx.begin( );
List results = null;
try{
OQLQuery query = odmg.newOQLQuery( );
// Set the OQL select statement
query.create( "select featuredItems from " + ItemBO.class.getName( ) );
results = (List)query.execute( );
tx.commit( );
}catch( Exception ex ){
// Roll back the transaction
tx.abort( );
ex.printStackTrace( );
throw DatastoreException.datastoreError(ex);
}
int size = results.size( );
List 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 );
}
return items;
}
/**
* Return a detailed view of an item based on the itemId argument.
*/
public ItemDetailView getItemDetailView( String itemId )
throws DatastoreException{
// Start a transaction
Transaction tx = odmg.newTransaction( );
tx.begin( );
List results = null;
try{
OQLQuery query = odmg.newOQLQuery( );
// Set the OQL select statement
String queryStr = "select item from " + ItemBO.class.getName( );
queryStr += " where id = $1";
query.create(queryStr);
query.bind(itemId);
// Execute the transaction
results = (List)query.execute( );
tx.commit( );
}catch( Exception ex ){
// Roll back the transaction
tx.abort( );
ex.printStackTrace( );
throw DatastoreException.datastoreError(ex);
}
if (results.isEmpty( ) ){
throw DatastoreException.objectNotFound( );
}
ItemBO itemBO = (ItemBO)results.get(0);
// 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 {
// Start a transaction
Transaction tx = odmg.newTransaction( );
tx.begin( );
// Query the database for a user that matches the credentials
List results = null;
try{
OQLQuery query = odmg.newOQLQuery( );
// Set the OQL select statement
String queryStr = "select customer from " + CustomerBO.class.getName( );
queryStr += " where email = $1 and password = $2";
query.create(queryStr);
// Bind the input parameters
query.bind( email );
query.bind( password );
// Retrieve the results and commit the transaction
results = (List)query.execute( );
tx.commit( );
}catch( Exception ex ){
// Roll back the transaction
tx.abort( );
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 it right now, but might want to log it for auditing reasons
}
/**
* Opens the database and prepares it for transactions
*/
private void init( ) throws DatastoreException {
// Get odmg facade instance
odmg = OJB.getInstance( );
db = odmg.newDatabase( );
// Open database
try{
db.open("repository.xml", Database.OPEN_READ_WRITE);
}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 OJB 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, to determine which Storefront service to initialize. The factory is shown in Example 6-9.
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.ApplicationConfig;
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.
*
*/
public class StorefrontServiceFactory implements IStorefrontServiceFactory,PlugIn{
// Hold on to 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;
}
return (IStorefrontService)Class.forName(className).newInstance( );
}
public void init(ActionServlet servlet, ApplicationConfig 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 IStorefrontServiceFactory
* 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 web.xml file, which tells it the name of the IStorefrontService implementation class to instantiate. If it doesn't have an init-param 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.
package com.oreilly.struts.storefront.security;
import java.util.Locale;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.exceptions.BaseException;
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;
/**
* Implements the logic to authenticate a user for the Storefront application.
*/
public class LoginAction extends StorefrontBaseAction{
/**
* Called by the controller when a user attempts to log in 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
// been validated by the ActionForm.
String email = ((LoginForm)form).getEmail( );
String password = ((LoginForm)form).getPassword( );
// Log in through the security service
IStorefrontService serviceImpl = getStorefrontService( );
UserView userView = serviceImpl.authenticate(email, password);
UserContainer existingContainer = null;
HttpSession session = request.getSession(false);
if ( session != null ){
existingContainer = getUserContainer(request);
session.invalidate( );
}else{
existingContainer = new UserContainer( );
}
// Create a new session for the user
session = request.getSession(true);
existingContainer.setUserView(userView);
session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer);
return mapping.findForward(IConstants.SUCCESS_KEY);
}
}
The first highlighted line calls the getStorefrontService( ) method. This method is located in the superclass called StorefrontBaseAction because every Action class will need to call this method. 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.
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;
// Don't create a session if one isn't already present
HttpSession session = req.getSession(true);
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( );
session.setAttribute(IConstants.USER_CONTAINER_KEY, userContainer);
}
return userContainer;
}
/**
* Retrieve an object from the application scope by its name. This is
* a convenience method.
*/
protected Object getApplicationObject(String attrName) {
return servlet.getServletContext( ).getAttribute(attrName);
}
}
The getApplicationObject( ) method is just a convenience method for the Storefront Action classes; it calls the getAttribute( ) method on the ServletContext object.
Finally, the second highlighted line in Example 6-10 invokes a service method on the implementation. 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.
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.