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
figs/jstr2_0604.gif


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/index.html

CocoBase

http://www.cocobase.com

Torque

http://db.apache.org/torque/index.html

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_mapping.html.


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 />       </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 />       </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 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.

Figure 6-5. The Business Delegate and DAO patterns combined
figs/jstr2_0605.gif


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.



Programming Jakarta Struts
Programming Jakarta Struts, 2nd Edition
ISBN: 0596006519
EAN: 2147483647
Year: 2003
Pages: 180

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net