Eclipse/JBoss/Oracle Entity Beans

The Eclipse/JBoss (IDE/J2EE server) combination has been receiving a lot of attention recently and with good reason. It permits the rapid development, testing and debugging of server-side applications. They're both open-source packages and cost nothing to download and run. JBoss is built on a foundation of Tomcat, which itself has a fine pedigree. With the appropriate “glue” (the JBoss IDE package), the whole is greater than the sum of its parts.

I still have some reservations about Eclipse (I've written a separate article about some of the difficulties I've encountered) but the increased productivity possible when it works makes up for the occasionally “quirky” behaviour. XDoclet support enables the creation of vendor-specific deployment descriptors for most of the popular J2EE servers. While boasting of more than 5 million downloads, JBoss is a relative newcomer to the J2EE application server space. It's a fine tool for development but I'd probably err on the side of caution and recommend IBM WebSphere as the deployment platform for a mission-critical application.

UPDATE: Technology is constantly changing and improving. The Eclipse platform has matured and has become far more stable as well as ever more powerful. The plug-in architecture supports many powerful extensions; one of my favourites is QuantumDB which eliminates the need to pull up SQL*Plus in order to interface with Oracle. JBoss is a different story. As I've written elsewhere, trying to get multiple applications to inter-operate turned into an exercise in futility. It's also "pseudo-open" source so you have to pay if you want detailed documentation. I've abandoned it for the truly open-source Glassfish J2EE server from Sun Microsystems.

The Project

I'm developing an e-commerce site for a low-volume vendor of specialty CDs. It's your garden variety, “meat and potatoes” type of application involving the usual cast of characters: products arranged in categories, orders, line items, etc. Entity EJBs are actually overkill for such a project but I'm looking to create something which could be used across a wide range of businesses. The J2EE server architecture adroitly handles the scalability, caching and transactional requirements of a high-volume, high-availability on-line store. My application will hardly tax the capabilities of such a system.

I'm going to detail the mechanisms used to handle a single element of the application: the product category. Each category has a unique identifier and they're heirarchically organized, so each also contains the identifier of their parent. Two additional fields (name and description) complete the table definition. I'm going to make my task more difficult, while at the same time more realistic, by using a sequence (sometimes called auto-numbering) for my primary key column (category identifier). It's a scenario you'll likely encounter in the “real world” so we might as well set the bar at an appropriate height.

I'll enumerate the development environment and discuss what you need to do in order to duplicate it. I'll detail the creation of the tables in Oracle and show how we go about building the entity and stateless session beans required to support the application. Interspersed will be comments regarding the build environment and the packaging and XDoclet configurations. If you're travelling the same road then I hope that what I share here will get you to your destination sooner and with fewer bumps. So buckle up!

The Development Environment

My two front line servers could best be described as "a dog's breakfast". They started life as a couple of Compaq Presarios but with various hardware upgrades, including motherboard swaps and moves to new cases, they no longer merit a manufacturer's label. The operating system started out as RedHat Linux 5.2, upgraded to 6.2 then further upgraded as applications demanded. I'm currently running an AMD Athlon XP1800+ on an MSI motherboard with a 2.2.16 Linux kernel. All the machines on my LAN are connected via a 10 Mbps ethernet backbone and share a 2 Mbps Internet feed.

The following table specifies the packages and versions I used for this project but there's no guarantee that they'll still be available by the time you read this. Rather than hard-coding the download links I'll just provide pointers to the websites.

Release Website Notes
Eclipse 2.1.2 SDK www.eclipse.org/downloads/
Make sure that you download the SDK version, not the platform binary.
JBoss 3.2.2 www.jboss.org This is the latest (at time of writing) stable release.
JBoss-IDE 1.2.1 N/A See below.
xdoclet-lib-1.2b3 xdoclet.sourceforge.net The release name is a bit misleading as the version I downloaded actually contains 1.2b4 jars.

These packages arrive as either tgz or zip files so you'll need the appropriate tools to unpack the contents. I've created a /u filesystem for external tools but /opt is also commonly used, as is /usr/local. Unzip Eclipse into a directory of your choosing but don't try to start it just yet. You'll need to copy the xdoclet-apache-module-X.Y.jar file (replace X.Y with the correct values, 1.2b4 in my case) from the xdoclet-lib archive to the $ECLIPSE_HOME/plugins/org.jboss.ide.eclipse.xdoclet.core_X.Y directory (X.Y being 1.2.2 in my case). Eclipse uses the installation directory as the data directory by default, which means that you could lose all your work if you have to reinstall Eclipse. I strongly recommend creating separate data directories and specifying the location at application startup. Since I have more than one project, I create a Bourne shell script for each. Here's a sample:

#!/bin/sh
#
# don't start if an instance is already running
#
pgmName=`basename $0`
count=`ps -ef | grep eclipse | grep -v grep | grep -v $pgmName | wc -l`
if [ $count -gt 0 ]
then
	echo "Cannot run multiple instances of Eclipse"
	exit 0
fi
#
# Add the eclipse directory to the LD_LIBRARY_PATH
#
LD_LIBRARY_PATH=/u/eclipse:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
#
# start eclipse with the appropriate definitions
#
/u/eclipse/eclipse -data /home/sudsy/myproject \
  -vm /u/j2sdk1.4.2_02/jre/bin/java

Now it's time to fire up Eclipse. The JBoss-IDE component interfaces seamlessly with the Eclipse Install/Update manager; click here to download the details. When finished, select Window->Preferences from the menu. Expand JBoss IDE and select XDoclet. Click on the Refresh XDoclet Modules button. Now expand XDoclet and click on the Refresh XDoclet Data. You should now have access to code completion and the tags from all modules.

You'll note that I also specify the Java virtual machine in my startup script. I mostly run with j2sdk1.4.2_02 but have other versions installed as well. Being able to simply change the shell script in order to use a different version is quite convenient.

You're now ready to create your project. Click here to download a truly excellent PDF tutorial. You'll need to work through the examples in order to fully appreciate what follows.

The Database

I run a couple of Oracle 8.1.7 instances on one of my servers. I was in a hurry when performing the installation and configuration many moons ago and so used the defaults for the database creation. I don't recommended this approach. Take the time to properly size your database and pay particular attention to the shared_pool_size in your initINSTANCE.ora file. One of my instances specified a shared_pool_size of 30 MB and a java_pool_size of 20 MB; that's 50 MB of virtual memory. You'll get snappier response times and encounter less paging (unless you're flush with RAM) if you configure reasonable values.

A tool such as Visual CASE is ideal for creating Entity-Relationship Diagrams (ERDs) and can even interface directly with your database to forward- and reverse-engineer table definitions. It's also a bit pricey at US$495 and I couldn't find a way to create sequences. That's not to say that the functionality doesn't exist, merely that I had a limited time frame in which to evaluate the software. I've also installed ArgoUML but have had even less time to become familiar with its capabilities.

While I'm sure that visual design tools are helpful for some, I prefer to work everything out in my head first. Cogitating permits me to consider a multitude of approaches, their strengths, limitations and potential failure scenarios. Once I've arrived at a solution then I either commit it to paper as a drawing or enter it directly at the keyboard. It's when the model becomes large and/or complex that mapping tools can (when used properly) provide the “big picture”. The category model was simple enough so here's the SQL script for dropping and creating the sequence in Oracle:

DROP SEQUENCE category_seq;

CREATE SEQUENCE category_seq NOCACHE;

I've specified NOCACHE since the volume of new categories is typically very low. Apart from the initial population of the database, you might expect to add a new category less than once a week (if that). Here's the script for the category table itself:

ALTER TABLE category DROP CONSTRAINT category_fk1;

DROP INDEX category_idx;

DROP TABLE category;

CREATE TABLE category (
	category_id	integer not null primary key,
	parent_id	integer not null,
	category_name	varchar(127) not null,
	category_description	varchar(2048)
);

INSERT INTO category VALUES ( 0, 0, 'root', null );

ALTER TABLE category ADD CONSTRAINT category_fk1 FOREIGN KEY
  ( parent_id ) REFERENCES category ( category_id );

CREATE INDEX category_idx ON category ( parent_id );

There are a couple of things worth noting here. First I create a constraint such that no category can be added without the parent_id existing in the table. That's just so we don't end up with orphans, although the business logic should prevent such an occurence anyway. We also create an index on the parent_id column since that's a field we're going to use for lookups. In my model I want to return a value object to the client which contains a java.util.TreeMap listing all the children of the selected category. That means I need the ability to find all categories with a parent_id of the category_id of the selected category, hence the index.

The JBoss Datasource

Next we need to tell JBoss about the Oracle database we're going to be using as the datasource. Copy the template file $JBOSS_HOME/docs/examples/jca/oracle-ds.xml to the $JBOSS_HOME/server/default/deploy directory. If you've ever done any JDBC programming with Oracle then the meaning of the tags will be fairly obvious. Here's my configuration:

<?xml version="1.0" encoding="UTF-8"?>

<!-- ===================================================================== -->
<!--  Standard Oracle data source                                          -->
<!--  jndi-name gets mapped to java:/<jndi-name> when accessing in EJBs    -->
<!--  via looking in the initial context                                   -->
<!-- ===================================================================== -->

<datasources>
  <local-tx-datasource>
    <jndi-name>OracleDS</jndi-name>
    <connection-url>jdbc:oracle:thin:@localhost:1521:instance</connection-url>
    <driver-class>oracle.jdbc.driver.OracleDriver</driver-class>
    <user-name>username</user-name>
    <password>password</password>
  </local-tx-datasource>
</datasources>

Just replace instance, username and password with the real values. JBoss also needs to access the Oracle classes12.zip file. It should be copied into the $JBOSS_HOME/server/default/lib directory. We're using the default name for the datasource, which we'll need in the next step.

XDoclet Configuration

The tutorial creates a couple of configurations but we need to add to them in order to create all the files needed to make this application work. This is a tree diagram of my XDoclet configuration:


This diagram was created using a simple program I wrote to display an XML document as a JTree. You can find the source here.

This is a screenshot showing the settings of some of these values. Right-click on the project in the Package Explorer and select Properties in the pop-up menu then click on XDoclet Configurations in the left panel to see this view:

This window should be familiar to you if you followed my advice and took the time to work through the tutorial. You might also notice a few additional elements; they're artifacts from a boilerplate I'm creating which will support all of the elements I commonly use, namely servlets, JSPs, custom tags, Struts Actions and Forms, and EJBs.

The Model

Here's an abbreviated class diagram. I haven't included remote interfaces since they essentially extend the bean methods to the client. I omitted the remote home for the entity bean and the local home for the session bean since they're not used. Also missing are the usual suspects such as ejbActivate, ejbPassivate, setSessionContext, etc. since they don't contain anything of interest, if anything at all. There are also a couple of Exception classes which I use to reflect errors to the client. You'll encounter them in the source files below.


This diagram was created using the aforementioned ArgoUML

Here's how our three primary classes map to the filesystem:

Some of you might be asking why I named the file CategoryEntityBean.java since it's already in a different package than CategoryBean.java, i.e. com.myorg.ejb.entity.CategoryBean is different from com.myorg.ejb.session.CategoryBean. This is true but the XDoclets use the bean name to generate the interface classes as well as the entry in ejb-jar.xml. That file has no concept of packages and requires every bean to have a unique name. If I was hand-assembling the deployment descriptors then I could simply create a unique name for ejb-jar.xml, jboss.xml and jbosscmp-jdbc.xml but with all the “usual” attributes, i.e. home, remote, jndi-name, etc. But by doing so I'd be losing out on the advantages that auto-generation provides. This minor concession is perfectly acceptable to me, especially since the name is invisible to the client. As you'll see in the source code, I can always specify a JNDI name more to my liking.

Source code

Now it's time to look at the annotated source files. Leading off is CategoryEntityBean.java:

/*
 * @author: sudsy
 * @date: Nov 26, 2003
 */
 
package com.myorg.ejb.entity;

import java.io.FileWriter;
import java.io.PrintWriter;
import java.rmi.RemoteException;
import java.sql.DriverManager;

import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.EntityBean;
import javax.ejb.EntityContext;
import javax.ejb.RemoveException;

/**
 * @ejb.bean description="Product Category Entity Bean"
 *           display-name="CategoryEntityBean"
 *           local-jndi-name="com.myorg.ejb.entity.CategoryLocalHome"
 *           jndi-name = "com.myorg.ejb.entity.CategoryHome"
 *           name="CategoryEntity"1
 *           cmp-version = "2.x"
 *           schema = "categoryEJB"2
 *           primkey-field = "categoryId"
 *           type="CMP"
 *           view-type="both"
 * @ejb.pk class = "java.lang.Integer"3
 * @ejb.persistence table-name = "CATEGORY"4
 * @jboss.persistence datasource = "java:/OracleDS"5
 *           datasource-mapping = "Oracle8"
 *           create-table = "true"
 * @jboss.entity-command name = "oracle-sequence"6
 * @jboss.entity-command-attribute name = "sequence"
 *           value = "category_seq"7
 * @ejb.finder description = "Find categories by parent category ID"
 *           signature = "java.util.Collection findByParent( java.lang.Integer parent )"
 *           query = "select distinct object (c) from categoryEJB as c where c.parentId = ?1 and c.categoryId <> 0" 
 * @jboss.query signature = "java.util.Collection findByParent( java.lang.Integer parent )"8
 *           query = "select distinct object (c) from categoryEJB as c where c.parentId = ?1 and c.categoryId <> 0" 
 * @ejb.finder description = "Find categories by category name"
 *           signature = "java.util.Collection findByName( java.lang.String name )"
 *           query = "select distinct object (c) from categoryEJB as c where c.categoryName = ?1" 
 * @jboss.query signature = "java.util.Collection findByName( java.lang.String name )"
 *           query = "select distinct object (c) from categoryEJB as c where c.categoryName = ?1" 
 * @author sudsy
 */
public abstract class CategoryEntityBean implements EntityBean {

    private EntityContext    ctx = null;

    /**
     * @ejb.persistence column-name = "category_id"
     * @ejb.interface-method view-type = "both"
     * @ejb.pk-field9
     * @return the category ID
     */
    public abstract Integer getCategoryId();

    /**
     * @ejb.persistence column-name = "parent_id"
     * @ejb.interface-method view-type = "both"
     * @return the parent category ID
     */
    public abstract Integer getParentId();

    /**
     * @ejb.interface-method view-type = "both"
     * @param I the parent category ID
     */
    public abstract void setParentId( Integer I );

    /**
     * @ejb.persistence column-name = "category_name"
     * @ejb.interface-method view-type = "both"
     * @return the (short) name of the category
     */
    public abstract String getCategoryName();

    /**
     * @ejb.interface-method view-type = "both"
     * @param name the (short) name of the category
     */
    public abstract void setCategoryName( String name );

    /**
     * @ejb.persistence column-name = "category_description"
     * @ejb.interface-method view-type = "both"
     * @return
     */
    public abstract String getCategoryDescription();

    /**
     * @ejb.interface-method view-type = "both"
     * @param description a text description of the category
     */
    public abstract void setCategoryDescription( String description );

    /**
     * @ejb.create-method view-type = "local"10
     * @param parent the parent category ID
     * @param name the (short) category name
     * @return the new primary key
     * @throws EJBException
     * @throws CreateException
     */
    public Integer ejbCreate( Integer parent, String name )
      throws EJBException, CreateException {
        setParentId( parent );
        setCategoryName( name );
        return( null );
    }

    public void ejbPostCreate( Integer parent, String name )
      throws EJBException, CreateException {
    }

    /**
     * @ejb.create-method view-type = "local"
     * @param parent the parent category ID
     * @param name the (short) category name
     * @param description the category description
     * @return the new primary key
     * @throws EJBException
     * @throws CreateException
     */
    public Integer ejbCreate( Integer parent, String name, String description )
      throws EJBException, CreateException {
        setParentId( parent );
        setCategoryName( name );
        setCategoryDescription( description );
        return( null );
    }

    public void ejbPostCreate( Integer parent, String name, String description )
      throws EJBException, CreateException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.EntityBean#ejbActivate()
     */
    public void ejbActivate() throws EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.EntityBean#ejbLoad()
     */
    public void ejbLoad() throws EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.EntityBean#ejbPassivate()
     */
    public void ejbPassivate() throws EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.EntityBean#ejbRemove()
     */
    public void ejbRemove()
        throws RemoveException, EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.EntityBean#ejbStore()
     */
    public void ejbStore() throws EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.EntityBean#setEntityContext(javax.ejb.EntityContext)
     */
    public void setEntityContext( EntityContext context )
        throws EJBException, RemoteException {
        ctx = context;
    }

    /* (non-Javadoc)
     * @see javax.ejb.EntityBean#unsetEntityContext()
     */
    public void unsetEntityContext() throws EJBException, RemoteException {
        ctx = null;
    }
}

Notes:

  1. This is the EJB name which is propogated to all the deployment descriptor files and must be unique within an application.
  2. This is the abstract schema name which is used in the EJB-QL queries.
  3. An Oracle sequence maps to java.lang.Integer.
  4. This table name is propogated to the jbosscmp-jdbc.xml file. If not set then the name of the bean will be used as the table name.
  5. This isn't actually required since it matches the default in the XDoclet jboss task. I've included it for the sake of clarity and portability.
  6. This maps to an entity-command tag in standardjbosscmp-jdbc.xml in the $JBOSS_HOME/server/default/conf directory.
  7. The value here (category_seq) is the name of the sequence we created in the database.
  8. You'd be forgiven for asking why we have to duplicate the information in the ejb:finder and jboss:query tags. The reason is that these tags build different files. The ejb:finder tag information is used to populate the ejb-jar.xml file, which is generic to all servers implementing the J2EE standards. The jboss:query tag populates jbosscmp-jdbc.xml which is, as its' name implies, vendor-specific. Note also that the abstract schema name is used in the query as the source in the ‘from’ clause.
  9. It almost goes without saying that you can't have a mutator method for your primary key. We have a getCategoryId method but no setCategoryId. Note that all the methods which interact with the CMP fields are defined as abstract and that I only specify the column mapping on the accessor methods.
  10. The ejbCreate methods are concrete but merely use the mutator methods to set the CMP-managed fields according to the arguments passed. While the method signature should include a return type which matches the primary key type, the returned value is null since it's up to the container to return the actual reference. Finally, the create methods are included in the local home interface only, as per the view-type attribute value.

Next up is CategoryBean.java:

/*
 * @author: sudsy
 * @date: Nov 28, 2003
 */
package com.myorg.ejb.session;

import java.rmi.RemoteException;
import java.util.Collection;
import java.util.Iterator;
import java.util.TreeMap;

import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.naming.Context;
import javax.naming.InitialContext;

import com.myorg.ejb.CategoryNotFoundException;
import com.myorg.ejb.CategoryVO;
import com.myorg.ejb.ParentNotFoundException;
import com.myorg.ejb.entity.CategoryEntityLocal;
import com.myorg.ejb.entity.CategoryEntityLocalHome;

/**
 * @ejb.bean description="Category Session Bean"
 *           display-name="CategoryBean"
 *           jndi-name = "com.myorg.ejb.session.CategoryHome"
 *           name="Category"
 *           type="Stateless"
 *           view-type="both"
 * @author sudsy
 */
public class CategoryBean implements SessionBean {

    private SessionContext    ctx;
    private CategoryEntityLocalHome    home = null;

    /**
     * @ejb.create-method view-type = "remote"
     * Called when the stateless session bean is first entered,
     * use the opportunity to obtain a home reference.
     */
    public void ejbCreate() {
        Context    ctx = null;
        Object    obj = null;

        /*
	 * obtain a home reference; no need to cast local references
         */

        try {
            ctx = new InitialContext();
            obj = ctx.lookup( "com.myorg.ejb.entity.CategoryLocalHome" );
            home = (CategoryEntityLocalHome) obj;
        }
        catch( Exception e ) {
            e.printStackTrace();
            throw( new EJBException( e.toString() ) );
        }
    };

    /**
     * @ejb.interface-method view-type = "remote"
     * @param parent_id the parent category identifier
     * @param name the new category name
     * @param description the new category description
     * @return the CategoryVO corresponding to the new entity
     * @throws ParentNotFoundException if parent_id is invalid
     * Create a new category.
     */
    public CategoryVO addCategory( int parent_id, String name, String description )
      throws ParentNotFoundException {
        CategoryEntityLocal    bean = null;
        CategoryVO            result = null;
        
        try {
            bean = home.create( new Integer( parent_id ), name, description );
            result = new CategoryVO( bean.getCategoryId().intValue(), parent_id, name,
              description, null );
        }
        catch( CreateException e ) {
            throw( new ParentNotFoundException( e.toString() ) );
        }
        catch( Exception e ) {
            throw( new EJBException( e.toString() ) );
        }
        return( result );
    }

    /**
     * @ejb.interface-method view-type = "remote"
     * @param category the category identifier (primary key)
     * @return the matching CategoryVO
     * @throws EJBException
     * @throws CategoryNotFound if the category_id isn't found in the database
     * Basic method to retrieve a category.
     */
    public CategoryVO getCategory( int category_id )
      throws EJBException, CategoryNotFoundException {
        CategoryEntityLocal    bean = null;
        Collection            c = null;
        CategoryEntityLocal    child = null;
        CategoryVO            result = null;
        Iterator            iter = null;
        int                    parent_id;
        TreeMap                children = null;
                
        try {
            bean = home.findByPrimaryKey( new Integer( category_id ) );        
        }
        catch( FinderException e ) {
            throw( new CategoryNotFoundException( e.toString() ) );
        }
        catch( Exception e ) {
            throw( new EJBException( e.toString() ) );
        }
        parent_id = bean.getParentId().intValue();
        try {
            c = home.findByParent( bean.getCategoryId() );
        }
        catch( Exception e ) {
            try {
                result = new CategoryVO( category_id, parent_id, bean.getCategoryName(),
                  bean.getCategoryDescription(), children );
            }
            catch( Exception f ) {
            }
            return( result ); 
        }
        children = new TreeMap();
        iter = c.iterator();
        while( iter.hasNext() ) {
            child = (CategoryEntityLocal) iter.next();
            children.put( child.getCategoryName(), child.getCategoryId() );
        }
        try {
            result = new CategoryVO( category_id, parent_id, bean.getCategoryName(),
              bean.getCategoryDescription(),  children );
        }
        catch( Exception e ) {
        }
        return( result );
    }

    /**
     * @ejb.interface-method view-type = "remote"
     * @param category the category identifier (primary key)
     * @param name the new category name
     * @return void
     * @throws EJBException
     * This is an example of a mutator for the underlying entity
     * bean accessed through the stateless session bean.
     */
    public void setCategoryName( int category_id, String name )
      throws EJBException, CategoryNotFoundException {
        CategoryEntityLocal    bean = null;
                
        try {
            bean = home.findByPrimaryKey( new Integer( category_id ) );
            bean.setCategoryName( name );        
        }
        catch( FinderException e ) {
            throw( new CategoryNotFoundException( e.toString() ) );
        }
        catch( Exception e ) {
            throw( new EJBException( e.toString() ) );
        }
    }

    /* (non-Javadoc)
     * @see javax.ejb.SessionBean#ejbActivate()
     */
    public void ejbActivate() throws EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.SessionBean#ejbPassivate()
     */
    public void ejbPassivate() throws EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.SessionBean#ejbRemove()
     */
    public void ejbRemove() throws EJBException, RemoteException {
    }

    /* (non-Javadoc)
     * @see javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
     */
    public void setSessionContext( SessionContext context )
        throws EJBException, RemoteException {
        ctx = context;
    }
}

Unlike the entity bean, there's nothing here really worthy of note. I've only included one mutator and there's no delete method but those are easy enough to add.

In the clean-up position is CategoryVO.java:

/*
 * @author: sudsy
 * @date: Nov 28, 2003
 */
package com.myorg.ejb;

import java.io.Serializable;
import java.util.TreeMap;

/**
 * A simple Value-Object wrapper for category data
 * @author sudsy
 */
public class CategoryVO implements Serializable {
    private int        category_id;
    private int        parent_id;
    private String    category_name;
    private String    category_description;
    private TreeMap    children;

    /*
     * constructor
     */

    public CategoryVO( int category_id, int parent_id, String category_name,
      String category_description, TreeMap children ) {
        this.category_id = category_id;
        this.parent_id = parent_id;
        this.category_name = category_name;
        this.category_description = category_description;
        this.children = children;
    }

    /*
     * accessors
     */

    public int getCategoryId() {
        return( category_id );
    }

    public int getParentId() {
        return( parent_id );
    }

    public String getCategoryName() {
        return( category_name );    
    }

    public String getCategoryDescription() {
        return( category_description );    
    }

    public TreeMap getChildren() {
        return( children );
    }
}

It might seem odd at first that I'm using a complex object like TreeMap for the children while the other fields are ints or Strings. Given the nature of the way the information is likely to be presented to customers, I have to store both the child category names as well as their category_ids (to facilitate navigation). I could have gone with two arrays, one of ints, on of Strings, but that's not very OO. A Hashtable would have worked but a TreeMap maintains keys in sorted order. Presenting sub-category names to customers in alphabetical order is preferable to the (lack of) order from either a Hashtable or a TreeMap with customer_id as the key.

The documentation would be incomplete if it didn't include a simple application to demonstrate the interface with the stateless session bean. This code attempts to retrieve category information for a category_id entered as the command-line argument. While the code for CategoryNotFoundException hasn't been shown, just accept that it extends java.lang.Exception and that no methods are overridden.

import    java.util.Hashtable;
import    java.util.TreeMap;
import    java.util.Set;
import    java.util.Iterator;
import    javax.naming.InitialContext;
import    javax.naming.Context;
import    javax.naming.NamingException;
import    com.myorg.ejb.CategoryVO;
import    com.myorg.ejb.session.Category;
import    com.myorg.ejb.session.CategoryHome;

public class SessionTest {

    public static void main( String args[] ) {
        Context      ctx = null;
        Object       obj = null;
        CategoryHome home = null;
        Category     bean = null;
        CategoryVO   category = null;
        Hashtable    env = new Hashtable();
        Integer      categoryId = null;

	/*
	 * ensure correct usage
	 */

        if( args.length != 1 ) {
            System.err.println( "Usage: SessionTest category" );
            System.exit( 12 );
        }

	/*
         * get the category id from the command line
	 */

        try {
            categoryId = new Integer( args[0] );
        }
        catch( NumberFormatException e ) {
            System.err.println( "SessionTest: " + args[0] +
              ": " + e.getMessage() );
            System.exit( 12 );
        }

        /*
         * get the naming context
         */

        try {
            env.put("java.naming.factory.initial",
              "org.jnp.interfaces.NamingContextFactory");
            env.put("java.naming.provider.url",
              "localhost:8099");
            ctx = new InitialContext( env );
        }
        catch( NamingException e ) {
            System.err.println( "SessionTest: " + e.toString() );
            System.exit( 12 );
        }

        /*
         * get the reference to a stateless session bean
         */

        try {
            obj = ctx.lookup(
              "com.myorg.ejb.session.CategoryHome" );
            home = (CategoryHome)
              javax.rmi.PortableRemoteObject.narrow( obj,
              CategoryHome.class );
            bean = home.create();
        }
        catch( Exception e ) {
            e.printStackTrace();
            System.exit( 12 );
        }

	/*
	 * retrieve the category and display the results
	 */

	try {
            category = bean.getCategory( categoryId.intValue() );
            dumpCategory( category );
        }
        catch( Exception e ) {
            e.printStackTrace();
            System.exit( 12 );
        }
    }

    private static void dumpCategory( CategoryVO category ) {
        TreeMap        children = null;
        Set        keys = null;
        Iterator    iter = null;
        String        key = null;
        Integer        value = null;

        if( category == null ) {
            System.out.println( "category is NULL " );
            return;
        }
        System.out.println( "category_id = " +
          category.getCategoryId() );
        System.out.println( "parent_id = " +
          category.getParentId() );
        System.out.println( "category_name = '" +
          category.getCategoryName() + "'" );
        System.out.println( "category_description = '" +
          category.getCategoryDescription() + "'" );
        children = category.getChildren();
        if( children == null ) {
            System.out.println( "children = NULL" );
            return;
        }
        keys = children.keySet();
        if( keys == null ) {
            System.out.println( "keySet = NULL " );
            return;
        }
        iter = keys.iterator();
        if( ! iter.hasNext() ) {
            System.out.println( "category has no children" );
            return;
        }
        System.out.println( "Children:" );
        while( iter.hasNext() ) {
            key = (String) iter.next();
            value = (Integer) children.get( key );
            System.out.println( "\t" + key + " = " +
              value.intValue() );
        }    
    }
}

You'll need to include $JBOSS_HOME/client/jbossall-client.jar in your classpath to run this code, unless you already have JNP installed elsewhere.

The Build Environment

The build environment for this project is dead simple. Use the tutorial example as a foundation and simply remove the web archive (war) from the enterprise archive (ear) target. Remove the web module from the META-INF/application.xml file, choose an appropriate display name and you're ready to deploy. The tutorial contains complete instructions but here's a tree diagram of my packaging configuration:

Summary

The Eclipse/JBoss environment can greatly assist you in creating and deploying entity EJBs. I haven't needed to show the code for the home, local home, remote and local interfaces as they're generated automagically. The trickiest part of the process lies in knowing which tags to use and the attributes and subelements which need to be defined. While this discussion has been specific to the Oracle 8 RDBMS, it should also be generally applicable to other implementations.

On a final note, lack of centralized documentation continues to represent a challenge for users of these open-source projects. I had to visit numerous sites on the 'net in order to collect all the information required to complete this task, some more helpful than others. It's similar to the situation with Struts; you're almost forced to go out and purchase the book in order to get the requisite level of understanding. This appears to be a new trend: providing the software for free and then making up for the lack of license fees with profit from the sale of books and documentation. So "open source" doesn't necessarily mean free.

Copyright © 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 by Phil Selby
All rights reserved.
Updated May, 2009