The Stateful Session Bean

Each stateful session bean is dedicated to one client for the life of the bean instance; it acts on behalf of that client as its agent. Stateful session beans are not swapped among EJB objects or kept in an instance pool like entity and stateless session bean instances. Once a stateful session bean is instantiated and assigned to an EJB object, it is dedicated to that EJB object for its entire life cycle.[43]

Stateful session beans maintain conversational state, which means that the instance variables of the bean class can cache data relative to the client between method invocations. This makes it possible for methods to be interdependent, so that changes made to the bean’s state in one method call can affect the results of subsequent method invocations. In contrast, the stateless session beans we have been talking about do not maintain conversational state. Although stateless beans may have instance variables, these fields are not specific to one client. A stateless instance is swapped among many EJB objects, so you cannot predict which instance will service a method call. With stateful session beans, every method call from a client is serviced by the same instance (at least conceptually), so the bean instance’s state can be predicted from one method invocation to the next.

Although stateful session beans maintain conversational state, they are not themselves persistent like entity beans. Entity beans represent data in the database; their persistence fields are written directly to the database. Stateful session beans, like stateless beans, can access the database but do not represent data in the database. In addition, stateful beans are not used concurrently like entity beans. If you have an entity EJB object that wraps an instance of the ship called Paradise, for example, all client requests for that ship will be coordinated through the same EJB object.[44] With stateful session beans, the EJB object is dedicated to one client—stateful session beans are not used concurrently.

Stateful session beans are often thought of as extensions of the client. This makes sense if you think of a client as being made up of operations and state. Each task may rely on some information gathered or changed by a previous operation. A GUI client is a perfect example: when you fill in the fields on a GUI client you are creating conversational state. Pressing a button executes an operation that might fill in more fields, based on the information you entered previously. The information in the fields is conversational state.

Stateful session beans allow you to encapsulate some of the business logic and conversational state of a client and move it to the server. Moving this logic to the server thins the client application and makes the system as a whole easier to manage. The stateful session bean acts as an agent for the client, managing processes or workflow to accomplish a set of tasks; it manages the interactions of other beans in addition to direct data access over several operations to accomplish a complex set of tasks. By encapsulating and managing workflow on behalf of the client, stateful beans present a simplified interface that hides the details of many interdependent operations on the database and other beans from the client.

EJB 2.0: Getting Set Up for the TravelAgent EJB

The TravelAgent EJB will make use of the Cabin, Cruise, Reservation, and Customer beans developed in Chapter 6 and Chapter 7. It will coordinate the interaction of these entity beans to book a passenger on a cruise.

The Reservation EJB that was used in Chapter 7 will be modified slightly so that it can be created with all its relationships identified right away. To accommodate this, we overload its ejbCreate() method:

public abstract class ReservationBean implements javax.ejb.EntityBean {

    public Integer ejbCreate(CustomerRemote customer, CruiseLocal cruise,
        CabinLocal cabin, double price, Date dateBooked) {

        setAmountPaid(price);
        setDate(dateBooked);
        return null;
    }
    public void ejbPostCreate(CustomerRemote customer, CruiseLocal cruise,
        CabinLocal cabin, double price, Date dateBooked)
        throws javax.ejb.CreateException {

        setCruise(cruise);

        // add Cabin to collection-based CMR field
        Set cabins = new HashSet();
        cabins.add(cabin);
        this.setCabins(cabins);

        try {
            Integer primKey = (Integer)customer.getPrimaryKey();
            javax.naming.Context jndiContext = new InitialContext();
            CustomerHomeLocal home = (CustomerHomeLocal)
                jndiContext.lookup("java:comp/env/ejb/CustomerHomeLocal");
            CustomerLocal custL = home.findByPrimaryKey(primKey);

            // add Customer to collection-based CMR field
            Set customers = new HashSet();
            customers.add(custL);
            this.setCustomers(customers);

        } catch (RemoteException re) {
            throw new CreateException("Invalid Customer");
        } catch (FinderException fe) {
            throw new CreateException("Invalid Customer");
        } catch (NamingException ne) {
            throw new CreateException("Invalid Customer");
        }
    }

Relationship fields use local EJB object references, so we must convert the CustomerRemote reference to a CustomerLocal reference in order to set the Reservation EJB’s customer relationship field. To do this, you can either use the JNDI ENC to locate the local home interface and then executing the findByPrimaryKey() method or implement an ejbSelect() method in the Reservation EJB to locate the CustomerLocal reference.

EJB 1.1: Getting Set Up for the TravelAgent EJB

Both the TravelAgent EJB and the ProcessPayment EJB, which we develop in this chapter, depend on other entity beans, some of which we developed earlier in this book and several of which you can download from O’Reilly’s web site. We developed the Cabin EJB in Chapter 4, but we still need several other beans for this example: namely, the Cruise, Customer, and Reservation EJBs. The source code for these beans is available with the rest of the examples for this book at the O’Reilly download site. Instructions for downloading code are available in the Preface of this book and in the workbooks.

Before you can use these beans, you need to create some new tables in your database. Here are the table definitions the new entity beans will need. The Cruise EJB maps to the CRUISE table:

CREATE TABLE CRUISE 
(
    ID         INT PRIMARY KEY, 
    NAME       CHAR(30), 
    SHIP_ID    INT
)

The Customer EJB maps to the CUSTOMER table:

CREATE TABLE CUSTOMER
(
    ID             INT PRIMARY KEY, 
    FIRST_NAME     CHAR(30), 
    LAST_NAME      CHAR(30), 
    MIDDLE_NAME    CHAR(30)
)

The Reservation EJB maps to the RESERVATION table:

CREATE TABLE RESERVATION 
(
    CUSTOMER_ID    INT, 
    CABIN_ID       INT, 
    CRUISE_ID      INT, 
    AMOUNT_PAID DECIMAL (8,2),
    DATE_RESERVED DATE
)

Once you have created the tables, deploy these beans as container-managed entities in your EJB server and test them to ensure that they are working properly.

The TravelAgent EJB

The TravelAgent EJB, which we have already seen, is a stateful session bean that encapsulates the process of making a reservation on a cruise. We will develop this bean further in this chapter to demonstrate how stateful session beans can be used as workflow objects.

Although EJB 2.0 readers will use the local interfaces of other beans than the TravelAgent EJB, we will not develop a local interface for the TravelAgent EJB. The rules for developing local interfaces for stateful session beans are the same as those for stateless session and entity beans. The TravelAgent EJB is designed to be used only by remote clients and therefore does not require a set of local component interfaces.

TravelAgent: The remote interface

In Chapter 4, we developed an early version of the TravelAgentRemote interface that contained a single business method, listCabins(). We are now going to remove the listCabins() method and redefine the TravelAgent EJB so that it behaves like a workflow object. Later in this chapter, we will add a modified listing method for obtaining a more specific list of cabins for the user.

As a stateful session bean that models workflow, the TravelAgent EJB manages the interactions of several other beans while maintaining conversational state. The following code contains the modified TravelAgentRemote interface:

package com.titan.travelagent;

import java.rmi.RemoteException;
import javax.ejb.FinderException;
import com.titan.processpayment.CreditCardDO;

public interface TravelAgentRemote extends javax.ejb.EJBObject {

    public void setCruiseID(Integer cruise) 
        throws RemoteException, FinderException;

    public void setCabinID(Integer cabin) 
        throws RemoteException, FinderException;

    public TicketDO bookPassage(CreditCardDO card, double price)
        throws RemoteException,IncompleteConversationalState;   
}

The purpose of the TravelAgent EJB is to make cruise reservations. To accomplish this task, the bean needs to know which cruise, cabin, and customer make up the reservation. Therefore, the client using the TravelAgent EJB needs to gather this kind of information before making the booking. The TravelAgentRemote interface provides methods for setting the IDs of the cruise and cabin that the customer wants to book. We can assume that the cabin ID comes from a list and that the cruise ID comes from some other source. The customer is set in the create() method of the home interface—more about this later.

Once the customer, cruise, and cabin are chosen, the TravelAgent EJB is ready to process the reservation. This operation is performed by the bookPassage() method, which needs the customer’s credit card information and the price of the cruise. bookPassage() is responsible for charging the customer’s account, reserving the chosen cabin in the right ship on the right cruise, and generating a ticket for the customer. How this is accomplished is not important to us at this point; when we are developing the remote interface, we are concerned only with the business definition of the bean. We will discuss the implementation when we talk about the bean class.

Note that the bookPassage() method throws an application-specific exception, IncompleteConversationalState. This exception is used to communicate business problems encountered while booking a customer on a cruise. The IncompleteConversationalState exception indicates that the TravelAgent EJB did not have enough information to process the booking. The IncompleteConversationalState application exception class is defined as follows:

package com.titan.travelagent;

public class IncompleteConversationalState extends java.lang.Exception {
    public IncompleteConversationalState(){super();}
    public IncompleteConversationalState(String msg){super(msg);}
}

Dependent object: TicketDO

Like the CreditCardDO and CheckDO classes used in the ProcessPayment EJB, the TicketDO class bookPassage() returns is defined as a pass-by-value object. One could argue that a ticket should be an entity bean since it is not dependent and may be accessed outside the context of the TravelAgent EJB. However, determining how a business object is used can also dictate whether it should be a bean or simply a class. The TicketDO object, for example, could be digitally signed and emailed to the client as proof of purchase. This would not be feasible if the TicketDO object were an entity bean. Enterprise beans are referenced only through their component interfaces and are not passed by value, as are serializable objects such as TicketDO, CreditCardDO, and CheckDO. As an exercise in passing by value, we will define TicketDO as a simple serializable object instead of a bean.

EJB 2.0 utilizes the local interfaces of the Cruise and Cabin EJBs and the remote interface of the Customer EJB when creating a new TicketDO object:

package com.titan.travelagent;

import com.titan.cruise.CruiseLocal;
import com.titan.cabin.CabinLocal;
import com.titan.customer.CustomerRemote;

public class TicketDO implements java.io.Serializable {
    public Integer customerID;
    public Integer cruiseID;
    public Integer cabinID;
    public double price;
    public String description;
    
    public TicketDO(CustomerRemote customer, CruiseLocal cruise, 
        CabinLocal cabin, double price) throws javax.ejb.FinderException, 
        RemoteException, javax.naming.NamingException {
        
            description = customer.getFirstName()+
                " " + customer.getLastName() + 
                " has been booked for the "
                + cruise.getName() + 
                " cruise on ship " + 
                  cruise.getShip().getName() + ".
" +  
                " Your accommodations include " + 
                  cabin.getName() + 
                " a " + cabin.getBedCount() + 
                " bed cabin on deck level " + cabin.getDeckLevel() + 
                ".
 Total charge = " + price;
            customerID = (Integer)customer.getPrimaryKey();
            cruiseID = (Integer)cruise.getPrimaryKey();
            cabinID = (Integer)cabin.getPrimaryKey();
            this.price = price;
        }
        
    public String toString() {
        return description;
    }
}

EJB 1.1 also utilizes the remote interfaces of the Customer, Cruise, and Cabin EJBs when creating a new TicketDO object:

package com.titan.travelagent;

import com.titan.cruise.CruiseRemote;
import com.titan.cabin.CabinRemote;
import com.titan.customer.CustomerRemote;
import java.rmi.RemoteException;

public class TicketDO implements java.io.Serializable {
    public Integer customerID;
    public Integer cruiseID;
    public Integer cabinID;
    public double price;
    public String description;
    
    public TicketDO(CustomerRemote customer, CruiseRemote cruise, 
        CabinRemote cabin, double price) throws javax.ejb.FinderException,
        RemoteException, javax.naming.NamingException {
        
            description = customer.getFirstName()+
                " " + customer.getLastName() + 
                " has been booked for the "
                + cruise.getName() + 
                " cruise on ship " + cruise.getShipID() + ".
" +  
                " Your accommodations include " + 
                  cabin.getName() + 
                " a " + cabin.getBedCount() + 
                " bed cabin on deck level " + cabin.getDeckLevel() + 
                ".
 Total charge = " + price;
            customerID = (Integer)customer.getPrimaryKey();
            cruiseID = (Integer)cruise.getPrimaryKey();
            cabinID = (Integer)cabin.getPrimaryKey();
            this.price = price;

        }
    public String toString() {
        return description;
    }
}

TravelAgentHomeRemote: The home interface

Starting with the TravelAgentHomeRemote interface we developed in Chapter 4, we can modify the create() method to take a remote reference to the customer who is making the reservation:

package com.titan.travelagent;

import java.rmi.RemoteException;
import javax.ejb.CreateException;
import com.titan.customer.CustomerRemote;

public interface TravelAgentHomeRemote extends javax.ejb.EJBHome {
    public TravelAgentRemote create(CustomerRemote cust)
        throws RemoteException, CreateException;
}

The create() method in this home interface requires that a remote reference to a Customer EJB be used to create the TravelAgent EJB. Because there are no other create() methods, you cannot create a TravelAgent EJB if you do not know who the customer is. The Customer EJB reference provides the TravelAgent EJB with some of the conversational state it will need to process the bookPassage() method.

Taking a peek at the client view

Before settling on definitions for your component interfaces, it is a good idea to figure out how the bean will be used by clients. Imagine that the TravelAgent EJB is used by a Java application with GUI fields. The GUI fields capture the customer’s preference for the type of cruise and cabin. We start by examining the code used at the beginning of the reservation process:

Context jndiContext = getInitialContext();
Object ref = jndiContext.lookup("CustomerHomeRemote");
CustomerHomeRemote customerHome =(CustomerHomeRemote)
    PortableRemoteObject.narrow(ref, CustomerHomeRemote.class);

String ln = tfLastName.getText();
String fn = tfFirstName.getText();
String mn = tfMiddleName.getText();
CustomerRemote customer = customerHome.create(nextID, ln, fn, mn); 

ref = jndiContext.lookup("TravelAgentHomeRemote");
TravelAgentHomeRemote home = (TravelAgentHomeRemote)
    PortableRemoteObject.narrow(ref, TravelAgentHomeRemote.class);
        
TravelAgentRemote agent = home.create(customer);

This snippet of code creates a new Customer EJB based on information the travel agent gathered over the phone. The CustomerRemote reference is then used to create a TravelAgent EJB. Next, we gather the cruise and cabin choices from another part of the applet:

Integer cruise_id = new Integer(textField_cruiseNumber.getText());

Integer cabin_id = new Integer( textField_cabinNumber.getText());

agent.setCruiseID(cruise_id);
                  agent.setCabinID(cabin_id);

The travel agent chooses the cruise and cabin the customer wishes to reserve. These IDs are set in the TravelAgent EJB, which maintains the conversational state for the whole process.

At the end of the process, the travel agent completes the reservation by processing the booking and generating a ticket. Because the TravelAgent EJB has maintained the conversational state, caching the customer, cabin, and cruise information, only the credit card and price are needed to complete the transaction:

String cardNumber = textField_cardNumber.getText();
Date date = dateFormatter.parse(textField_cardExpiration.getText());
String cardBrand = textField_cardBrand.getText();
CreditCardDO card = new CreditCardDO(cardNumber,date,cardBrand);
double price = double.valueOf(textField_cruisePrice.getText()).doubleValue();
TicketDO ticket = agent.bookPassage(card,price);
PrintingService.print(ticket);

We can now move ahead with development. This summary of how the client will use the TravelAgent EJB confirms that our remote interface and home interface definitions are workable.

TravelAgentBean: The bean class

We can now implement all of the behavior expressed in the new remote interface and home interface for the TravelAgent EJB.[45]

Here is a partial definition of the new TravelAgentBean class for EJB 2.0:

import com.titan.reservation.*;

import java.sql.*;
import javax.sql.DataSource;
import java.util.Vector;
import java.rmi.RemoteException;
import javax.naming.NamingException;
import javax.ejb.EJBException;
import com.titan.processpayment.*;
import com.titan.cruise.*;
import com.titan.customer.*;
import com.titan.cabin.*;

public class TravelAgentBean implements javax.ejb.SessionBean {

    public CustomerRemote customer;
    public CruiseLocal cruise;
    public CabinLocal cabin;

    public javax.ejb.SessionContext ejbContext;

    public javax.naming.Context jndiContext;

    public void ejbCreate(CustomerRemote cust) {
        customer = cust;
    }
    public void setCabinID(Integer cabinID) throws javax.ejb.FinderException { 
        try { 
            CabinHomeLocal home = (CabinHomeLocal)
                jndiContext.lookup("java:comp/env/ejb/CabinHomeLocal");
            
            cabin = home.findByPrimaryKey(cabinID);
        } catch(RemoteException re) {
            throw new EJBException(re);
        }
    }
    public void setCruiseID(Integer cruiseID) throws javax.ejb.FinderException { 
        try { 
           CruiseHomeLocal home = (CruiseHomeLocal)
               jndiContext.lookup("java:comp/env/ejb/CruiseHomeLocal");
           
           cruise = home.findByPrimaryKey(cruiseID);
        } catch(RemoteException re) {
            throw new EJBException(re);
        }
        
    }
    public TicketDO bookPassage(CreditCardDO card, double price)
        throws IncompleteConversationalState {
                   
        if (customer == null || cruise == null || cabin == null) 
        {
            throw new IncompleteConversationalState();
        }
        try {
            ReservationHomeLocal resHome = (ReservationHomeLocal)            
                jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal");
             
            ReservationLocal reservation =
                resHome.create(customer, cruise, cabin, price, new Date());
                
            Object ref = jndiContext.lookup("java:comp/env/ejb/
                ProcessPaymentHomeRemote");

            ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote)
                PortableRemoteObject.narrow (ref, ProcessPaymentHomeRemote.class);
            
            ProcessPaymentRemote process = ppHome.create();
            process.byCredit(customer, card, price);

            TicketDO ticket = new TicketDO(customer, cruise, cabin, price);
            return ticket;
        } catch(Exception e) {
            throw new EJBException(e);
        }
    }
    public void ejbRemove() {}
    public void ejbActivate() {}
    public void ejbPassivate() {}
    
    public void setSessionContext(javax.ejb.SessionContext cntx) 
    {

        ejbContext = cntx;
        try {
            jndiContext = new javax.naming.InitialContext();
        } catch(NamingException ne) {
            throw new EJBException(ne);
        }
    }
}

In EJB 1.1, the TravelAgentBean class definition looks like this:

import com.titan.reservation.*;

import java.sql.*;
import javax.sql.DataSource;
import java.util.Vector;
import java.rmi.RemoteException;
import javax.naming.NamingException;
import javax.ejb.EJBException;
import com.titan.processpayment.*;
import com.titan.cruise.*;
import com.titan.customer.*;
import com.titan.cabin.*;

public class TravelAgentBean implements javax.ejb.SessionBean {

    public CustomerRemote customer;
    public CruiseRemote cruise;
    public CabinRemote cabin;

    public javax.ejb.SessionContext ejbContext;

    public javax.naming.Context jndiContext;

    public void ejbCreate(CustomerRemote cust) {
        customer = cust;
    }
    public void setCabinID(Integer cabinID) throws javax.ejb.FinderException { 
        try { 
            CabinHomeRemote home = (CabinHomeRemote)
                getHome("CabinHomeRemote", CabinHomeRemote.class);
            cabin = home.findByPrimaryKey(cabinID);
        } catch(Exception re) {
            throw new EJBException(re);
        }
    }
    public void setCruiseID(Integer cruiseID) throws javax.ejb.FinderException { 
        try { 
            CruiseHomeRemote home = (CruiseHomeRemote)
                getHome("CruiseHomeRemote", CruiseHomeRemote. class);
            cruise = home.findByPrimaryKey(cruiseID);
        } catch(Exception re) {
            throw new EJBException(re);
        }
        
    }
    public TicketDO bookPassage(CreditCardDO card, double price)
        throws IncompleteConversationalState {
                   
        if (customer == null || cruise == null || cabin == null){
            throw new IncompleteConversationalState();
        }
        try {
            ReservationHomeRemote resHome = (ReservationHomeRemote)
                getHome("ReservationHomeRemote", ReservationHomeRemote.class);
            ReservationRemote reservation = 
                resHome.create(customer, cruise, cabin, price, new Date());
            ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote) 
                getHome("ProcessPaymentHomeRemote", ProcessPaymentHomeRemote.class);
            ProcessPaymentRemote process = ppHome.create();
            process.byCredit(customer, card, price);

            TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
            return ticket;
        } catch(Exception e) {
            throw new EJBException(e);
        }
    }
    public void ejbRemove() {}
    public void ejbActivate() {}
    public void ejbPassivate() {}
    
    public void setSessionContext(javax.ejb.SessionContext cntx) 
    {

        ejbContext = cntx;
        try {
            jndiContext = new javax.naming.InitialContext();
        } catch(NamingException ne) {
            throw new EJBException(ne);
        }
    }
    protected Object getHome(String name,Class type) {
        try {
            Object ref = jndiContext.lookup("java:comp/env/ejb/"+name);
            return PortableRemoteObject.narrow(ref, type);
        } catch(NamingException ne) {
            throw new EJBException(ne);
        }
    }
}

There is a lot of code to digest in the TravelAgentBean class definition, so we will approach it in small pieces. First, let’s examine the ejbCreate() method:

public class TravelAgentBean implements javax.ejb.SessionBean {

    public CustomerRemote customer;
        ...

    public javax.ejb.SessionContext ejbContext;
    public javax.naming.Context jndiContext;

    public void ejbCreate(CustomerRemote cust) {
        customer = cust;
    }

When the bean is created, the remote reference to the Customer EJB is passed to the bean instance and maintained in the customer field. The customer field is part of the bean’s conversational state. We could have obtained the customer’s identity as an integer ID and constructed the remote reference to the Customer EJB in the ejbCreate() method. However, we passed the reference directly to demonstrate that remote references to beans can be passed from a client application to a bean. They can also be returned from the bean to the client and passed between beans on the same EJB server or between EJB servers.

References to the SessionContext and JNDI context are held in fields called ejbContext and jndiContext. The “ejb” and “jndi” prefixes help to avoid confusion between the different content types.

When a bean is passivated, the JNDI ENC must be maintained as part of the bean’s conversational state. This means that the JNDI context should not be transient. Once a field is set to reference the JNDI ENC, the reference remains valid for the life of the bean. In the TravelAgentBean, we set the jndiContext field to reference the JNDI ENC when the SessionContext is set at the beginning of the bean’s life cycle:

public void setSessionContext(javax.ejb.SessionContext cntx) {
    ejbContext = cntx;
    try {
        jndiContext = new InitialContext();
    } catch(NamingException ne) {
        throw new EJBException(ne);
    }
}

The EJB container makes special accommodations for references to SessionContext, the JNDI ENC, references to other beans (remote and home interface types), and the JTA UserTransaction type, which is discussed in detail in Chapter 14. The container must maintain any instance fields that reference objects of these types as part of the conversational state, even if they are not serializable. All other fields must be serializable or null when the bean is passivated.

The TravelAgent EJB has methods for setting the desired cruise and cabin. These methods take Integer IDs as arguments and retrieve references to the appropriate Cruise or Cabin EJB from the appropriate home interface. These references are also part of the TravelAgent EJB’s conversational state.

Here’s how setCabinID() and getCabinID() are used in EJB 2.0:

public void setCabinID(Integer cabinID) 
    throws javax.ejb.FinderException { 
    try { 
        CabinHomeLocal home = (CabinHomeLocal)
            jndiContext.lookup("java:comp/env/ejb/CabinHomeLocal");
            
        cabin = home.findByPrimaryKey(cabinID);
    } catch(RemoteException re) {
        throw new EJBException(re);
    }
}
public void setCruiseID(Integer cruiseID) 
    throws javax.ejb.FinderException { 
    try { 
        CruiseHomeLocal home = (CruiseHomeLocal)
            jndiContext.lookup("java:comp/env/ejb/CruiseHomeLocal");
           
        cruise = home.findByPrimaryKey(cruiseID);
    } catch(RemoteException re) {
        throw new EJBException(re);
    }     
}

Here’s how setCabinID() and getCabinID() are used in EJB 1.1:

public void setCabinID(Integer cabinID) 
    throws javax.ejb.FinderException { 
    try { 
        CabinHomeRemote home = (CabinHome)
            getHome("CabinHomeRemote", CabinHome.class);
        cabin = home.findByPrimaryKey(cabinID);
    } catch(RemoteException re) {
        throw new EJBException(re);
    }
}
public void setCruiseID(Integer cruiseID) 
    throws javax.ejb.FinderException { 
    try { 
        CruiseHome home = (CruiseHome)
            getHome("CruiseHomeRemote", CruiseHome.class);
        cruise = home.findByPrimaryKey(cruiseID);
    } catch(RemoteException re) {
        throw new EJBException(re);
    }
}

It may seem strange that we set these values using the Integer IDs, but we keep them in the conversational state as entity bean references. Using the Integer IDs for these objects is simpler for the client, which does not work with their entity bean references. In the client code, we get the cabin and cruise IDs from text fields. Why make the client obtain a bean reference to the Cruise and Cabin EJBs when an ID is simpler? In addition, using the IDs is cheaper than passing a remote reference in terms of network traffic. We need the EJB object references to these bean types in the bookPassage() method, so we use their IDs to obtain actual entity bean references. We could have waited until the bookPassage() method was invoked before reconstructing the remote references, but this way we keep the bookPassage() method simple.

JNDI ENC and EJB references

You can use the JNDI ENC to obtain a reference to the home interfaces of other beans. Using the ENC lets you avoid hardcoding vendor-specific JNDI properties into the bean. In other words, the JNDI ENC allows EJB references to be network and vendor independent.

In the EJB 2.0 listing for the TravelAgentBean, we used the JNDI ENC to access both the remote home interface of the ProcessPayment EJB and the local home interfaces of the Cruise and Cabin EJBs. This illustrates the flexibility of the JNDI ENC, which can provide directories for both local and remote enterprise beans.

In the EJB 1.1 listing for the TravelAgentBean class, getHome() is a convenience method that hides the details of obtaining remote references to EJB home objects. The getHome() method uses the jndiContext reference to obtain references to the Cabin, Ship, ProcessPayment, and Cruise EJB home objects.

The EJB specification recommends that all EJB references be bound to the "java:comp/env/ejb" context, which is the convention followed here. In the TravelAgent EJB, we pass in the name of the home object we want and append it to the "java:comp/env/ejb" context to do the lookup.

Remote EJB references in the JNDI ENC

The deployment descriptor provides a special set of tags for declaring remote EJB references. Here’s how the <ejb-ref> tag and its subelements are used:

<ejb-ref>
    <ejb-ref-name>ejb/ProcessPaymentHomeRemote</ejb-ref-name>
    <ejb-ref-type>Session</ejb-ref-type>
    <home>
        com.titan.processpayment.ProcessPaymentHomeRemote
    </home>
    <remote>
       com.titan.processpayment.ProcessPaymentRemote
    </remote>
</ejb-ref>

These elements should be self-explanatory: they define a name for the bean within the ENC, declare the bean’s type, and give the names of its remote and home interfaces. When a bean is deployed, the deployer maps the <ejb-ref> elements to actual beans in a way specific to the vendor. The <ejb-ref> elements can also be linked by the application assembler to beans in the same deployment (a subject covered in detail in Chapter 16). EJB 2.0 developers should try to use local component interfaces for beans located in the same deployment and container.

At deployment time, the EJB container’s tools map the remote references declared in the <ejb-ref> elements to enterprise beans, which might located on the same machine or at a different node on the network.

EJB 2.0: Local EJB references in the JNDI ENC

The deployment descriptor also provides a special set of tags, the <ejb-local-ref> elements, to declare local EJB references: enterprise beans that are co-located in the same container and deployed in the same EJB JAR file. The <ejb-local-ref> elements are declared immediately after the <ejb-ref> elements:

<ejb-local-ref>
    <ejb-ref-name>ejb/CruiseHomeLocal</ejb-ref-name>
    <ejb-ref-type>Entity</ejb-ref-type>
    <local-home>
        com.titan.cruise.CruiseHomeLocal
    </local-home>
    <local>
        com.titan.cruise.CruiseLocal
    </local>
    <ejb-link>CruiseEJB</ejb-link>
</ejb-local-ref>
<ejb-local-ref>
    <ejb-ref-name>ejb/CabinHomeLocal</ejb-ref-name>
    <ejb-ref-type>Entity</ejb-ref-type>
    <local-home>
        com.titan.cabin.CabinHomeLocal
    </local-home>
    <local>
        com.titan.cabin.CabinLocal
    </local>
    <ejb-link>CabinEJB</ejb-link>
</ejb-local-ref>

The <ejb-local-ref> element defines a name for the bean within the ENC, declares the bean’s type, and gives the names of its local component interfaces. These elements should be linked explicitly to other co-located beans using the <ejb-link> element, although linking them is not strictly required at this stage—the application assembler or deployer can do it later. The value of the <ejb-link> element within the <ejb-local-ref> must equal the <ejb-name> of the appropriate bean in the same JAR file.

At deployment time the EJB container’s tools map the local references declared in the <ejb-local-ref> elements to entity beans that are co-located in the same container system.

The bookPassage( ) method

The last point of interest in our bean definition is the bookPassage() method. This method leverages the conversational state accumulated by the ejbCreate(), setCabinID(), and setCruiseID() methods to process a reservation for a customer.

Here’s how the bookPassage() method is used in EJB 2.0:

public TicketDO bookPassage(CreditCardDO card, double price)
    throws IncompleteConversationalState {
                   
    if (customer == null || cruise == null || cabin == null) {
        throw new IncompleteConversationalState();
    }
    try {
        ReservationHomeLocal resHome = (ReservationHomeLocal)
            jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal");
            
        ReservationLocal reservation =
            resHome.create(customer, cruise, cabin, price, new Date());
                
        Object ref = jndiContext.lookup("java:comp/env/ejb/
            ProcessPaymentHomeRemote");

        ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote)
            PortableRemoteObject.narrow(ref, ProcessPaymentHomeRemote.class);
            
        ProcessPaymentRemote process = ppHome.create();
        process.byCredit(customer, card, price);

        TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
        return ticket;
    } catch(Exception e) {
        throw new EJBException(e);
    }
}

Here’s how the bookPassage() method is used in EJB 1.1:

public TicketDO bookPassage(CreditCardDO card, double price)
    throws IncompleteConversationalState {
                   
    if (customer == null || cruise == null || cabin == null) {
        throw new IncompleteConversationalState();
    }
    try {
        ReservationHomeRemote resHome = (ReservationHomeRemote)
        getHome("ReservationHomeRemote", ReservationHomeRemote.class);
        ReservationRemote reservation =
            resHome.create(customer, cruise, cabin, price, new Date());
        ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote) 
            getHome("ProcessPaymentHomeRemote", ProcessPaymentHomeRemote.class);
        ProcessPaymentRemote process = ppHome.create();
        process.byCredit(customer, card, price);

        TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
        return ticket;
    } catch(Exception e) {
        // EJB 1.0: throw new RemoteException("",e);
        throw new EJBException(e);
    }
}

This method exemplifies the workflow concept. It uses several beans, including the Reservation, ProcessPayment, Customer, Cabin, and Cruise EJBs, to accomplish one task: booking a customer on a cruise. Deceptively simple, this method encapsulates several interactions that ordinarily might have been performed on the client. For the price of one bookPassage() call from the client, the TravelAgent EJB performs many operations:

  1. Looks up and obtains a reference to the Reservation EJB’s home.

  2. Creates a new Reservation EJB.

  3. Looks up and obtains a remote reference to the ProcessPayment EJB’s home.

  4. Creates a new ProcessPayment EJB.

  5. Charges the customer’s credit card using the ProcessPayment EJB.

  6. Generates a new TicketDO with all the pertinent information describing the customer’s purchase.

From a design standpoint, encapsulating the workflow in a stateful session bean means a less complex interface for the client and more flexibility for implementing changes. We could, for example, easily change the bookPassage() method to check for overlapped booking (when a customer books passage on two different cruises that overlap). This type of enhancement would not change the remote interface, so the client application would not need modification. Encapsulating workflow in stateful session beans allows the system to evolve over time without impacting clients.

In addition, the type of clients used can change. One of the biggest problems with two-tier architectures—besides scalability and transactional control—is that the business logic is intertwined with the client logic. This makes it difficult to reuse the business logic in a different kind of client. With stateful session beans this is not a problem, because stateful session beans are an extension of the client but are not bound to the client’s presentation. Let’s say that our first implementation of the reservation system used a Java applet with GUI widgets. The TravelAgent EJB would manage conversational state and perform all the business logic while the applet focused on the GUI presentation. If, at a later date, we decide to go to a thin client (HTML generated by a Java servlet, for example), we would simply reuse the TravelAgent EJB in the servlet. Because all the business logic is in the stateful session bean, the presentation ( Java applet or servlet or something else) can change easily.

The TravelAgent EJB also provides transactional integrity for processing the customer’s reservation. If any one of the operations within the body of the bookPassage() method fails, all the operations are rolled back so that none of the changes are accepted. If the credit card cannot be charged by the ProcessPayment EJB, the newly created Reservation EJB and its associated record are not created. The transactional aspects of the TravelAgent EJB are explained in detail in Chapter 14.

Tip

In EJB 2.0, remote and local EJB references can be used within the same workflow. For example, the bookPassage() method uses local references when accessing the Cruise and Cabin beans, but remote references when accessing the ProcessPayment and Customer EJBs. This usage is totally appropriate. The EJB container ensures that the transaction is atomic, i.e., that failures in either the remote or local EJB reference will impact the entire transaction.

Why use a Reservation entity bean?

If we have a Reservation EJB, why do we need a TravelAgent EJB? Good question! The TravelAgent EJB uses the Reservation EJB to create a reservation, but it also has to charge the customer and generate a ticket. These activities are not specific to the Reservation EJB, so they need to be captured in a stateful session bean that can manage workflow and transactional scope. In addition, the TravelAgent EJB provides listing behavior, which spans concepts in Titan’s system. It would have been inappropriate to include any of these other behaviors in the Reservation entity bean. (For EJB 2.0 readers, the Reservation EJB was developed in Chapter 7. For EJB 1.1 readers, the code for this bean is available at http://www.oreilly.com/catalog/entjbeans3/.)

listAvailableCabins( ): Listing behavior

As promised, we are going to bring back the cabin-listing behavior we played around with in Chapter 4. This time, however, we are not going to use the Cabin EJB to get the list; instead, we will access the database directly. Accessing the database directly is a double-edged sword. On one hand, we don’t want to access the database directly if entity beans exist that can access the same information. Entity beans provide a safe and consistent interface for a particular set of data. Once an entity bean has been tested and proven, it can be reused throughout the system, substantially reducing data-integrity problems. The Reservation EJB is an example of that kind of usage. Entity beans can also pull together disjointed data and apply additional business logic such as validation, limits, and security to ensure that data access follows the business rules.

But entity beans cannot define every possible data access needed, and they shouldn’t. One of the biggest problems with entity beans is that they tend to become bloated over time. Huge entity beans containing dozens of methods are a sure sign of poor design. Entity beans should be focused on providing data access to a very limited, but conceptually bound, set of data. You should be able to update, read, and insert records or data. Data access that spans concepts, like listing behavior, should not be encapsulated in one entity bean.

Systems always need listing behavior to present clients with choices. In the reservation system, for example, customers need to choose a cabin from a list of available cabins. The word available is key to the definition of this behavior. The Cabin EJB can provide us with a list of cabins, but it does not know whether any given cabin is available. As you may recall, the Cabin-Reservation relationship we defined for EJB 2.0 in Chapter 7 was unidirectional: the Reservation was aware of its Cabin relationships, but the reverse was not true.

The question of whether a cabin is available is relevant to the process using it—in this case, the TravelAgent EJB—but may not be relevant to the cabin itself. As an analogy, an automobile entity would not care what road it is on; it is concerned only with characteristics that describe its state and behavior. An automobile-tracking system, on the other hand, would be concerned with the locations of individual automobiles.

To get availability information, we need to compare the list of cabins on our ship to the list of cabins that have already been reserved. The listAvailableCabins() method does exactly that. It uses a complex SQL query to produce a list of cabins that have not yet been reserved for the cruise chosen by the client:

public String [] listAvailableCabins(int bedCount)
    throws IncompleteConversationalState { 
    if (cruise == null) 
        throw new IncompleteConversationalState();

    Connection con = null;
    PreparedStatement ps = null;;
    ResultSet result = null;
    try {
        Integer cruiseID = (Integer)cruise.getPrimaryKey();
        Integer shipID = (Integer)cruise.getShip().getPrimaryKey();
        con = getConnection();
        ps = con.prepareStatement(
            "select ID, NAME, DECK_LEVEL  from CABIN "+
            "where SHIP_ID = ? and BED_COUNT = ? and ID NOT IN "+
            "(SELECT CABIN_ID FROM RESERVATION "+" WHERE CRUISE_ID = ?)");

        ps.setInt(1,shipID.intValue());
        ps.setInt(2, bedCount);
        ps.setInt(3,cruiseID.intValue());
        result = ps.executeQuery();
        Vector vect = new Vector();
        while(result.next()) {
            StringBuffer buf = new StringBuffer();
            buf.append(result.getString(1));
            buf.append(','),
            buf.append(result.getString(2));
            buf.append(','),
            buf.append(result.getString(3));
            vect.addElement(buf.toString());
        }
        String [] returnArray = new String[vect.size()];
        vect.copyInto(returnArray);
        return returnArray;
    } catch (Exception e) {
        throw new EJBException(e);
    }
    finally {
        try {
            if (result != null) result.close();
            if (ps != null) ps.close();
            if (con!= null) con.close();
        } catch(SQLException se){se.printStackTrace();}
    }
}

EJB 1.1 readers use almost exactly the same code for listAvailableCabins(), but obtain the Ship EJB’s ID differently. EJB 1.1 readers should replace the line:

Integer shipID = (Integer)cruise.getShip().getPrimaryKey();

With the line:

Integer shipID = cruise.getShipID();

This change is necessary because EJB 1.1 does not support relationship fields.

As you can see, the SQL query is complex. It could have been defined using a method like Cabin.findAvailableCabins(Cruise cruise) in the Cabin EJB. However, this method would be difficult to implement because the Cabin EJB would need to access the Reservation EJB’s data. Another reason for accessing the database directly in this example is to demonstrate that this kind of behavior is both normal and, in some cases, preferred. Sometimes the query is fairly specific to the scenario and is not reusable. To avoid adding find methods for every possible query, you can instead simply use direct database access as shown in the listAvailableCabins() method. Direct database access generally has less impact on performance because the container does not have to manifest EJB object references, but it is also less reusable. These things must be considered when deciding if a query for information should be done using direct database access or if a new find method should be defined.

The listAvailableCabins() method returns an array of String objects to the remote client. We could have opted to return an collection of remote Cabin references, but we didn’t. The reason is simple: we want to keep the client application as lightweight as possible. A list of String objects is much more lightweight than the alternative, a collection of remote references. In addition, using a collection of remote would require the client to work with many stubs, each with its own connection to EJB objects on the server. By returning a lightweight String array we reduce the number of stubs on the client, which keeps the client simple and conserves resources on the server.

To make this method work, you need to create a getConnection() method for obtaining a database connection and add it to the TravelAgentBean:

private Connection getConnection() throws SQLException {
    try {
        DataSource ds = (DataSource)jndiContext.lookup(
            "java:comp/env/jdbc/titanDB");
        return ds.getConnection();
    } catch(NamingException ne) {
        throw new EJBException(ne);
    }
}

Change the remote interface for TravelAgent EJB to include the listAvailableCabins() method:

package com.titan.travelagent;

import java.rmi.RemoteException;
import javax.ejb.FinderException;
import com.titan.processpayment.CreditCard;

public interface TravelAgentRemote extends javax.ejb.EJBObject {

    public void setCruiseID(Integer cruise) throws RemoteException, FinderException;

    public void setCabinID(Integer cabin) throws RemoteException, FinderException;

    public TicketDO bookPassage(CreditCardDO card, double price)
        throws RemoteException,IncompleteConversationalState;   
               
    public String [] listAvailableCabins(int bedCount)
        throws RemoteException, IncompleteConversationalState;
}

EJB 2.0: The TravelAgent deployment descriptor

The following listing is an abbreviated version of the XML deployment descriptor used for the TravelAgent EJB. It defines not only the TravelAgent EJB, but also the Customer, Cruise, Cabin, and Reservation EJBs. The ProcessPayment EJB is not defined in this deployment descriptor because it is assumed to be deployed in a separate JAR file, or possibly even an EJB server on a different network node:

<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise
JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd">

<ejb-jar>
    <enterprise-beans>
        <session>
            <ejb-name>TravelAgentEJB</ejb-name>
            <home>com.titan.travelagent.TravelAgentHomeRemote</home>
            <remote>com.titan.travelagent.TravelAgentRemote</remote>
            <ejb-class>com.titan.travelagent.TravelAgentBean</ejb-class>
            <session-type>Stateful</session-type>
            <transaction-type>Container</transaction-type>

            <ejb-ref>
                <ejb-ref-name>ejb/ProcessPaymentHomeRemote</ejb-ref-name>
                <ejb-ref-type>Session</ejb-ref-type>
                <home>com.titan.processpayment.ProcessPaymentHomeRemote</home>
                <remote>com.titan.processpayment.ProcessPaymentRemote</remote>
            </ejb-ref>
            <ejb-local-ref>
                <ejb-ref-name>ejb/CabinHomeLocal</ejb-ref-name>
                <ejb-ref-type>Entity</ejb-ref-type>
                <local-home>com.titan.cabin.CabinHomeLocal</local-home>
                <local>com.titan.cabin.CabinLocal</local>
            </ejb-local-ref>
            <ejb-local-ref>
                <ejb-ref-name>ejb/CruiseHomeLocal</ejb-ref-name>
                <ejb-ref-type>Entity</ejb-ref-type>
                <local-home>com.titan.cruise.CruiseHomeLocal</local-home>
                <local>com.titan.cruise.CruiseLocal</local>
            </ejb-local-ref>
            <ejb-local-ref>
                <ejb-ref-name>ejb/ReservationHomeLocal</ejb-ref-name>
                <ejb-ref-type>Entity</ejb-ref-type>
                <local-home>com.titan.reservation.ReservationHomeLocal</local-home>
               <local>com.titan.reservation.ReservationLocal</local>
            </ejb-local-ref>

            <resource-ref>
                <description>DataSource for the Titan database</description>
                <res-ref-name>jdbc/titanDB</res-ref-name>
                <res-type>javax.sql.DataSource</res-type>
                <res-auth>Container</res-auth>
           </resource-ref>
        </session>
        <entity>
            <ejb-name>CabinEJB</ejb-name>
            <local-home>com.titan.cabin.CabinHomeLocal</local-home>
            <local>com.titan.cabin.CabinLocal</local>
            ...
        </entity>
        <entity>
            <ejb-name>CruiseEJB</ejb-name>
            <local-home>com.titan.cruise.CruiseHomeLocal</local-home>
            <local>com.titan.cruise.CruiseLocal</local>
            ...
        </entity>
        <entity>
            <ejb-name>ReservationEJB</ejb-name>
            <local-home>com.titan.reservation.ReservationHomeLocal</local-home>
            <local>com.titan.reservation.ReservationLocal</local>
            ... 
        </entity>   
    </enterprise-beans>

    <assembly-descriptor>
        <security-role>
            <description>This role represents everyone</description>
            <role-name>everyone</role-name>
        </security-role>

        <method-permission>
            <role-name>everyone</role-name>
            <method>
                <ejb-name>TravelAgentEJB</ejb-name>
                <method-name>*</method-name>
            </method>
        </method-permission>

        <container-transaction>
            <method>
                <ejb-name>TravelAgentEJB</ejb-name>
                <method-name>*</method-name>
            </method>
            <trans-attribute>Required</trans-attribute>
        </container-transaction>
    </assembly-descriptor>
</ejb-jar>

EJB 1.1: The TravelAgent deployment descriptor

Use the following XML deployment descriptor when deploying the TravelAgent EJB in EJB 1.1. The most important differences between this descriptor and the deployment descriptor used for the ProcessPayment EJB are the <session-type> tag, which states that this bean is stateful, and the use of the <ejb-ref> elements to describe beans that are referenced through the ENC:

<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise
JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd">

<ejb-jar>
    <enterprise-beans>
        <session>
            <description>
                Acts as a travel agent for booking passage on a ship.
            </description>
            <ejb-name>TravelAgentEJB</ejb-name>
            <home>com.titan.travelagent.TravelAgentHomeRemote</home>
            <remote>com.titan.travelagent.TravelAgentRemote</remote>
            <ejb-class>com.titan.travelagent.TravelAgentBean</ejb-class>
            <session-type>Stateful</session-type>
            <transaction-type>Container</transaction-type>
      
            <ejb-ref>
                <ejb-ref-name>ejb/ProcessPaymentHomeRemote</ejb-ref-name>
                <ejb-ref-type>Session</ejb-ref-type>
                <home>com.titan.processpayment.ProcessPaymentHome</home>
                <remote>com.titan.processpayment.ProcessPayment</remote>
            </ejb-ref>
            <ejb-ref>
                <ejb-ref-name>ejb/CabinHomeRemote</ejb-ref-name>
                <ejb-ref-type>Entity</ejb-ref-type>
                <home>com.titan.cabin.CabinHome</home>
                <remote>com.titan.cabin.Cabin</remote>
            </ejb-ref>
            <ejb-ref>
                <ejb-ref-name>ejb/CruiseHomeRemote</ejb-ref-name>
                <ejb-ref-type>Entity</ejb-ref-type>
                <home>com.titan.cruise.CruiseHome</home>
                <remote>com.titan.cruise.Cruise</remote>
            </ejb-ref>
            <ejb-ref>
                <ejb-ref-name>ejb/CustomerHomeRemote</ejb-ref-name>
                <ejb-ref-type>Entity</ejb-ref-type>
                <home>com.titan.customer.CustomerHome</home>
                <remote>com.titan.customer.Customer</remote>
            </ejb-ref>
            <ejb-ref>
                <ejb-ref-name>ejb/ReservationHomeRemote</ejb-ref-name>
                <ejb-ref-type>Entity</ejb-ref-type>
                <home>com.titan.reservation.ReservationHome</home>
                <remote>com.titan.reservation.Reservation</remote>
            </ejb-ref>

            <resource-ref>
                <description>DataSource for the Titan database</description>
                <res-ref-name>jdbc/titanDB</res-ref-name>
                <res-type>javax.sql.DataSource</res-type>
                <res-auth>Container</res-auth>
            </resource-ref>
        </session>
    </enterprise-beans>
 
    <assembly-descriptor>
        <security-role>
            <description>This role represents everyone</description>
            <role-name>everyone</role-name>
        </security-role>

        <method-permission>
            <role-name>everyone</role-name>
            <method>
                <ejb-name>TravelAgentEJB</ejb-name>
                <method-name>*</method-name>
            </method>
        </method-permission>

        <container-transaction>
            <method>
                <ejb-name>TravelAgentEJB</ejb-name>
                <method-name>*</method-name>
            </method>
            <trans-attribute>Required</trans-attribute>
        </container-transaction>
    </assembly-descriptor>
</ejb-jar>

Once you have generated the deployment descriptor, jar the TravelAgent EJB and deploy it in your EJB server. You will also need to deploy the Reservation, Cruise, and Customer EJBs you downloaded earlier. Based on the business methods in the remote interface of the TravelAgent EJB and your past experiences with the Cabin, Ship, and ProcessPayment EJBs, you should be able to create your own client application to test this code.

EJB 1.1: The TravelAgent deployment descriptorPlease refer to Workbook Exercise 12.2, A Stateful Session Bean. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks.



[43] This is a conceptual model. Some EJB containers may actually use instance swapping with stateful session beans but make it appear as if the same instance is servicing all requests. Conceptually, however, the same stateful session bean instance services all requests.

[44] This is also a conceptual model. Some EJB containers may use separate EJB objects for concurrent access to the same entity, relying on the database to control concurrency. Conceptually, however, the end result is the same.

[45] If you are modifying the bean developed in Chapter 4, remember to delete the listCabin() method. We will add a new implementation of that method later in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset