JaVa
   

Case Study: Adding an Entity Bean to the Pet Store

In this section of the chapter we add an Enterprise JavaBean to our pet store app and step through the process of deployment.

Adding an Enterprise JavaBean to the Pet Store

This section adds a feature to the baseline pet store app: the ability to add, edit, and delete products from the Web. We have decided to use a container-managed entity EJB. Thus, we need to do the following:

  1. Create the product entity bean.

  2. Add a new subproject buildfile for EJBs.
  3. Add a new test case class to test our Product EJB.
  4. Update our categorySystem class.
  5. Create an HTML form.
  6. Add additional JSPs to handle the form submission and the back-end navigation.

Following is a block diagram of how the output will look when we are done.

Java Start Sidebar
Security and Authentication

In the real world, you should password-protect your product entry management. Servlets 2.3 enables you to do this with a servlet filter. Or you can do this with Web authentication provided by your app server or Web server. This example does not demonstrate authentication, but you can add it without changing the JSPs introduced with any of the methods we use.

Java End Sidebar

The entity bean is fairly simple, because it is uses container-managed persistence (CMP). The container takes care of persisting the bean to the database. The next three listings contain the complete product entity bean: its interface, implementation, and home, respectively. With this product entity bean we add, delete, and edit product entries. You can use a number of techniques and design patterns to reduce the number of remote procedure calls; for simplicity, we don't use them here.

package xptoolkit.petstore.entity;
import javax.ejb.*;
import java.rmi.RemoteException;
public interface Product extends EJBObject {
 public String getDescription()throws RemoteException;
 public String getName()throws RemoteException;
 public Integer getId()throws RemoteException;
 public int getSubcategoryId()throws RemoteException;
 public int getQty()throws RemoteException;
 public java.math.BigDecimal getPrice()throws RemoteException;
 public void setDescription(String description)throws RemoteException;
 public void setName(String name)throws RemoteException;
 public void setId(Integer ID)throws RemoteException;
 public void setSubcategoryId(int subcat)throws RemoteException;
 public void setQty(int qty)throws RemoteException;
 public void setPrice(java.math.BigDecimal price)throws RemoteException;
}
package xptoolkit.petstore.entity;
import javax.ejb.*;
public class ProductBean implements EntityBean {
 EntityContext ctx;
 public String description;
 public String name; public Integer id;
 public int subcategoryId; 
 public int qty;
 public java.math.BigDecimal price;
 public Integer ejbCreate (Integer id, String name, int qty, String description, int subCat, java.math.BigDecimal price){
 this.id = id;
 this.name=name;
 this.description=description;
 this.qty=qty=0;
 subcategoryId=subCat; this.price= price;
 return null;
 }
 public void ejbPostCreate(Integer _id, String name, int qty, String description, int subCat, java.math.BigDecimal price){
 id = (Integer)ctx.getPrimaryKey();
 System.out.println("Product ID " + id);
 }
 public String getDescription(){return description;}
 public String getName(){return name;}
 public Integer getId(){return id;}
 public int getSubcategoryId(){return subcategoryId;}
 public int getQty(){return qty;}
 public java.math.BigDecimal getPrice(){return price;}
 public void setDescription(String description)
 {this.description=description;}
 public void setName(String name){this.name=name;}
 public void setId(Integer id){this.id=id;}
 public void setSubcategoryId(int subcategoryId)
 {this.subcategoryId=subcategoryId;}
 public void setQty(int qty){this.qty=qty;}
 public void setPrice(java.math.BigDecimal price){this.price=price;}
 public void setEntityContext(EntityContext ctx) { this.ctx = ctx; }
 public void unsetEntityContext() { ctx = null; }
 public void ejbActivate() { }
 public void ejbPassivate() { }
 public void ejbLoad() { }
 public void ejbStore() { }
 public void ejbRemove() { }
}
package xptoolkit.petstore.entity;
import javax.ejb.*;
import java.util.*;

import java.rmi.RemoteException;
public interface ProductHome extends EJBHome {
 public Product create(Integer id, String name, int qty, String description, int subCat, java.math.BigDecimal price) throws RemoteException, CreateException;
 Product findByPrimaryKey(Integer key) throws RemoteException, FinderException;
 Collection findAll()throws RemoteException, FinderException;
 Collection findBySubcategoryId()throws RemoteException, FinderException;
}
<ejb-jar>
<description>
This ejb-jar files contains the Enterprise beans for the Petstore Case Study
</description>
<enterprise-beans>
 <entity>
 <ejb-name>ProductBean</ejb-name>
 <home>xptoolkit.petstore.entity.ProductHome</home>
 <remote>xptoolkit.petstore.entity.Product</remote>
 <ejb-class>xptoolkit.petstore.entity.ProductBean</ejb-class>
 <persistence-type>Container</persistence-type>
 <prim-key-class>java.lang.Integer</prim-key-class>
 <primkey-field>id</primkey-field>
 <reentrant>False</reentrant>
 <cmp-field><field-name>description</field-name></cmp-field>
 <cmp-field><field-name>name</field-name></cmp-field>
 <cmp-field><field-name>id</field-name></cmp-field>
 <cmp-field><field-name>subcategoryId</field-name></cmp-field>
 <cmp-field><field-name>qty</field-name></cmp-field>
 <cmp-field><field-name>price</field-name></cmp-field>
 </entity>
</enterprise-beans>
<assembly-descriptor>
 <container-transaction>
 <method>
 <ejb-name>ProductBean</ejb-name>
 <method-name>*</method-name>
 </method>
 <trans-attribute>Required</trans-attribute>
 </container-transaction>
</assembly-descriptor>
</ejb-jar>


The new ProductTest (test.xptoolkit.petstore.entity.ProductTest) is used to test the entity bean (xptoolkit.petstore.entity.Product). ProductTestCase extends Cactus's ServletTestCase and thus gets its JNDI properties from the container. In order to create, find, and delete Products, we first need to locate an instance of the home interface as follows:

protected void setUp()throws Exception{
 Object ref;
 InitialContext jndiContext= new InitialContext();
 ref = jndiContext.lookup("ProductBean");
 home = (ProductHome) PortableRemoteObject.narrow (ref, ProductHome.class);
}


This is fairly standard client-side EJB code. The first test uses the home interface to create a product entity with test data:

 public void testCreate()throws Exception{
 product = home.create(new Integer(876662), "Rick", 5, "you ", 555, new java.math.BigDecimal(1200));
 assertNotNull("product", product);
 assertEquals("name", "Rick", product.getName());
 } 


As you can see, the first and second tests depend on each other; they are order dependent. The scenario works because the test framework uses reflection, and reflection uses the methods in the order they are declared. This code is brittle and depends on some minutia in the Java reflection API. You can ensure the order of execution by explicitly setting it in the suite() method instead of relying on the generic reflection-based methods. The TestSuite has an addTest() method that lets you add test cases to it. You could also OrderedTestSuite available in junit-addons (http://sf.net/projects/junit-addons). The second test finds the entity created with the first test and then deletes that entity by calling the product entities' remove() method. Then, to make sure the entity was removed, the test tries to find it again. If the home interface's findByPrimaryKey() method does not find the object, we return; otherwise, we force a fail by calling fail(), as follows:

 public void testRemove()throws Exception{
 product = home.findByPrimaryKey(new Integer(876662));
 product.remove();
 try{
 product = home.findByPrimaryKey(new Integer(876662));
 }
 catch(javax.ejb.ObjectNotFoundException e){
 return;
 }
 
 fail("Product entity should already be gone and not findable.");
 }


The other methods test the setter and getter methods of the product, as follows:

 /** Test of setter methods, of class xptoolkit.petstore.model.Product. */
 public void testSetters() throws Exception{
 testCreate();
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 testRemove();
 }
 /** Test of getter methods, of class xptoolkit.petstore.model.Product. */
 public void testGetters() throws Exception{
 testCreate(); this.assertEquals("name", product.getName(), "Rick");
 this.assertEquals("description", product.getDescription(), "you ");
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 this.assertEquals("name", product.getName(), "Boo");
 this.assertEquals("description", product.getDescription(), "Designer");
 testRemove();
 }


The tests in ProductTest are quite simple. They ensure that we have set up and created our entity bean correctly. Please see the following listing for the complete ProductTest code.

package test.xptoolkit.petstore.entity;
import org.apache.cactus.*;
import java.util.Properties;
import javax.rmi.PortableRemoteObject; import javax.naming.*;
import xptoolkit.petstore.entity.Product;
import xptoolkit.petstore.entity.ProductHome;
public class ProductTest extends ServletTestCase {
 Product product;
 ProductHome home;
 protected void setUp()throws Exception{

 Object ref;
 InitialContext jndiContext = new InitialContext();
 ref = jndiContext.lookup("ProductBean");
 home = (ProductHome) PortableRemoteObject.narrow (ref, ProductHome.class);
 }
 public static void main(java.lang.String[] args) {
 junit.textui.TestRunner.run(suite());
 }
 public static Test suite() {
 TestSuite suite = new TestSuite(ProductTest.class);
 return suite;
 }
 public void testCreate()throws Exception{
 product = home.create(new Integer(876662), "Rick", 5, "you ", 555,
 new java.math.BigDecimal(1200));
 assertNotNull("product", product);
 assertEquals("name", "Rick", product.getName());
 }
 public void testRemove()throws Exception{
 product = home.findByPrimaryKey(new Integer(876662));
 product.remove();
 try{
 product = home.findByPrimaryKey(new Integer(876662));
 }
 catch(javax.ejb.ObjectNotFoundException e){
 return;
 }
 fail("Product entity should already be gone and not findable.");
 }
 /** Test of getSetter methods, of class xptoolkit.petstore.model.Product. */
 public void testSetters() throws Exception{
 testCreate();
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 testRemove();
 }
 /** Test of getter methods, of class xptoolkit.petstore.model.Product. */
 public void testGetters() throws Exception{
 testCreate(); this.assertEquals("name", product.getName(), "Rick");
 this.assertEquals("description", product.getDescription(), "you ");
 product.setName("Boo");
 product.setDescription("Designer");
 product.setQty(5);
 this.assertEquals("name", product.getName(), "Boo");
 this.assertEquals("description", product.getDescription(), "Designer");
 testRemove();
 }
}


Java Start Sidebar
IDEs support JUnit and Ant

Plug-ins are available for Eclipse, Forte, NetBeans, TogetherSoft ControlCenter, JBuilder, and so on for both JUnit and Ant. We create many of our tests by generating the tests' started code in the NetBeans IDE. The ProductTest started code was initially generated with NetBeans support for JUnit. You specify the class, and NetBeans generates the started skeleton to test your class. Cool beans!

Java End Sidebar

It's our considered opinion that no JSP should know whether you are using JDBC, flat files, or entity beans to manage the persistence of the system. Thus, we decided to add support for adding and removing products behind the CategorySystem façade class. In fact, the implementation of the client-side piece of the Product entity is in CategoryDB, and the public interface is defined in the Category abstract class. Here are the additional methods that we added to the CategorySystem class (xptoolkit.petstore.model .CategorySystem):

 public void createProduct(Product product) throws Exception{
 currentCategory.createProduct(product);
 if(currentSubcategory!=null)currentSubcategory.invalidate();
 }
 public void editProduct(Product product) throws Exception{
 currentCategory.editProduct(product);
 if(currentSubcategory!=null)currentSubcategory.invalidate();
 }
 public void deleteProduct(int id) throws Exception{
 currentCategory.deleteProduct(id);
 if(currentSubcategory!=null)currentSubcategory.invalidate();
 }


Here are the corresponding methods we added to the Category class:

 public abstract void createProduct(Product product) throws Exception;
 public abstract void editProduct(Product product) throws Exception;
 public abstract void deleteProduct(int id) throws Exception; 


The actual implementation of these methods is in the CategoryDB class, as follows:

 public void createProduct(Product product) throws Exception{
 getHome().create(new Integer(product.getId()), product.getName(), product.getQty(), product.getDescription(), product.getFkSubcategoryId(),
 new java.math.BigDecimal(product.getPrice()));
 }
 public void editProduct(Product product) throws Exception{
 xptoolkit.petstore.entity.Product p
 =getHome().findByPrimaryKey(
new Integer(product.getId()));
 p.setName(product.getName());
 p.setDescription(product.getDescription());
 p.setPrice(new java.math.BigDecimal(product.getPrice()));
 p.setQty(product.getQty());
 }
 public void deleteProduct(int id) throws Exception{
 getHome().findByPrimaryKey(new Integer(id)).remove();
 }
 private ProductHome getHome() throws Exception{
 Object ref;
 InitialContext jndiContext = new InitialContext();
 ref = jndiContext.lookup("ProductBean");
 return (ProductHome) PortableRemoteObject.narrow (ref, ProductHome.class);
 } 


This code should look familiar. It is much like the code in our test, except that now we are using it to implement the public interface to our Web app. You should add tests at the boundary points to every tier in an n-tier architecture. That way, you test the public interface of each tier. This approach becomes particularly useful if things begin to go wrong; when debugging a distributed multi-tiered app, it's helpful to be able to test access to each tier independently from the rest of the added business logic in the encapsulating tier. Because the CategorySystem is a very thin wrapper in the case of adding, removing, and editing products, we decided to add the tests in the CategoryTest as follows:

 public void testCreateDeleteProduct() throws Exception {
 . . .
 Product p = new Product(){};
 p.setId(1119996);
 p.setFkSubcategoryId(111);
 p.setName("Test1");
 p.setDescription("Test1 Description");
 p.setPrice(11);
 p.setPrice(6); category.createProduct(p); 
 Product p2 = category.getProduct(1119996);
 assertEquals("name after create",p2.getName(), p.getName());
 p.setName("Test2");
 category.editProduct(p);
 Product p3 = category.getProduct(1119996);
 assertEquals("name after edit", p3.getName(), p.getName());
 category.deleteProduct(p.getId());
 Product p4 = category.getProduct(1119996);
 this.assertEquals("product should be gone", -1, p4.getId());
 }


This code is fairly simple, because the actual product implementation is tested thoroughly in the entity ProductTest. Essentially, the test creates a product, edits it, and deletes it. It makes sure the product data is added, edited, and removed. Here the test creates the product by calling the category createProduct() method:

 Product p = new Product(){};
 p.setId(1119996);
 p.setFkSubcategoryId(111);
 p.setName("Test1");
 p.setDescription("Test1 Description");
 p.setPrice(11);
 p.setPrice(6); category.createProduct(p);


Next, the test verifies that the product was actually created by looking it up:

 Product p2 = category.getProduct(1119996);
 assertEquals("name after create",p2.getName(), p.getName());


Here the test edits the product by changing the product object and then submitting it. Then, the test makes sure the product was edited:

 p.setName("Test2");
 category.editProduct(p);
 Product p3 = category.getProduct(1119996);
 assertEquals("name after edit", p3.getName(), p.getName());


Finally, the test removes the product, as follows:

 category.deleteProduct(p.getId());
 Product p4 = category.getProduct(1119996);
 this.assertEquals("proudct should be gone", -1, p4.getId());


One thing is wrong with this test. It should be further functionally decomposed. For example, let's say the create part or the delete part fails. The output of the test will not make clear which functionality was not working. There is a fine line between being overly cautious and sloppy. The more you functionally decompose, the better your reports will be able to point you to the correct failure point. So, let's decompose the test a little further. We can see that we are doing four things: creating a product, getting a product from the data store, editing a product, and deleting a product from the data store. We begin by moving the test data that the tests will share to the setUp() method, as follows:

 Product p;
 public void setUp()throws Exception{
 . . .
 . . . p = new Product(){};
 p.setId(1119996);
 p.setFkSubcategoryId(111);
 p.setName("Test1");
 p.setDescription("Test1 Description");
 p.setPrice(11);
 p.setPrice(6); }


Notice that the test object p is now an instance variable, so it can be used in all four tests. The next step is to break the method testCreateDeleteProduct() into four methods. We add the product from the setUp() method:

 public void testCreateProduct() throws Exception { category.createProduct(p);
 }


The next test tests the ability to get the product out of the database. Notice that you could combine this get-product test and the previous test, because this one validates the create-product test:

 public void testGetProduct()throws Exception {
 Product p2 = category.getProduct(1119996);
 assertEquals("name after create",p2.getName(), p.getName());
 }


The next test tests the ability to edit an existing product:

 public void testEditProduct() throws Exception {
 p.setName("Test2");
 category.editProduct(p);
 Product p3 = category.getProduct(1119996);
 assertEquals("name after edit", p3.getName(), p.getName());
 }


Finally, we test the deletion of the product as follows:

 public void testDeleteProduct()throws Exception {
 category.deleteProduct(p.getId());
 Product p4 = category.getProduct(1119996);
 this.assertEquals("proudct should be gone", -1, p4.getId());
 }


You really need to try and keep tests as small and atomic in operation as possible. If something breaks in the future, the test will identify exactly what broke. If we had left the test as it was and something broke, it would be hard to tell what broke—not impossible, but difficult. Plus, these methods describe exactly what we are testing.


JaVa
Comments