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.
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 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.
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.
<?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
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.
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.
/* * @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:
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.
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.