home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


CONTENTS

Chapter 12. Session Beans

As Chapter 6 thorough Chapter 11 demonstrated, entity beans provide an object-oriented interface that makes it easier for developers to create, modify, and delete data from the database. Entity beans allow developers to be more productive by encouraging reuse and reducing development costs. You can reuse a concept like a Ship throughout a business system without having to redefine, recode, or retest the business logic and data access.

However, entity beans are not the entire story. We have also seen another kind of enterprise bean: the session bean. Session beans fill the gaps left by entity beans. They are useful for describing interactions between other beans (workflow) and for implementing particular tasks. Unlike entity beans, session beans do not represent shared data in the database, but they can access shared data. This means that we can use session beans to read, update, and insert data. For example, we might use a session bean to provide lists of information, such as a list of all available cabins. Sometimes we might generate the list by interacting with entity beans, like the cabin list we developed in the TravelAgent EJB in Chapter 4. More frequently, session beans will generate lists by accessing the database directly.

So when do you use an entity bean and when do you use a session bean to directly access data? Good question! As a rule of thumb, an entity bean is developed to provide a safe and consistent interface to a set of shared data that defines a concept. This data may be updated frequently. Session beans access data that spans concepts, is not shared, and is usually read-only.

In addition to accessing data directly, session beans can represent workflow. Workflow describes all the steps required to accomplish a particular task, such as booking passage on a ship or renting a video. Session beans are part of the same business API as entity beans, but as workflow components, they serve a different purpose. Session beans can manage the interactions between entity beans, describing how they work together to accomplish a specific task. The relationship between session beans and entity beans is like the relationship between a script for a play and the actors that perform the play. Where entity beans are the actors, the session bean is the script. Actors without a script can each serve a function individually, but only in the context of a script can they tell a story. In terms of our example, it makes no sense to have a database full of cabins, ships, customers, and other entities if we can't create interactions between them, such as booking a customer for a cruise.

Session beans are divided into two basic types: stateless and stateful. A stateless session bean is a collection of related services, each represented by a method; the bean maintains no state from one method invocation to the next. When you invoke a method on a stateless session bean, it executes the method and returns the result without knowing or caring what other requests have gone before or might follow. Think of a stateless session bean as a set of procedures or batch programs that execute a request based on some parameters and return a result. Stateless session beans tend to be general-purpose or reusable, such as a software service.

A stateful session bean is an extension of the client application. It performs tasks on behalf of the client and maintains state related to that client. This state is called conversational state because it represents a continuing conversation between the stateful session bean and the client. Methods invoked on a stateful session bean can write and read data to and from this conversational state, which is shared among all methods in the bean. Stateful session beans tend to be specific to one scenario. They represent logic that might have been captured in the client application of a two-tier system. Session beans, whether they are stateful or stateless, are not persistent like entity beans. In other words, session beans are not saved to the database.

Depending on the vendor, stateful session beans may have a timeout period. If the client fails to use the stateful bean before it times out, the bean instance is destroyed and the EJB object reference is invalidated. This prevents the stateful session bean from lingering long after a client has shut down or otherwise finished using it. A client can also explicitly remove a stateful session bean by calling one of its remove methods.

Stateless session beans have longer lives because they do not retain any conversational state and are not dedicated to one client, but they still are not saved in a database because they do not represent any data. Once a stateless session bean has finished a method invocation for a client, it can be reassigned to any other EJB object to service a new client. A client can maintain a connection to a stateless session bean's EJB object, but the bean instance itself is free to service requests from any client. Because it does not contain any state information, a stateless session bean does not distinguish between clients. Stateless session beans may also have a timeout period and can be removed by the client, but the impact of these events is different than with a stateful session bean. With a stateless session bean, a timeout or remove operation simply invalidates the EJB object reference for that client; the bean instance is not destroyed and is free to service other client requests.

12.1 The Stateless Session Bean

A stateless session bean is very efficient and relatively easy to develop. Stateless session beans require few server resources because they are neither persistent nor dedicated to one client. Because they are not dedicated to one client, many EJB objects can share a few instances of a stateless bean. A stateless session bean does not maintain conversational state relative to the client it is servicing, so it can be swapped freely between EJB objects. As soon as a stateless instance finishes servicing a method invocation, it can be swapped to another EJB object. Because it does not maintain conversational state, a stateless session bean does not require passivation or activation, further reducing the overhead of swapping. In short, stateless session beans are lightweight and fast.

Stateless session beans often perform services that are fairly generic and reusable. The services may be related, but they are not interdependent. This means that everything a method needs to know has to be passed via the method's parameters. This provides an interesting limitation. Stateless session beans can't remember anything from one method invocation to the next, which means that they have to take care of the entire task in one method invocation. The only exception to this rule is information obtainable from the SessionContext and the JNDI ENC.

Stateless session beans are EJB's version of the traditional transaction-processing applications, which are executed using a procedure call. The procedure executes from beginning to end and then returns the result. Once the procedure is done, nothing about the data that was manipulated or the details of the request are remembered. There is no state.

These restrictions don't mean that a stateless session bean can't have instance variables or maintain any kind of internal state. Nothing prevents you from keeping a variable that tracks the number of times a bean has been called or that saves data for debugging. An instance variable can even hold a reference to a live resource, such as a URL connection for logging, verifying credit cards, or anything else that might be useful—the resource should be obtained from the JNDI ENC. However, it is important to remember that this state can never be visible to a client. A client can't assume that the same bean instance will service it every time. If these instance variables have different values in different bean instances, their values will appear to change randomly as stateless session beans are swapped from one client to another. Therefore, any resources you reference in instance variables should be generic. For example, each bean instance might reasonably record debugging messages—that might be the only way to figure out what is happening on a large server with many bean instances. The client doesn't know or care where debugging output is going. However, it would clearly be inappropriate for a stateless bean to remember that it was in the process of making a reservation for Madame X—the next time it is called, it may be servicing another client entirely.

Stateless session beans can be used for report generation, batch processing, or some stateless services such as validating credit cards. Another good application would be a StockQuote EJB that returns a stock's current price. Any activity that can be accomplished in one method call is a good candidate for the high-performance stateless session bean.

12.1.1 The ProcessPayment EJB

Chapter 2 and Chapter 3 discussed the TravelAgent EJB, which has a business method called bookPassage() that uses the ProcessPayment EJB. The next section develops a complete definition of the TravelAgent EJB, including the logic of the bookPassage() method. At this point, however, we are primarily interested in the ProcessPayment EJB, which is a stateless bean the TravelAgent EJB uses to charge the customer for the price of the cruise.

Charging customers is a common activity in Titan's business systems. Not only does the reservation system need to charge customers, but so do Titan's gift shops, boutiques, and other related businesses. The process of charging a customer for services is common to many systems, so it has been encapsulated in its own bean.

Payments are recorded in a special database table called PAYMENT. The PAYMENT data is batch processed for accounting purposes and is not normally used outside of accounting. In other words, the data is only inserted by Titan's system; it is not read, updated, or deleted. Because the process of making a charge can be completed in one method, and because the data is not updated frequently or shared, we will use a stateless session bean for processing payments. Several different forms of payment can be used: credit card, check, or cash. We will model these payment forms in our stateless ProcessPayment EJB.

12.1.1.1 PAYMENT: The database table

The ProcessPayment EJB accesses an existing table in Titan's system called the PAYMENT table. Create a table in your database called PAYMENT with this definition:

CREATE TABLE PAYMENT 
(
    customer_id     INTEGER, 
    amount          DECIMAL(8,2), 
    type            CHAR(10), 
    check_bar_code  CHAR(50),
    check_number    INTEGER,
    credit_number   CHAR(20), 
    credit_exp_date DATE
)
12.1.1.2 ProcessPaymentRemote: The remote interface

A stateless session bean, like any other bean, needs a component interface. While EJB 1.1 uses only remote interfaces, in EJB 2.0 session beans may have either a local or remote interface.

For the remote interface, we obviously need a byCredit() method because the TravelAgent EJB uses it. We can also identify two other methods that we'll need: byCash() for customers paying cash and byCheck() for customers paying with a personal check.

Here is a complete definition of the remote interface for the ProcessPayment EJB:

package com.titan.processpayment;

import java.rmi.RemoteException;
import java.util.Date;
import com.titan.customer.CustomerRemote;

public interface ProcessPaymentRemote extends javax.ejb.EJBObject {

    public boolean byCheck(CustomerRemote customer, CheckDO check, double amount)
        throws RemoteException,PaymentException;

    public boolean byCash(CustomerRemote customer, double amount)
        throws RemoteException,PaymentException;

    public boolean byCredit(CustomerRemote customer, CreditCardDO card, 
        double amount) throws RemoteException,PaymentException;
}

Remote interfaces in session beans follow the same rules as in entity beans. Here we have defined the three business methods, byCheck(), byCash(), and byCredit(), which take information relevant to the form of payment used and return a boolean value that indicates the success of the payment. In addition to the required RemoteException, these methods can throw an application-specific exception, the PaymentException. The PaymentException is thrown if any problems occur while processing the payment, such as a low check number or an expired credit card. Notice, however, that nothing about the ProcessPaymentRemote interface is specific to the reservation system. It could be used just about anywhere in Titan's system. In addition, each method defined in the remote interface is completely independent of the others. All the data that is required to process a payment is obtained through the method's arguments.

As an extension of the javax.ejb.EJBObject interface, the remote interface of a session bean inherits the same functionality as the remote interface of an entity bean. However, the getPrimaryKey() method throws a RemoteException, since session beans do not have a primary key to return:

public interface javax.ejb.EJBObject extends java.rmi.Remote {
    public abstract EJBHome getEJBHome() throws RemoteException;
    public abstract Handle getHandle() throws RemoteException;
    public abstract Object getPrimaryKey() throws RemoteException;
    public abstract boolean isIdentical(EJBObject obj) throws RemoteException;
    public abstract void remove() throws RemoteException, RemoveException;
}

The getHandle() method returns a serializable Handle object, just like the getHandle() method in the entity bean. For stateless session beans, this Handle can be serialized and reused any time, as long as the stateless bean type is still available in the container that generated the Handle.

Unlike stateless session beans, stateful session beans are available through the Handle for only as long as that specific bean instance is kept alive on the EJB server. If the client explicitly destroys the stateful session bean using one of the remove() methods, or if the bean times out, the instance is destroyed and the Handle becomes invalid. As soon as the server removes a stateful session bean, its Handle is no longer valid and will throw a RemoteException when its getEJBObject() is invoked.

You can obtain a remote reference to the bean from the Handle by invoking its getEJBObject() method:

public interface javax.ejb.Handle {
    public abstract EJBObject getEJBObject() throws RemoteException;
}

The ProcessPayment EJB has its own package, which means it has its own directory in our development tree, dev/com/titan/processpayment. That's where we'll store all the code and compile class files for this bean.

12.1.1.3 Dependent objects: The CreditCardDO and CheckDO classes

The ProcessPayment EJB's remote interface uses two classes in its definition that are particularly interesting: CreditCardDO and CheckDO. The definitions for these classes are as follows:

/* CreditCardDO.java */
package com.titan.processpayment;

import java.util.Date;

public class CreditCardDO implements java.io.Serializable {
    final static public String MASTER_CARD = "MASTER_CARD";
    final static public String VISA = "VISA";
    final static public String AMERICAN_EXPRESS = "AMERICAN_EXPRESS";
    final static public String DISCOVER = "DISCOVER";
    final static public String DINERS_CARD = "DINERS_CARD";

    public String number;
    public Date expiration;
    public String type;

    public CreditCardDO(String nmbr, Date exp, String typ) {
        number = nmbr;
        expiration = exp;
        type = typ;
    }
}

/* CheckDO.java */
package com.titan.processpayment;

public class CheckDO implements java.io.Serializable {
    public String checkBarCode;
    public int checkNumber;

    public CheckDO(String barCode, int number) {
        checkBarCode = barCode;
        checkNumber = number;
    }
}

CreditCardDO and CheckDO are dependent objects a concept we explored with the Address EJB in Chapter 6. If you examine the class definitions of the CreditCardDO and CheckDO classes, you will see that they are not enterprise beans; they are simply serializable Java classes. These classes provide a convenient mechanism for transporting and binding together related data. CreditCardDO, for example, binds all the credit card data together in one class, making it easier to pass the information across the network as well as making our interfaces a little cleaner.

12.1.1.4 PaymentException: An application exception

Any remote or local interface, whether it's for an entity bean or a session bean, can throw application exceptions. Application exceptions are created by the bean developer and should describe a business logic problem—in this case, a problem making a payment. Application exceptions should be meaningful to the client, providing a brief and relevant identification of the error.

It is important to understand what exceptions to use and when to use them. The RemoteException indicates subsystem-level problems and is used by the RMI facility. Likewise, exceptions such as javax.naming.NamingException and java.sql.SQLException are thrown by other Java subsystems; usually these should not be thrown explicitly by your beans. The Java Compiler requires that you use try/catch blocks to capture checked exceptions like these.

In EJB 2.0, the EJBException can express problems the container has with processing local interface invocations. The EJBException is an unchecked exception, so you won't get a compile error if you don't write code to handle it. However, under certain circumstances it is a good idea to catch EJBException, and in other circumstances it should be propagated.

When a checked exception from a subsystem ( JDBC, JNDI, JMS, etc.) is caught by a bean method, it should be rethrown as an EJBException or an application exception. You would rethrow a checked exception as an EJBException if it represented a system-level problem; checked exceptions are rethrown as application exceptions when they result from business logic problems. Your beans incorporate your business logic; if a problem occurs in the business logic, that problem should be represented by an application exception. When the enterprise bean throws an EJBException or some other type of RuntimeException, the exception is first processed by the container, which discards the bean instance and replaces it with another. After the container processes the exception, it propagates an exception to the client. For remote clients, the container throws a RemoteException; for local clients (co-located enterprise beans), the container rethrows the original EJBException or RuntimeException thrown by the bean instance.

The PaymentException describes a specific business problem, so it is an application exception. Application exceptions extend java.lang.Exception. Any instance variables you include in these exceptions should be serializable.

Here is the definition of the PaymentException application exception:

package com.titan.processpayment;

public class PaymentException extends java.lang.Exception {
    public PaymentException() {
        super();
    }
    public PaymentException(String msg) {
        super(msg);
    }
}
12.1.1.5 ProcessPaymentHomeRemote: The home interface

The home interface of a stateless session bean must declare a single create() method with no arguments. This is a requirement of the EJB specification. It is illegal to define create() methods with arguments, because stateless session beans do not maintain conversational state that needs to be initialized. There are no find methods in session beans, because session beans do not have primary keys and do not represent data in the database.

Although EJB 2.0 defines create<SUFFIX>() methods for stateful session beans and entity beans, stateless session beans may define only a single create() method, with no suffix and no arguments. This is also the case in EJB 1.1. The reason for this restriction has to do with the life cycle of stateless session beans, which is explained later in this chapter.

Here is the definition of the remote home interface for the ProcessPayment EJB:

package com.titan.processpayment;

import java.rmi.RemoteException;
import javax.ejb.CreateException;

public interface ProcessPaymentHomeRemote extends javax.ejb.EJBHome {
    public ProcessPaymentRemote create() throws RemoteException, CreateException;
}

The CreateException is mandatory, as is the RemoteException. The CreateException can be thrown by the bean itself to indicate an application error in creating the bean. A RemoteException is thrown when other system errors occur; for example, when there is a problem with network communication or when an unchecked exception is thrown from the bean class.

The ProcessPaymentHomeRemote interface, as an extension of the javax.ejb.EJBHome, offers the same EJBHome methods as entity beans. The only difference is that remove(Object primaryKey) does not work, because session beans do not have primary keys. If EJBHome.remove(Object primaryKey) is invoked on a session bean (stateless or stateful), a RemoteException is thrown. Logically, this method should never be invoked on the remote home interface of a session bean. Here are the definitions of the javax.ejb.EJBHome interface for EJB 1.1 and 2.0:

public interface javax.ejb.EJBHome extends java.rmi.Remote {
    public abstract HomeHandle getHomeHandle() throws RemoteException;
    public abstract EJBMetaData getEJBMetaData() throws RemoteException;
    public abstract void remove(Handle handle) throws RemoteException, 
        RemoveException;
    public abstract void remove(Object primaryKey) throws RemoteException, 
        RemoveException;
}

The home interface of a session bean can return the EJBMetaData for the bean, just like an entity bean. EJBMetaData is a serializable object that provides information about the bean's interfaces. The only difference between the EJBMetaData for a session bean and an entity bean is that the getPrimaryKeyClass() on the session bean's EJBMetaData throws a java.lang.RuntimeException when invoked:

public interface javax.ejb.EJBMetaData {
    public abstract EJBHome getEJBHome();
    public abstract Class getHomeInterfaceClass();
    public abstract Class getPrimaryKeyClass();
    public abstract Class getRemoteInterfaceClass();
    public abstract boolean isSession();
    public abstract boolean isStateless();  // EJB 1.0 only
}
12.1.1.6 ProcessPaymentBean: The bean class

As stated earlier, the ProcessPayment EJB accesses data that is not generally shared by systems, so it is an excellent candidate for a stateless session bean. This bean really represents a set of independent operations—another indication that it is a good candidate for a stateless session bean.

Here is the definition of the ProcessPaymentBean class, which supports the remote interface functionality:

package com.titan.processpayment;
import com.titan.customer.*;

import java.sql.*;
import java.rmi.RemoteException;
import javax.ejb.SessionContext;

import javax.naming.InitialContext;
import javax.sql.DataSource;
import javax.ejb.EJBException;
import javax.naming.NamingException;

public class ProcessPaymentBean implements javax.ejb.SessionBean {

    final public static String CASH = "CASH";
    final public static String CREDIT = "CREDIT";
    final public static String CHECK = "CHECK";
        
    public SessionContext context;

    public void ejbCreate() {
    }
    
    public boolean byCash(CustomerRemote customer, double amount)
        throws PaymentException{
        return process(getCustomerID(customer), amount, CASH, null, -1, null, null);
    }
    
    public boolean byCheck(CustomerRemote customer, CheckDO check, double amount)
        throws PaymentException{
        int minCheckNumber = getMinCheckNumber();
        if (check.checkNumber > minCheckNumber) {
            return process(getCustomerID(customer), amount, CHECK, 
                check.checkBarCode, check.checkNumber, null, null);
        }
        else {
            throw new PaymentException("Check number is too low. 
                Must be at least "+minCheckNumber);
        }
    }
    public boolean byCredit(CustomerRemote customer, CreditCardDO card, 
        double amount) throws PaymentException {
        if (card.expiration.before(new java.util.Date())) {
            throw new PaymentException("Expiration date has"+ " passed");
        }
        else {
            return process(getCustomerID(customer), amount, CREDIT, null,
            -1, card.number, new java.sql.Date(card.expiration.getTime()));
        }
    }
    private boolean process(Integer customerID, double amount, String type, 
        String checkBarCode, int checkNumber, String creditNumber, 
        java.sql.Date creditExpDate) throws PaymentException {

        Connection con = null;
        
        PreparedStatement ps = null;

        try {
            con = getConnection();
            ps = con.prepareStatement
                ("INSERT INTO payment (customer_id, amount, type,"+ 
                "check_bar_code,check_number,credit_number,"+
                "credit_exp_date) VALUES (?,?,?,?,?,?,?)");
            ps.setInt(1,customerID.intValue());
            ps.setDouble(2,amount);
            ps.setString(3,type);
            ps.setString(4,checkBarCode);
            ps.setInt(5,checkNumber);
            ps.setString(6,creditNumber);
            ps.setDate(7,creditExpDate);
            int retVal = ps.executeUpdate();
            if (retVal!=1) {
                throw new EJBException("Payment insert failed");
            }         
            return true;
        } catch(SQLException sql) {
            throw new EJBException(sql);
        } finally {
            try {
                if (ps != null) ps.close();
                if (con!= null) con.close();
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }
    }
    public void ejbActivate() {}
    public void ejbPassivate() {}
    public void ejbRemove() {}
    public void setSessionContext(SessionContext ctx) {
        context = ctx;
    }
    private Integer getCustomerID(CustomerRemote customer) {
        try {
            return (Integer)customer.getPrimaryKey();
        } catch(RemoteException re) {
            throw new EJBException(re);
        }
    }
    private Connection getConnection() throws SQLException {
        // Implementations shown below
    }
    private int getMinCheckNumber() {
        // Implementations shown below
    }
}

The three payment methods all use the private helper method process(), which does the work of adding the payment to the database. This strategy reduces the possibility of programmer error and makes the bean easier to maintain. The process() method simply inserts the payment information into the PAYMENT table. The use of JDBC in this method should be familiar to you from your work on the bean-managed Ship EJB in Chapter 10. The JDBC connection is obtained from the getConnection() method, as shown in the following code:

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

The byCheck() and byCredit() methods contain some logic to validate the data before processing it. The byCredit() method verifies that the credit card's expiration date does not precede the current date. If it does, a PaymentException is thrown.

The byCheck() method verifies that the serial number of the check is above a certain minimum, which is determined by a property that is defined when the bean is deployed. If the check number is below this value, a PaymentException is thrown. The property is obtained from the getMinCheckNumber() method. We can use the JNDI ENC to read the value of the minCheckNumber property:

private int getMinCheckNumber() {
    try {
        InitialContext jndiCntx = new InitialContext();
        Integer value = (Integer)
            jndiCntx.lookup("java:comp/env/minCheckNumber");
        return value.intValue();
    } catch(NamingException ne) {
        throw new EJBException(ne);
    }
}

Here we are using an environment property set in the deployment descriptor to change the business behavior of the bean. It is a good idea to capture thresholds and other limits in the environment properties of the bean rather than hardcoding them. This gives you greater flexibility. If, for example, Titan decided to raise the minimum check number, you would need to change the bean's deployment descriptor only, not the class definition. (You could also obtain this type of information directly from the database.)

12.1.1.7 JNDI ENC: Accessing environment properties

In EJB, the bean container contract includes the JNDI environment naming context ( JNDI ENC). The JNDI ENC is a JNDI name space that is specific to each bean type. This name space can be referenced from within any bean, not just entity beans, using the name "java:comp/env". The enterprise naming context provides a flexible, yet standard, mechanism for accessing properties, other beans, and resources from the container.

We've already seen the JNDI ENC several times. In Chapter 10, we used it to access a resource factory, the DataSource. The ProcessPaymentBean also uses the JNDI ENC to access a DataSource in the getConnection() method. Furthermore, it uses the JNDI ENC to access an environment property in the getMinCheckNumber() method. This section examines the use of the JNDI ENC to access environment properties.

Named properties can be declared in a bean's deployment descriptor. The bean accesses these properties at runtime by using the JNDI ENC. Properties can be of type String or one of several primitive wrapper types including Integer, Long, Double, Float, Byte, Boolean, and Short. By modifying the deployment descriptor, the bean deployer can change the bean's behavior without changing its code. As we've seen in the ProcessPayment EJB, we could change the minimum check number that we're willing to accept by modifying the minCheckNumber property at deployment. Two ProcessPayment EJBs deployed in different containers could easily have different minimum check numbers. Here's how to declare a named property:

<ejb-jar>
    <enterprise-beans>
        <session>
            <env-entry>
                <env-entry-name>minCheckNumber</env-entry-name>
                <env-entry-type>java.lang.Integer</env-entry-type>
                <env-entry-value>2000</env-entry-value>
            </env-entry>
            ...
        </session>
        ...
    </enterprise-beans>
    ...
</ejb-jar>
12.1.1.8 EJBContext

The EJBContext.getEnvironment() method is optional in EJB 2.0 and 1.1, which means that it may or may not be supported. If it is not functional, the method will throw a RuntimeException. If it is functional, it returns only those values declared in the deployment descriptor as follows (where minCheckNumber is the property name):

<ejb-jar>
    <enterprise-beans>
        <session>
            <env-entry>
                <env-entry-name>ejb10-properties/minCheckNumber</env-entry-name>
                <env-entry-type>java.lang.String</env-entry-type>
                <env-entry-value>20000</env-entry-value>
            </env-entry>
            ...
        </session>
        ...
    </enterprise-beans>
    ...
</ejb-jar>

The ejb10-properties subcontext specifies that the minCheckNumber property is available from both the JNDI ENC "java:comp/env/ejb10-properties/minCheckNumber" (as a String value) and the getEnvironment() method.

Only those properties declared under the ejb10-properties subcontext are available via the EJBContext. Furthermore, such properties are available through the EJBContext only in containers that choose to support the EJB 1.0 getEnvironment() method; all other containers will throw a RuntimeException. It is expected that most EJB 2.0 vendors will have dropped support for this feature, and developers are encouraged to use the JNDI ENC instead of the EJBContext.getEnvironment() method to obtain property values.

12.1.1.9 The ProcessPayment EJB's deployment descriptor

Deploying the ProcessPayment EJB presents no significant problems. It is essentially the same as deploying entity beans, except that the ProcessPayment EJB has no primary key or persistence fields. Here is the XML deployment descriptor for the ProcessPayment EJB:

<!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>
            <description>
                A service that handles monetary payments.
            </description>
            <ejb-name>ProcessPaymentEJB</ejb-name>
            <home>
                com.titan.processpayment.ProcessPaymentHomeRemote
            </home>
            <remote>
                com.titan.processpayment.ProcessPaymentRemote
            </remote>
            <ejb-class>
                com.titan.processpayment.ProcessPaymentBean
            </ejb-class>
            <session-type>Stateless</session-type>
            <transaction-type>Container</transaction-type>
            <env-entry>
                <env-entry-name>minCheckNumber</env-entry-name>
                <env-entry-type>java.lang.Integer</env-entry-type>
                <env-entry-value>2000</env-entry-value>
            </env-entry>
            <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 who is allowed full access 
                to the ProcessPayment EJB.
            </description>
            <role-name>everyone</role-name>
        </security-role>

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

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

The deployment descriptor for EJB 1.1 is exactly the same, except its header specifies the EJB 1.1 specification and deployment descriptor:

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

figs/book.gifPlease refer to Workbook Exercise 12.1, A Stateless Session Bean. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks.

12.1.1.10 EJB 2.0: Local component interfaces

Like entity beans, stateless session beans can define local component interfaces. This allows the local interfaces of a stateless session bean to be used by other co-located enterprise beans, including other stateless and stateful session beans and even entity beans. Obviously, it is more efficient to use local component interfaces between two beans in the same container system than to use the remote interfaces.

The process of defining local interfaces for a stateless or stateful session bean is the same as that for entity beans. The local interfaces extend javax.ejb.EJBLocalObject (for business methods) and javax.ejb.EJBLocalHome (for the home interfaces). These interfaces are then defined in the XML deployment descriptor in the <local> and <local-home> elements.

For the sake of brevity, we will not define local interfaces for either the stateless ProcessPayment EJB or the stateful TravelAgent EJB developed later in this chapter. Your experience creating local interfaces for entity beans in Chapter 5, Chapter 6, and Chapter 7 can be applied easily to any kind of session bean.

12.2 The Life Cycle of a Stateless Session Bean

Just as the entity bean has a well-defined life cycle, so does the stateless session bean. The stateless session bean's life cycle has two states: Does Not Exist and Method-Ready Pool. The Method-Ready Pool is similar to the instance pool used for entity beans. This is one of the significant life-cycle differences between stateless and stateful session beans; stateless beans define instance pooling in their life cycle and stateful beans do not.[1] Figure 12-1 illustrates the states and transitions a stateless session bean instance goes through in its lifetime.

Figure 12-1. Stateless session bean life cycle

figs/ejb3_1201.gif

12.2.1 Does Not Exist

When a bean is in the Does Not Exist state, it is not an instance in the memory of the system. In other words, it has not been instantiated yet.

12.2.2 The Method-Ready Pool

Stateless bean instances enter the Method-Ready Pool as the container needs them. When the EJB server is first started, it may create a number of stateless bean instances and enter them into the Method-Ready Pool. (The actual behavior of the server depends on the implementation.) When the number of stateless instances servicing client requests is insufficient, more can be created and added to the pool.

12.2.2.1 Transitioning to the Method-Ready Pool

When an instance transitions from the Does Not Exist state to the Method-Ready Pool, three operations are performed on it. First, the bean instance is instantiated by invoking the Class.newInstance() method on the stateless bean class.

Second, the SessionBean.setSessionContext(SessionContext context) method is invoked on the bean instance. This is when the instance receives its reference to the EJBContext for its lifetime. The SessionContext reference may be stored in a nontransient instance field of the stateless session bean.

Finally, the no-argument ejbCreate() method is invoked on the bean instance. Remember that a stateless session bean has only one ejbCreate() method, which takes no arguments. The ejbCreate() method is invoked only once in the life cycle of the stateless session bean; when the client invokes the create() method on the EJB home, it is not delegated to the bean instance.

Entity, session, and message-driven beans must never define constructors. Take care of initialization needs within ejbCreate() and other callback methods. The container instantiates instances of the bean class using Class.newInstance(), which requires a no-argument constructor.

Stateless session beans are not subject to activation, so they can maintain open connections to resources for their entire life cycles.[2] The ejbRemove() method should close any open resources before the stateless session bean is evicted from memory at the end of its life cycle. You'll read more about ejbRemove() later in this section.

12.2.2.2 Life in the Method-Ready Pool

Once an instance is in the Method-Ready Pool, it is ready to service client requests. When a client invokes a business method on an EJB object, the method call is delegated to any available instance in the Method-Ready Pool. While the instance is executing the request, it is unavailable for use by other EJB objects. Once the instance has finished, it is immediately available to any EJB object that needs it. This is slightly different from the instance pool for entity beans described in Chapter 11. In the entity instance pool, a bean instance might be swapped in to service an EJB object for several method invocations. Stateless session instances, however, are typically dedicated to an EJB object only for the duration of a single method call.

Although vendors can choose different strategies to support stateless session beans, it is likely that they will use an instance-swapping strategy similar to that used for entity beans (see Chapter 11). However, the swap is very brief, lasting only as long as the business method needs to execute. When an instance is swapped in, its SessionContext changes to reflect the context of its EJB object and the client invoking the method. The bean instance may be included in the transactional scope of the client's request and it may access SessionContext information specific to the client request: for example, the security and transactional methods. Once the instance has finished servicing the client, it is disassociated from the EJB object and returned to the Method-Ready Pool.

Stateless session beans are not subject to activation and never have their ejbActivate() or ejbPassivate() callback methods invoked. The reason is simple: stateless instances have no conversational state to be preserved. (Stateful session beans do depend on activation, as we'll see later.)

Clients that need a remote or local reference to a stateless session bean begin by invoking the create() method on the bean's EJB home:

Object ref = jndiConnection.lookup("ProcessPaymentHomeRemote");
ProcessPaymentHomeRemote home = (ProcessPaymentHomeRemote)
    PortableRemoteObject.narrow(ref,ProcessPaymentHomeRemote.class);

ProcessPaymentRemote pp = home.create();

Unlike the entity bean and stateful session bean, invoking the create() method does not result in a call to the bean's ejbCreate() method. In stateless session beans, calling the EJB home's create() method results in the creation of an EJB object for the client, but that is all.

A stateless session bean's ejbCreate() method is invoked only once in the life cycle of an instance—when it is transitioning from the Does Not Exist state to the Method-Ready Pool. It is not reinvoked every time a client requests a remote reference to the bean. Stateless session beans are limited to a single no-argument create() method because there is no way for the container to anticipate which create() method the client will invoke.

12.2.2.3 Transitioning out of the Method-Ready Pool: The death of a stateless bean instance

Bean instances leave the Method-Ready Pool for the Does Not Exist state when the server no longer needs them; that is, when the server decides to reduce the total size of the Method-Ready Pool by evicting one or more instances from memory. The process begins by invoking the ejbRemove() method on the instance. At this time, the bean instance should perform any cleanup operations, such as closing open resources. The ejbRemove() method is invoked only once in the life cycle of a stateless session bean's instance—when it is about to transition to the Does Not Exist state. When a client invokes one of the remove methods on a stateless session bean's remote or home interface, the method is not delegated to the bean instance. The client's invocation of the method simply invalidates the stub and releases the EJB object: it notifies the container that the client no longer needs the bean. The container itself invokes the ejbRemove() method on the stateless instance, but only at the end of the instance's life cycle. Again, this is different from both stateful session beans and entity beans, which suffer more destructive consequences when the client invokes a remove method. During the ejbRemove() method, the SessionContext and access to the JNDI ENC are still available to the bean instance. Following the execution of the ejbRemove() method, the bean is dereferenced and eventually garbage collected.

12.3 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.[3]

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.[4] 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.

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

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

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

12.3.3.1 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);}
}
12.3.3.2 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() + ".\n" +  
                " Your accommodations include " + 
                  cabin.getName() + 
                " a " + cabin.getBedCount() + 
                " bed cabin on deck level " + cabin.getDeckLevel() + 
                ".\n 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() + ".\n" +  
                " Your accommodations include " + 
                  cabin.getName() + 
                " a " + cabin.getBedCount() + 
                " bed cabin on deck level " + cabin.getDeckLevel() + 
                ".\n Total charge = " + price;
            customerID = (Integer)customer.getPrimaryKey();
            cruiseID = (Integer)cruise.getPrimaryKey();
            cabinID = (Integer)cabin.getPrimaryKey();
            this.price = price;

        }
    public String toString() {
        return description;
    }
}
12.3.3.3 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.

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

12.3.3.5 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.[5]

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.

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

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

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

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

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.

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

12.3.3.9 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;
}
12.3.3.10 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>
12.3.3.11 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.

figs/book.gifPlease 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.

12.4 The Life Cycle of a Stateful Session Bean

The biggest difference between the stateful session bean and the other bean types is that stateful session beans do not use instance pooling. Stateful session beans are dedicated to one client for their entire lives, so there is no swapping or pooling of instances.[6] Instead of being pooled, stateful session bean instances are simply evicted from memory to conserve resources. The EJB object remains connected to the client, but the bean instance is dereferenced and garbage collected during inactive periods. This means that each stateful bean must be passivated before it is evicted to preserve the conversational state of the instance, and it must be activated to restore the state when the EJB object becomes active again.

The bean's perception of its life cycle depends on whether or not it implements a special interface called javax.ejb.SessionSynchronization. This interface defines an additional set of callback methods that notify the bean of its participation in transactions. A bean that implements SessionSynchronization can cache database data across several method calls before making an update. We have not discussed transactions in detail yet, so we will not consider this part of the bean's life cycle until Chapter 14. This section describes the life cycle of stateful session beans that do not implement the SessionSynchronization interface.

The life cycle of a stateful session bean has three states: Does Not Exist, Method-Ready, and Passivated. This sounds a lot like a stateless session bean, but the Method-Ready state is significantly different from the Method-Ready Pool of stateless beans. Figure 12-2 shows the state diagram for stateful session beans.

Figure 12-2. Stateful session bean life cycle

figs/ejb3_1202.gif

12.4.1 Does Not Exist State

When a stateful bean instance is in the Does Not Exist state, it is not an instance in the memory of the system. In other words, it has not been instantiated yet.

12.4.2 Method-Ready State

The Method-Ready state is the state in which the bean instance can service requests from its clients. This section explore the instance's transition into and out of the Method-Ready state.

12.4.2.1 Transitioning to the Method-Ready state

When a client invokes the create() method on an EJB home of a stateful session bean, the bean's life cycle begins. When the create() method is received by the container, the container invokes newInstance() on the bean class, creating a new instance of the bean. Next, the container invokes setSessionContext() on the instance, handing it its reference to the SessionContext, which it must maintain for life. At this point, the bean instance is assigned to its EJB object. Finally, the container invokes the ejbCreate() method on the instance that matches the create() method invoked by the client. Once ejbCreate() has completed, the container returns the EJB object's reference to the client. The instance is now in the Method-Ready state and is ready to service business methods invoked by the client on the bean's remote reference.

12.4.2.2 Life in the Method-Ready state

While in the Method-Ready state, the bean instance is free to receive method invocations from the client, which may involve controlling the workflow of other beans or accessing the database directly. During this time, the bean can maintain conversational state and open resources in its instance variables.

12.4.2.3 Transitioning out of the Method-Ready state

Bean instances leave the Method-Ready state to enter either the Passivated state or the Does Not Exist state. Depending on how the client uses the stateful bean, the EJB container's load, and the passivation algorithm used by the vendor, a bean instance may be passivated (and activated) several times in its life or not at all. If the bean is removed, it enters the Does Not Exist state. A client application can remove a bean by invoking one of the remove() methods on the client API, or the container can choose to remove the bean.

The container can also move the bean instance from the Method-Ready state to the Does Not Exist state if the bean times out. Timeouts are declared at deployment time in a vendor-specific manner. When a timeout occurs in the Method-Ready state, the container may, but is not required to, call the ejbRemove() method. A stateful bean cannot time out while a transaction is in progress.

12.4.3 Passivated State

During the lifetime of a stateful session bean, there may be periods of inactivity when the bean instance is not servicing methods from the client. To conserve resources, the container can passivate the bean instance while it is inactive by preserving its conversational state and evicting the bean instance from memory.

When a stateful bean is passivated, the instance fields are read and then written to the secondary storage associated with the EJB object. When the stateful session bean has been successfully passivated, the instance is evicted from memory; it is destroyed.

When a bean is about to be passivated, its ejbPassivate() method is invoked, alerting the bean instance that it is about to enter the Passivated state. At this time, the bean instance should close any open resources and set all nontransient, nonserializable fields to null. This will prevent problems from occurring when the bean is serialized. Transient fields will simply be ignored.

A bean's conversational state may consist of only primitive values, objects that are serializable, and the following special types:

  • EJB 2.0 and 1.1

    javax.ejb.SessionContext
    javax.ejb.EJBHome (remote home interface types)
    javax.ejb.EJBObject (remote interface types)
    javax.jta.UserTransaction (bean transaction interface)
    javax.naming.Context (only when it references the JNDI ENC)
  • EJB 2.0 only

    javax.ejb.EJBLocalHome (local home interface types)
    javax.ejb.EJBLocalObject (local interface types)
    References to managed resource factories (e.g., javax.sql.DataSource)

The types in this list (and their subtypes) are handled specially by the passivation mechanism. They do not need to be serializable; they will be maintained through passivation and restored automatically to the bean instance when it is activated.

A bean instance's conversational state is written to secondary storage to preserve it when the instance is passivated and destroyed. Containers can use standard Java serialization to preserve the bean instance, or some other mechanism that achieves the same result. Some vendors, for example, simply read the values of the fields and store them in a cache. The container is required to preserve remote references to other beans with the conversational state. When the bean is activated, the container must restore any bean references automatically. The container must also restore any references to the special types listed earlier.

Fields declared transient will not be preserved when the bean is passivated. Except for the special types listed earlier, all fields that are nontransient and nonserializable must be set to null before the instance is passivated or else the container will destroy the bean instance, making it unavailable for continued use by the client. References to special types must automatically be preserved with the serialized bean instance by the container so that they can be reconstructed when the bean is activated.

When the client makes a request on an EJB object whose bean is passivated, the container activates the instance. This involves deserializing the bean instance and reconstructing the SessionContext reference, bean references, and managed resource factories (EJB 2.0 only) held by the instance before it was passivated. When a bean's conversational state has been successfully restored, the ejbActivate() method is invoked. The bean instance should open any resources that cannot be passivated and initialize the values of any transient fields within the ejbActivate() method. Once ejbActivate() is complete, the bean is back in the Method-Ready state and available to service client requests delegated by the EJB object.

In EJB 1.1, open resources such as sockets or JDBC connections must be closed whenever the bean is passivated. In stateful session beans, open resources will not be maintained for the life of the bean instance. When a stateful session bean is passivated, any open resource can cause problems with the activation mechanism.

The activation of a bean instance follows the rules of Java serialization. The exception to this is transient fields. In Java serialization, transient fields are set to their default values when an object is deserialized; primitive numbers become zero, Boolean fields false, and object references null. In EJB, transient fields do not have to be set to their initial values; therefore, they can contain arbitrary values when the bean is activated. The values held by transient fields following activation are unpredictable across vendor implementations, so do not depend on them to be initialized. Instead, use ejbActivate() to reset their values.

The container can also move the bean instance from the Passivated state to the Does Not Exist state if the bean times out. When a timeout occurs in the Passivated state, the ejbRemove() method is not invoked.

12.4.3.1 System exceptions

Whenever a system exception is thrown by a bean method, the container invalidates the EJB object and destroys the bean instance. The bean instance moves directly to the Does Not Exist state and the ejbRemove() method is not invoked.

A system exception is any unchecked exception, including EJBException. Checked exceptions thrown from subsystems are usually wrapped in an EJBException and rethrown as system exceptions. A checked exception thrown by a subsystem does not need to be handled this way if the bean can safely recover from the exception. In most cases, however, the subsystem exception should be rethrown as an EJBException.

In EJB 1.1, the java.rmi.RemoteException is also considered a system exception for backward compatibly with EJB 1.0. However, throwing the RemoteException from a bean class method has been deprecated and is discouraged.

[1]  Some vendors do not pool stateless instances, but may instead create and destroy instances with each method invocation. This is an implementation-specific decision that shouldn't impact the specified life cycle of the stateless bean instance.

[2]  The duration of a stateless bean instance's life is assumed to be very long. However, some EJB servers may actually destroy and create instances with every method invocation, making this strategy less attractive. Consult your vendor's documentation for details on how your EJB server handles stateless instances.

[3]  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.

[4]  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.

[5]  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.

[6]  Some vendors use pooling with stateful session beans, but that is a proprietary implementation and should not impact the specified life cycle of the stateful session bean.

CONTENTS