Building the Architectural Prototype: Part 3
Part 3 of building the architectural prototype for our EJB solution covers the addition of CMP code to the skeleton code generated out of Rose. It also covers the changes necessary to the JSPs, servlet, and use-case control classes.
Adding Logic to the Generated Code
It's time to make our initial code generation effort bear some fruit. By itself it won't buy us much. We will start out easy and take the simple pathway of asking for all customers, their addresses, and the role each address plays. Let's begin by reviewing some of the key components of the CustomerBean class. Remember that bean classes implement all the business logic of our application, but they are even more instrumental in the part of the application that implements container-managed persistence (CMP). It is CMP that eliminates the need to write SQL code, and CMP is a major productivity boost to anyone building EJB applications today.
The following accessor methods are the cmp fields for the Customer bean:
abstract public Integer getCustomerId(); abstract public void setCustomerId(Integer val); /** * @cmp-field customerNumber * @business-method */ abstract public String getCustomerNumber(); abstract public void setCustomerNumber(String val); /** * @cmp-field firstName * @business-method */ abstract public String getFirstName(); abstract public void setFirstName(String val); /** * @cmp-field middleInitial * @business-method */ abstract public String getMiddleInitial(); abstract public void setMiddleInitial(String val); /** * @cmp-field prefix * @business-method */ abstract public String getPrefix(); abstract public void setPrefix(String val); /** * @cmp-field suffix * @business-method */ abstract public String getSuffix(); abstract public void setSuffix(String val); /** * @cmp-field lastName * @business-method */ abstract public String getLastName(); abstract public void setLastName(String val); /** * @cmp-field phone1 * @business-method */ abstract public String getPhone1(); abstract public void setPhone1(String val); /** * @cmp-field phone2 * @business-method */ abstract public String getPhone2(); abstract public void setPhone2(String val); /** * @cmp-field eMail * @business-method */ abstract public String getEMail(); abstract public void setEMail(String val);
These fields represent the attributes that will be persisted to and retrieved from the database. The modeling tool (e.g., Rational's Rose, TogetherSoft's Together Control Center) generates these accessors for you on the basis of the input provided to the modeling tool. In EJB 2.0, unlike prior versions, the accessors are declared abstract because the persistence engine implemented by the container vendor's product (i.e., BEA's WebLogic, or IBM's WebSphere) is what actually implements the accessor. The selling point that beans can be transported between different container vendor products is now finally moving closer to reality. You may have noticed comments with some tag indicators between the methods; I will discuss those comments at the end of this code section.
The next important fields to look at in the CustomerBean class are the container-managed relationship, or cmr, fields. These are instrumental in implementing the relationships between beans. This information comes from the relationships that are drawn in the modeling tool between the beans:
s /** * Syntax: * cmr source ejb-name multiplicity * ======================================================= * @cmr-field CustomerBean one * @business-method */ abstract public Collection getRoles(); abstract public void setRoles(Collection Role); /** * Syntax: * cmr source ejb-name multiplicity * ======================================================= * @cmr-field CustomerBean one * @business-method */ abstract public Collection getOrders(); abstract public void setOrders(Collection Role);
Remember that there were two one-to-many relationships in which Customer was involved: one with Order and one with Role (via the association relationship between Customer and Address). Again, just as with the attribute accessors, the cmr accessors are declared abstract because it is up to the persistence engine implemented by the container to handle how relationships are physically traversed. Notice that these accessors return Collection objects because a Customer object can have many Role objects and many Order objects.
Let's look at the Role class next. I won't review Address here because it looks very similar to Customer, except it doesn't have a relationship with Order. Role is interesting because it contains relationships to both a single Customer and a single Address. Remember that the Role class defines the involvement between a Customer object and an Address object. Role is easy from an attribute standpoint, having only its primary key and roleName:
/** * @cmp-field roleId * @primkey-field * @business-method */ abstract public Integer getRoleId(); abstract public void setRoleId(Integer val); /** * @cmp-field roleName * @business-method */ abstract public String getRoleName(); abstract public void setRoleName(String val);
The two cmr accessors look like this:
/** * Syntax: * cmr source ejb-name multiplicity * ======================================================= * @cmr-field RoleBean many * @business-method */ abstract public Customer getCustomer(); abstract public void setCustomer(Customer Customer); /** * Syntax: * cmr source ejb-name multiplicity * ======================================================= * @cmr-field RoleBean many * @business-method */ abstract public Address getAddress(); abstract public void setAddress(Address Address);
Notice that unlike the cmr fields for Customer, the cmr fields for Role return actual objects of the Address and Customer classes.
There is quite a bit of power in what we have constructed so far. When we build a client to gain access to the services of our beans, you will see code that somewhat resembles this:
role.getAddress().getPrimaryKey()
Although these look like just methods being invoked, they are handled by the persistence implementation in the container and will generate SQL access code to retrieve the desired objects (unless they are already cached from previous access). In the case illustrated here, the Address object is retrieved and then the primary key of Address is returned.
A Bit of Magic in Those Tags
As I mentioned earlier, you may have noticed some funny-looking Javadoc-like tags in the comments above each accessor. These tags are not put there by the modeling tool but are another alternative to help you with generating code. Most of the visual modeling vendors do a decent job of generating deployment descriptors, but depending on your container vendor, you have other options, particularly if you are using BEA's WebLogic product. Don't worry, though; if you included these tags and you use another product, such as IBM's WebSphere, it will still enhance your productivity immensely.
If you are not using a modeling tool (which is a big mistake in my opinion), then a gentleman named Cedric Beust is potentially a hero to you. Beust developed a marvelous "free" tool called EJBGen, which is a doclet. It uses the little-known ability of the javadoc utility to override the standard document engine with your own parser. EJBGen takes in your bean classes and generates not only the corresponding home, remote, and primary-key classes but also the deployment descriptors. This is where the tags (e.g., @cmp-field, @business-method) come into play.
Actually you can use this tool regardless of your container engine product, but you will have to play a bit with the vendor-specific deployment descriptors if you are using something other than WebLogic. Remember that there is one common descriptor that all vendors must implement: ejb-jar.xml. All the XML descriptors go into the META-INF directory in your implementation. The descriptors are the real workhorse in the EJB implementation and they are standard across all vendors. Other descriptor tags, depending on your particular vendor, will also be required. I won't spend more time on EJBGen, other than to refer you to Beust's Web site, at http://beust.com/cedric/ ejbgen, for more information.
Compiling the EJB Code
Once the code has been written and the deployment descriptors created, a few steps are required. Note that step 3, running the EJB compiler, will vary slightly with each vendor:
All the Java components must be compiled, including the remote, home, and bean classes for each entity bean:
c:\>javac d %classdirectory% Customer.java CustomerHome.java CustomerBean.java Role.java RoleHome.java RoleBean.java Address.java AddressHome.java AddressBean.java
An EJB JAR file must be created that contains the result of the previous compile and the XML deployment descriptors that reside in the META-INF directory:
jar cv0f remulak.jar %metadirectory% %classdirectory%
The EJB compiler must be run against the JAR file created in step 2: c:\>java classpath %WL_HOME%/lib/weblogic_sp.jar; %WL_HOME% /lib/weblogic.jar weblogic.ejbc -compiler javac Remulak.jar ejb20_Remulak.jar
c:\>java classpath %WL_HOME%/lib/weblogic_sp.jar; %WL_HOME% /lib/weblogic.jar weblogic.ejbc -compiler javac Remulak.jar ejb20_Remulak.jar
The ejb20_Remulak.jar file can now be deployed and tested by the client. In WebLogic we do this by placing the JAR file in the applications directory of the deployment target.
Building a Simple Client to Test the Beans
Later in this chapter we will make our client much more appealing and flexible by enhancing the beans and JSPs created in Chapter 11. In this chapter, however, we will build a very rudimentary client that we will invoke from the command line to test the Customer, Role, and Address beans.
For a client to access a bean managed by an EJB container, it must first obtain a reference to the bean via the bean's home interface. The Java Naming and Directory Interface (JNDI) accomplishes this task for the client. Our client, which we will call ClientRemulak, will implement the initial bean-testing logic. To begin the process, we must first obtain a context to the JNDI services as implemented by our container product. This logic will be unique to the individual container product; however, all container products operate in the same fashion (with a few exceptions). The following logic obtains a JNDI context from the BEA WebLogic server:
/** * Get an initial context into the JNDI tree * */ private Context getInitialContext() throws NamingException { try { // Get an initial context Properties h = new Properties(); h.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory"); h.put(Context.PROVIDER_URL, url); return new InitialContext(h); } catch (NamingException ne) { throw ne; } }
After obtaining the context, we can obtain the address of a home interface. In our case we need access to the CustomerHome interface, as shown here:
package com.jacksonreed; import javax.ejb.CreateException; import javax.ejb.EJBHome; import javax.ejb.FinderException; import java.rmi.RemoteException; import java.util.Collection; public interface CustomerHome extends EJBHome { public Customer findByPrimaryKey(Integer primaryKey) throws FinderException, RemoteException; public Collection findAllCustomers() throws FinderException, RemoteException; public Customer findByCustomerNumber(String customerNumber) throws FinderException, RemoteException; public Customer create(Integer CustomerId, String CustomerNumber, String FirstName, String MiddleInitial, String Prefix, String Suffix, String LastName, String Phone1, String Phone2, String EMail) throws CreateException, RemoteException; }
We specifically want to access the findAllCustomers() method in the CustomerHome interface. The logic to get the reference to Customer Home uses the context retrieved earlier. Notice that the first method called in lookupCustomerHome() is getInitialContext():
/** * Look up the customer bean's home interface using JNDI */ private CustomerHome lookupCustomerHome() throws NamingException { Context ctx = getInitialContext(); try { Object home = (CustomerHome) ctx.lookup("remulak. CustomerHome"); return (CustomerHome) PortableRemoteObject.narrow(home, CustomerHome.class); } catch (NamingException ne) { throw ne; } }
As reviewed in Chapter 9, the key ingredient to the Enterprise Java-Bean framework and the flexibility gained at deployment time consists of the deployment descriptors. The deployment descriptor allows, among other things, the ability to express in a query-neutral fashion, the logic executed by finder methods, such as findAllCustomers(). This neutral query logic is called EJB-QL and is an EJB 2.0 standard. EJB-QL statements are parsed and turned into SQL queries by the persistence manager implemented by the EJB container. What follows is a snippet from the ejb-jar.xml file produced by either the visual modeling tool vendor (Rational, TogetherSoft) or as a result of a utility like EJBGen:
<query> <query-method> <method-name>findAllCustomers</method-name> <method-params/> </query-method> <ejb-ql><![CDATA[ WHERE customerId IS NOT NULL]]> </ejb-ql> </query> <query> <query-method> <method-name>findByCustomerNumber</method-name> <method-params> <method-param>java.lang.String</method-param> </method-params> </query-method> <ejb-ql><![CDATA[ WHERE customerNumber = ?1]]> </ejb-ql> </query>
The findAllCustomers() method specified by the <method-name> tag maps directly to the method implemented in the CustomerBean class. The information between the <ejb-ql> tags represents the generic query that the container converts to SQL at runtime. There is also a findByCustomerNumber() method that provides a similar implementation but returns only a Customer matching a given customer number. The following is the CustomerHome interface, which reflects the create and find methods:
package com.jacksonreed; import javax.ejb.CreateException; import javax.ejb.EJBHome; import javax.ejb.FinderException; import java.rmi.RemoteException; import java.util.Collection; public interface CustomerHome extends EJBHome { public Customer findByPrimaryKey(Integer primaryKey) throws FinderException, RemoteException; public Collection findAllCustomers() throws FinderException, RemoteException; public Customer findByCustomerNumber(String customerNumber) throws FinderException, RemoteException; public Customer findByCustomerId(Integer customerId) throws FinderException, RemoteException; public Customer create(com.jacksonreed.CustomerValue custVal) throws CreateException, RemoteException; }
Remember that the client engages with both the home and the remote interfaces, and they, in turn, work with the bean instance.
Let's now turn our attention back to our client, ClientRemulak. The findAllCustomers() method will select rows from the database and, in turn, instantiate objects in our code. The customerHome variable was returned from the lookupCustomerHome() method shown earlier. After retrieving the collection of Customer objects with the findAllCustomers() method, we use an Iterator object to cycle through the collection and retrieve the related Role objects and associated Address objects:
private void findAllCustomers() throws RemoteException, FinderException { Integer addrKey; log("Querying for all customers\n"); Collection custCol = customerHome.findAllCustomers(); Iterator custIter = custCol.iterator(); if(! custIter.hasNext()) { log("No customers were found with a null customerId"); return; } while (custIter.hasNext()) { Customer cust = (Customer) custIter.next(); log("customer id is " + cust.getPrimaryKey() + " customer number is " + cust.getCustomerNumber() + " last name is " + cust.getLastName() ); Collection roleCol = cust.getAllRoles(); Iterator roleIter = roleCol.iterator(); if(! roleIter.hasNext()) { log(" No roles for this customer"); } else { while (roleIter.hasNext()) { Role role = (Role) roleIter.next(); log(" role id is " + role.getPrimaryKey() + " role name is " + role.getRoleName() ); log(" address id is " + role.getAddress(). getPrimaryKey() + " address line 1 is " + role.getAddress(). getAddressLine1() + "\n" ); } } } }
It's time to run ClientRemulak from the DOS command prompt:
C:\> java com.jacksonreed.ClientRemulak Beginning com.jacksonreed.ClientRemulak . . . Querying for all customers customer id is 1234 customer number is abc1234 last name is Reed role id is 3456 role name is Billing address id is 1234 address line 1 is 6660 Delmonico Drive role id is 1234 role name is Shipping address id is 1234 address line 1 is 6660 Delmonico Drive customer id is 2345 customer number is abc2345 last name is Becnel role id is 2345 role name is Mailing address id is 2345 address line 1 is 2323 Happy Boy Lane customer id is 3456 customer number is abc3456 last name is Young No roles for this customer Ending com.jacksonreed.ClientRemulak . . .
The foundation has been laid. It is amazing how little code has to be written when the container-managed persistence support offered by the EJB specification is being used.