Server-side Java: Create forward-compatible beans in EJB, Part 2

More key strategies for developing portable EJB 1.0 beans for EJB 1.1 servers

While the Enterprise JavaBeans specification (EJB) 1.1 provides a more stable and concrete specification than its predecessor, EJB 1.0, does, it also introduces forward-compatibility problems. Beans developed for EJB 1.0-compliant servers today will not automatically port to EJB 1.1 servers tomorrow.

In the first installment of this article, we developed the PortableContext, an abstraction that insulates bean developers from changes that affect forward compatibility. At runtime, PortableContext automatically changes its behavior to support either EJB 1.0 or EJB 1.1 conventions for accessing properties, JDBC connections, and other beans. This article will continue to enhance the PortableContext in order to encapsulate access to security from a bean. I will also demonstrate how you can use the PortableContext to make enterprise beans portable between brands of servers, as well as different versions of the specification. In addition, I’ll address issues specific to entity bean portability and the changes to XML deployment descriptors.

This article covers some advanced topics, and is not intended for individuals new to Enterprise JavaBeans. Readers should already be familiar with Java development, JNDI, JDBC, and Enterprise JavaBeans. In addition, I have simplified exception handling so that example code is clear and easy to follow.

PortableContext

Last month, we developed the PortableContext so that it provided a common interface that hid implementation differences between EJB 1.0 and EJB 1.1 when accessing environment properties, JDBC connections, and other beans. Now we will extend the PortableContext to encapsulate differences between EJB 1.0 and 1.1 when utilizing the EJBContext security methods.

Bean access to security

Enterprise JavaBeans provides beans with limited access to authorization-based (access control) security. In EJB, a bean can obtain the security identity of its client and make sure that the client is a member of a specific security role or identity. EJB 1.0 and EJB 1.1 maintain slightly different semantics in the EJBContext to support these security features.

In EJB 1.0, the EJBContext is specifically designed to use the java.security.Identity type for identifying clients and verifying membership in a role; in EJB 1.1, the EJBContext uses the java.security.Principal type for this purpose. In EJB 1.1, the Identity methods used in EJB 1.0 are still available but are deprecated, which presents a forward-compatibility problem. Lists 1-A and 1-B show the security methods in the EJBContext for EJB 1.0 and EJB 1.1.

List 1-A. EJB 1.0: Security methods in the EJBContext
public interface EJBContext {
  <b>public java.security.Identity getCallerIdentity();
  public boolean isCallerInRole(Identity role);</b>
  ...
}
List 1-B. EJB 1.1: Security methods in the EJBContext
public interface EJBContext {
  <b>public java.security.Principal getCallerPrincipal();
  public boolean isCallerInRole(java.lang.String roleName);</b>
  // Deprecated
  public java.security.Identity getCallerIdentity();
  // Deprecated
  public boolean isCallerInRole(Identity role);
  ...
}

The change in the new spec is a result of change in the security architecture of the Java 2 Platform. However, the differences are cosmetic for most EJB developers, since the underlying objective is the same. The code fragments below demonstrate how a bean might use the EJBContext to determine a caller’s identity and verify membership in a role under both the EJB 1.0 and EJB 1.1 specs.

List 2-A. EJB 1.0: Using the EJBContext security methods
public class AccountBean implements EntityBean {
  int id;
  double balance;
  String modifiedBy;
  EntityContext ejbContext;
  PortableContext portableContext;
  public void withdraw(Double withdraw)
      throws WithdrawLimitException, AccessDeniedException {
    // only tellers can withdraw more than 10k
    if(withdraw.doubleValue() > 10000) {
      Identity tellerIdnty = new RoleIdentity("teller");
      <b>boolean isTeller = ejbContext.isCallerInRole(tellerIdnty)</b>
      if(!isTeller)
        throw new AccessDeniedException();
    }
    Double limit = (Double)
      portableContext.getEnvironmentEntry(
        "java:comp/env/withdraw_limit", Double.class);
    if (withdraw.doubleValue() > limit.doubleValue())
      throw new WithdrawLimitException(limit);
    else
      balance = balance - withdraw.doubleValue();
    <b>Identity identity = ejbContext.getCallerIdentity( );</b>
    String modifiedBy = identity.getName();
  }
  ...
}
List 2-B. EJB 1.1: Using the EJBContext security methods
public class AccountBean implements EntityBean {
  int id;
  double balance;
  String modifiedBy;
  EntityContext ejbContext;
  PortableContext portableContext;
  public void withdraw(Double withdraw)
      throws WithdrawLimitException, AccessDeniedException {
    // only tellers can withdraw more than 10k
    if(withdraw.doubleValue() > 10000) {
      <b>boolean isTeller = ejbContext.isCallerInRole("teller")</b>
      if (!isTeller)
        throw new AccessDeniedException( );
    }
    Double limit = (Double)
      portableContext.getEnvironmentEntry(
        "java:comp/env/withdraw_limit",Double.class);
    if (withdraw.doubleValue() > limit.doubleValue())
      throw new WithdrawLimitException(limit);
    else
      balance = balance - withdraw.doubleValue();
    <b>Principal principal = ejbContext.getCallerPrincipal( );</b>
    String modifiedBy = principal.getName();
  }
  ...
}

PortableContext can hide the EJB 1.0 and EJB 1.1 security models from the bean. To accomplish this, the PortableContext models its abstraction around the EJB 1.1 security model. Below, the abstract PortableContext class has been modified to include two new methods, getCallerPrincipal() and isCallerInRole(), which mimic the new security methods in the EJB 1.1 EJBContext.

List 3. PortableContext class with new abstract security methods

import javax.ejb.*; import java.lang.reflect.Method; import java.security.Principal;

public abstract class PortableContext { final static String SYSTEM_PROPERTY_NAME = "java.ejb.portable_context"; EJBContext ejbContext; public static PortableContext getInstance(EJBContext context) throws PortableContextException{ String className = System.getProperty(SYSTEM_PROPERTY_NAME); if(className == null) throw new PortableContextException("System property for implementation not set"); try{ Class clazz = Class.forName(className); PortableContext portableCtx = (PortableContext)clazz.newInstance(); portableCtx.setEJBContext(context); return portableCtx; }catch(Exception e){ throw new PortableContextException(e); } } public void setEJBContext(EJBContext ctx){ ejbContext = ctx; } public abstract Object lookup(String name, Class type)throws PortableContextException;

public abstract Principal getCallerPrincipal( );

public abstract boolean isCallerInRole(String roleName);

}

In the PortableContext class, the security methods are abstract, which means that the PortableContext implementations (PortableContext1_0 and PortableContext1_1) must implement these methods.

In Lists 4-A and 4-B below, the PortableContext implementations are modified to implement the security methods. Notice that the EJB 1.1 implementation (PortableContext1_1) simply delegates the method requests to the EJB 1.1 EJBContext, while the EJB 1.0 implementation converts requests from the Principal-based model of EJB 1.1 to the Identity model of EJB 1.0.

List 4-A. PortableContext1_0, with security methods
import javax.ejb.EJBContext;
  ...
import java.security.Principal;
import java.security.Identity;
public class <b>PortableContext1_0</b> extends PortableContext {
    public Principal <b>getCallerPrincipal( ) {</b>         return (Principal)ejbContext.getCallerIdentity( );
    }
    public boolean <b>isCallerInRole(String roleName)</b>{
        Identity identity = new RoleIdentity(roleName);
        return ejbContext.isCallerInRole(identity);
    }
...
}
List 4-B. PortableContext1_1, with security methods

import javax.naming.InitialContext; ... import java.security.Principal;

public class <b>PortableContext1_1</b> extends PortableContext {

public Principal <b>getCallerPrincipal( )</b>{ return ejbContext.getCallerPrincipal(); }

public boolean <b>isCallerInRole(String roleName)</b>{ return ejbContext.isCallerInRole(roleName);

} ... }

The java.security.Identity class implements the java.security.Principal interface, so the getCallerPrincipal( ) method in the PortableContext1_0 class simply casts the Identity object returned from the EJBContext.getCallerIdentity( ) method to its Principal type. This is simple enough; the implementation of the isCallerInRole() method in the PortableContext1_0 class is more complicated, however.

EJB 1.0 required the use of java.security.Identity to verify membership in a security role. As the code in Lists 2-A and 2-B demonstrates, checking a client’s role can provide valuable authorization logic that the access control declarations in the deployment descriptor can’t address. Unfortunately, while the EJB 1.0 specification requires the use of the Identity type as a role identifier, it doesn’t specify how a bean should acquire the Identity object specific to the role being tested. The Identity class is an abstract class, so simply instantiating it is not possible. In the examples above, a mysterious RoleIdentity object was instantiated with the name of the role being tested. This provided us with an Identity object that could be used in the isCallerInRole(Identity role) method. But where did the RoleIdentity object come from?

The RoleIdentity class is an extension of the java.security.Identity class, and provides us with a simple, concrete implementation of Identity that we can instantiate with a string name. (A similar RoleIdentity class was originally defined by Jian Lin in a post to the ejb-interest mailing list on September 24, 1999.) Below is the definition of this class.

List 5. RoleIdentity
import java.security.Identity;
public class RoleIdentity extends Identity {
  public RoleIdentity(String name) {
    super(name);
  }
}

Use of the RoleIdentity class works in those EJB servers that limit comparison operations of Identity to the name attribute. In other words, these servers simply compare the string values returned by the getName() methods of the Identity objects.

Some EJB vendors may enlist more complicated mechanisms for comparing the Identity objects. In these cases, you may have to enhance the RoleIdentity defined here, or use a vendor-specific mechanism for verifying membership in a role. BEA’s Weblogic Server, for example, works wonderfully with the RoleIdentity, but it also provides a proprietary mechanism for obtaining group Identity objects (i.e., roles to which identities belong). List 6 shows how the PortableContext1_0.isCallerInRole() method would be coded to use the Weblogic security API instead of RoleIdentity.

List 6. PortableContext1_0.isCallerInRole( ) method using the Weblogic Server’s security API
public class PortableContext1_0 extends PortableContext {
  public boolean <b>isCallerInRole(String roleName)</b>{
    // Weblogic specific solution
    <b>Identity identity = (Identity) weblogic.security.acl.Security.getRealm().getGroup(roleName);</b>
    if(identity==null) {
      return false;
    }
    <b>return ejbContext.isCallerInRole(identity);</b>
  }
  ...
}

The PortableContext insulates the bean developer from the differences in the security procedures of EJB 1.0 and EJB 1.1. In addition, because we are using the EJB 1.1 conventions, EJB 1.0 beans will support security in a forward-featured manner. Below is an example of how a forward-compatible bean would use the security methods defined in the PortableContext.

List 7. Forward-compatible bean using the PortableContext security methods
public class AccountBean implements EntityBean {
  int id;
  double balance;
  String modifiedBy;
  EntityContext ejbContext;
  PortableContext portableContext;
  public void withdraw(Double withdraw)
      throws WithdrawLimitException, AccessDeniedException {
    // only tellers can withdraw more than 10k
    if(withdraw.doubleValue() > 10000) {
      <b>boolean isTeller = portableContext.isCallerInRole("teller")</b>
      if(!isTeller)
        throw new AccessDeniedException( );
    }
    Double limit = (Double)
      portableContext.getEnvironmentEntry(
        "java:comp/env/withdraw_limit",Double.class);
    if (withdraw.doubleValue() > limit.doubleValue())
      throw new WithdrawLimitException(limit);
    else
      balance = balance - withdraw.doubleValue();
    <b>Principal principal = portableContext.getCallerPrincipal( );</b>
    String modifiedBy = principal.getName();
  }
  ...
}

Using the PortableContext across brands of EJB servers

The PortableContext isn’t just able to encapsulate differences between versions of the EJB specification; it can also do so across various brands of EJB servers. Although all EJB vendors strive for full compliance with the EJB specification, few accomplish this Herculean task overnight. As a result, servers support the specification in varying degrees, which can make portability between servers difficult. This problem was exacerbated by the recent introduction of the EJB 1.1 specification. Porting beans between brands of servers in this climate will be difficult, but many of the brand-to-brand portability problems can be solved easily using the PortableContext. Concrete implementations of the PortableContext can be created for any brand of EJB server and loaded dynamically at runtime by simply changing the system property java.ejb.portable_context so that it points at the correct implementation class.

For example, the Gemstone/J server supports most of the EJB 1.1 features, with one notable exception: it does not use of the JNDI ENC to obtain a JDBC database connection. When a database connection is needed, it is instead obtained using the DriverManager.getConnection() method. To support beans deployed in a Gemstone/J server, you simply define a Gemstone/J concrete implementation. List 8 is an example of just such an vendor-specific implementation.

List 8. The Gemstone/J PortableContext
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ejb.EJBContext;
import javax.ejb.EJBException;
import java.sql.SQLException;
import javax.rmi.PortableRemoteObject;
import java.sql.DriverManager;
import java.security.Principal;
public class GSPortableContext3_0 extends PortableContext {
    InitialContext jndiContext;
    public Principal getCallerPrincipal( ){
        return ejbContext.getCallerPrincipal();
    }
    public boolean isCallerInRole(String roleName){
        return ejbContext.isCallerInRole(roleName);
    }
    
    public Object lookup(String name, Class type)throws PortableContextException{
           try{
                jndiContext = new InitialContext();
                Object value = jndiContext.lookup(name);
                if(name.startsWith("java:comp/env/jdbc")){               return DriverManager.getConnection((String)value); 
                }
                else if(name.startsWith("java:comp/env/ejb")){
                    return PortableRemoteObject.narrow(value,type);
                }
                else              
                    return value;
           }catch(NamingException ne){
                throw new PortableContextException(ne);
           }catch(SQLException se){
                throw new PortableContextException(se);
           }
           
    }
}

If you were to run a forward-compatible bean (one that uses PortableContext) in BEA’s Weblogic Server 4.5, for example, you would use the PortableContext1_0 concrete implementation. The same bean could later be deployed in Gemstone/J 3.0 server by simply changing the system property java.ejb.portable_context to point at the GSPortableContext3_0 concrete implementation.

Other portability changes in EJB 1.1

Changes specific to entity beans

The most obvious change in EJB 1.1 is its mandated support for entity beans. This support must be complete, embracing support for both container-managed and bean-managed persistent entities. As big and burly as this change is, it’s not going to significantly affect the portability of beans developed in 1.0 servers. If entity beans weren’t available before, then the new version simply makes them available to you; if you’re already using entity beans, then in all likelihood you will need to make some cumbersome but simple changes to your code.

The entity bean did not make it into EJB 1.1 unchanged. In addition to changes in the EJBContext and access to beans and resources (addressed above), entity beans also lost bean-managed transactions and changed the return value of ejbCreate(..) methods.

Bean-managed transactions

In EJB 1.0 and 1.1, you can manage transactions both by declaration and by explicit use of a transactional bean-container interface. This declarative transaction control, called container-managed transactions (CMT), is the simplest of the two options because it doesn’t require that a bean developer explicitly control the transactional bounds — it’s automatic. In bean-managed transactions (BMT), the bean uses the EJBContext and the UserTransaction object to explicitly control the transaction. In EJB 1.0, both stateful and entity beans had the option of using BMT instead of CMT; this was deemed necessary in order to accommodate fairly complicated and unusual scenarios in which declarative transaction control was too restrictive. In practice, the use of BMT in stateful session beans is more common than BMT in entity beans. This is good, because EJB 1.1 prohibits the use of BMT in entity beans. If you are developing beans to the EJB 1.0 specification, you must not use BMT if you want your bean to port to EJB 1.1 servers.

ejbCreate(..): Return type changed

In EJB 1.0, the ejbCreate() methods in bean-managed persistence (BMP) and container-managed persistence (CMP) entities are required to have different return values. In CMP entities, the bean must return a void type, while in BMP entities, the bean must return the primary key type. The logic for this discrepancy was that CMP entities are not responsible for manufacturing their own primary key — that’s the container’s job — so the return value should reflect that aspect of the bean-container contract. BMP entities, however, are responsible for manufacturing their own primary keys (instantiating them with the right values), so they need to return the key to the container and thus must have a return value of the primary key type.

Unfortunately, this design choice has proven problematic for bean and container developers. In Java, method overloading of return values is prohibited, which means that a CMP entity can never be extended to create a BMP entity. But it turns out that this is just what some container vendors need to happen in order to easily generate implementation objects at deployment time. Bean developers also need this to extend prepackaged CMP entities with BMP behavior. To satisfy these needs, Sun changed the specification so that ejbCreate() in entity beans always returns the primary key type. With EJB 1.1, CMP beans simply return a null value to the container, while BMP entities return the primary key. List 9 illustrates the differences in return types for CMP entities.

List 9-A. EJB 1.0: The ejbCreate() method in CMP entities
public class AccountBean implements EntityBean {
  int id;
  double balance;
  ...
  public void ejbCreate(int identity, double startingBalance) {
            id = identity;
            balance = startingBalance;
  }   
  ...
}
List 9-B. EJB 1.1: The ejbCreate() method in CMP entities
public class AccountBean implements EntityBean {
  int id;
  double balance;
  ...
  public AccountKey ejbCreate(int identity, double startingBalance) {
            id = identity;
            balance = startingBalance;       return null;
  }   
  ...
}

Do you see a problem here? If ejbCreate(..) is required to return void for CMP in EJB 1.0, but is required to have a return type of the primary key in EJB 1.1, then 1.0 CMP entities will not run unmodified in a 1.1 server. In other words, you are going to have to change the return types of all the ejbCreate() methods in your CMP beans to be the primary key type and the actual value returned to null. This isn’t so bad, but will cause headaches for bean developers, because they will have to change the ejbCreate(..) methods in all of their entity beans. (On a related topic, note that CMP entities can also return Objects and allow the deployer to define the primary key type.)

Changes to the deployment descriptor

EJB 1.0 used serializable classes defined in the javax.ejb.deployment package to specify declarative attributes for enterprise beans. This approach was abandoned in EJB 1.1 in favor of a more flexible XML format. As a result, deployment descriptors defined using EJB 1.0 serializable classes will be of little use in EJB 1.1 servers. EJB 1.0 deployment descriptors will have to be converted to the XML format for EJB 1.1 deployments. Sun has promised to provide a converter that will read EJB 1.0 deployment descriptors and generate them for EJB 1.1. You can wait for Sun to release this utility, or you can write one yourself — it’s not that complicated. Below is a purely illustrative sample of an application that would perform this type of conversion, transforming an EJB 1.0 serialized DeploymentDescriptor into an EJB 1.1-compliant XML file. A full implementation is left as an exercise for the reader.

List 10. Illustrative example of a EJB 1.0 to EJB 1.1 deployment descriptor converter
import java.io.*;
import javax.ejb.deployment.*;
public class DDConverter {
  public static void main(String [] args) {
    validateArgs(args);
    try {
      // read the serialized EJB 1.0 DeploymentDescriptor object.
      <b>DeploymentDescriptor dd</b> = readDD(args[0]);
      String XMLString = new String();
      // document heading
      XMLString += "<?xml version="1.0"?>
n"+
        "<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN""
        + ""
      // root element
      XMLString += "<ejb-jar>n";
      XMLString += "   <enterprise-beans>n";
      if (dd instanceof SessionDescriptor) {
        XMLString += "  <session>n";
        XMLString += "    <ejb-name>"+<b>dd.getBeanHomeName()</b>+"</ejb-name>n";
        XMLString += "    <home>"+<b>dd.getHomeInterfaceClassName()</b>+"</home>n";
        XMLString += "    <remote>"+<b>dd.getRemoteInterfaceClassName()</b>+"</remote>n";
        XMLString += "    <ejb-class>"+<b>dd.getEnterpriseBeanClassName()</b>+"</ejb-class>n";
        if (<b>((SessionDescriptor)dd).getStateManagementType()</b>==<b>SessionDescriptor.STATEFUL_SESSION</b>) {
          XMLString +="      <session-type>Stateful</session-type>n";
        } else {
          XMLString +="      <session-type>Stateless</session-type>n";
        }
        ControlDescriptor [] controls = <b>dd.getControlDescriptors()</b>;
        String mgmtType = "Container";
        for (int i = 0; i < controls.length; i++) {
          if (<b>controls[i].getTransactionAttribute()</b>==<b>ControlDescriptor.TX_BEAN_MANAGED</b>) {
            mgmtType = "Bean";
            break;
          }
        }
        XMLString += "    <transaction-type>"+mgmtType+"</transaction-type>n";
        XMLString += "  </session>n";
      } else {
        ;// DO ENTITY BEAN
      }
      // DO ENVIRONMENT PROPERTIES, TX ATTRIBUTES, SECURITY ROLES, etc., HERE
      XMLString += "  </enterprise-beans>n";
      XMLString += "</ejb-jar>n";
      // write the EJB 1.1 XML version of the DeploymentDescriptor
      writeXML(args[1], XMLString);
    } catch (Exception e) { e.printStackTrace(); }
  }
  // read the EJB 1.0 DeploymentDescriptor file
  public static DeploymentDescriptor readDD(String fileName) throws Exception {
    File inFile = new File(fileName);
    FileInputStream fis = new FileInputStream(inFile);
    ObjectInputStream ois = new ObjectInputStream(fis);
    DeploymentDescriptor dd = (DeploymentDescriptor)ois.readObject( );
    ois.close();
    fis.close();
    return dd;
  }
  // write the EJB 1.0 XML file
  public static void writeXML(String fileName, String XMLString) throws Exception {
    FileWriter fw = new FileWriter(fileName);
    fw.write(XMLString);
    fw.flush();
    fw.close();
  }
  // validate that the source file and target file arguments are included in the command line
  public static void validateArgs(String [] args) {
    if (args.length < 2) {
      System.out.println("nERROR: ConverterDD did not run");
      System.out.println("Need to specify two arguments:");
      System.out.println("  1. The .ser file containing the EJB 1.1 deployment descriptor");
      System.out.println("  2. The name of the .xml file you want to create");
      System.out.println("Example:n java DDConverter /dev/CustomerDD.ser /dev/ejb-jar.xml");
      System.exit(0);
    }
  }
}

The code above illustrates the fundamental differences between EJB 1.0’s deployment descriptors and those of EJB 1.1. The EJB 1.1 XML deployment descriptors actually cover a lot more ground than the EJB 1.0 java.ejb.deployment package, so a converted deployment descriptor will require additional information not previously available in EJB 1.0.

Conclusion

Several changes in EJB 1.1 prevent beans developed in EJB 1.0 servers from porting to EJB 1.1 servers. Some of these changes affect the bean code while others are found in the new options offered by the XML deployment descriptor. In this article, we have focused on issues involving the portability of bean code, which is the topic that will cause the most grief for developers. By using the PortableContext or some similar construct, you can reduce the risk of forward-compatibility problems. The PortableContext can also be used to make beans portable across EJB vendors by creating custom implementations for specific brands of servers.

It is not necessary that you use the PortableContext developed in this article. This PortableContext is a good solution, but not the only solution. You may develop your own strategies based on the concepts developed in here, or you can simply use the PortableContext as defined. Proprietary idiosyncrasies may force you to modify the PortableContext with special vendor-specific implementation classes, or you may discover improvements to the general-purpose classes defined in this article. Either way, please relay your discoveries to me. I will keep a bulletin of proprietary extensions and improvements at my Website, EJBNow.com, for the entire EJB community. (See the Resources section below for the URL.)

The authors of this month’s server-side Java computing articles will be holding a free online seminar on January 13 at 11:00 a.m. PST. Register to join at https://seminars.jguru.com.

Richard Monson-Haefel is an EJB expert for
jGuru.com, and is the author of Enterprise JavaBeans
(O’Reilly & Associates, 1999). Richard would like to thank
Chris Raber of Gemstone, who was a technical reviewer of this
article and a contributor in the development of the
PortableContext. JavaWorld and jGuru have
formed a partnership to help the community better understand
server-side Java technology. Together, JavaWorld and jGuru
are jointly producing articles, free educational Web events, and
working together on the JavaWorld bookstore and Web-based
training.

Source: www.infoworld.com