ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: Building Java Enterprise Applications, Vol I: Architecture

Business Logic, Part 2

Related Reading

Building Java Enterprise Applications
By Brett McLaughlin

by Brett McLaughlin

This is the second part of an extended excerpt from Chapter 8 of Building Java Enterprise Applications, Vol I: Architecture. This installment focuses on the UserManager component. The first installment covered the facade pattern, a way to use session beans to access entity beans, and introduced the officeManager component.

The UserManager

Once offices are set up, the next logical step is to deal with Forethought users. Users are crucial to any application, which makes the UserManager component a critical part of the Forethought application. This particular manager component will also illustrate some of the important reasons for using managers at all. Chief among those reasons are data source transparency and data format transparency. Both offer advantages to the manager clients and provide many of the security and ease-of-use benefits already discussed earlier in terms of the OfficeManager.

Data Source Transparency

In the case of Forethought offices, all information related to an office is stored in a single table, in a single data source: the Forethought RDBMS OFFICES table, which we set up in Chapter 3. While extremely convenient, this is most often not the case. It's a lot more common to find that a single logical entity (like a user) has its information stored in multiple tables (like the USERS and USER_TYPES tables), and even in multiple data sources (like the Forethought database and the Forethought directory server). As a result, working with one logical piece of data often requires operating upon multiple physical pieces of data. This can become quite a pain for application clients: they must use JDBC to connect to a database, SQL to select from and join together tables, and then JNDI to operate upon a directory server; finally, the resultant information has to be spliced together in some meaningful form. As a good developer, you should seek to avoid this complexity.

The User entity bean and the LDAPManager component have already alleviated some of these problems; these two components abstract all the details of connection and specific SQL and LDAP statements from the client. However, a client (or piece of code) would still have to know that the core information about a user is in the database, and therefore an entity bean is needed, while the authentication information is in a directory server, so the manager is employed. Add to that the need to not only utilize the User entity bean, but the UserType and possibly Office entity beans as well, and things are only marginally better than they were without beans and managers at all. What is obviously needed here is another level of abstraction. As the saying goes, "Everything in programming can be solved with another layer of abstraction." It is here that UserManager-type components come in. By providing a single component for working with users, the data sources involved with that component are hidden from the client. For example, consider the process of adding a new user. Figure 8-5 shows that while the client makes one single method invocation (to add( )), the UserManager bean actually operates upon the directory server as well as multiple entity beans. This transparency of data source not only results in the client having a much easier means of adding a user, but also removes any exposure of the underlying data schema.

Diagram.
Figure 8-5. The UserManager's add( ) method in action.

Data Format Transparency

Also in this series:

Business Logic, Part 3
In Part 3 of our excerpt from Building Java Enterprise Applications (Vol. 1, Architecture), Brett McLaughlin addresses issues of statelessness and statefulness.

Business Logic, Part 1
In this excerpt from Chapter 8 of Building Java Enterprise Applications, Vol I: Architecture, Brett McLaughlin discusses the fašade pattern, in which you use session beans to access entity beans. This access method is used instead of allowing direct access to entity beans, and is key to a sound strategy in building enterprise applications.

In addition to data source transparency, designers of complex systems often need to worry about data format transparency. Data format transparency basically means that a client does not have to make distinctions in data that characterize best practices in data storage. In other words, a client does not have to worry about how data is actually stored. The back end can be designed according to the best practices in data storage; the client doesn't know or care about the details. In other words, a client can act logically without having to think physically. Of course, I've been addressing this logical-versus-physical concern for this entire chapter, so there should be no surprises here. Consider that a client will be dealing with a user most often by that user's username (such as gqg10012). However, the directory server and database deal with the user's distinguished name (such as uid=gqg10012,ou=People,o=forethought.com). Forcing the client to worry about that lengthier and less meaningful format is clearly not desired. Your manager components can hide these format details.

As a concrete example, the UserManager component allows users to be specified by their usernames, and then internally handles conversion to distinguished names. Of course, you already have a method to do just this in the LDAPManager component. To accommodate new needs here, it makes sense to make that method public. Additionally, there is no compelling need to require an instance of LDAPManager to be available for using the method; as a result, the method can be modified to be static, as well:

public static String getUserDN(String username) {
    return new StringBuffer(  )
            .append("uid=")
            .append(username)
            .append(",")
            .append(USERS_OU)
            .toString(  );
}

You'll want to make this change in your own manager component (including the getGroupDN( ) and getPermissionDN( ) methods). With that done, the UserManager component, as well as any other session bean dealing with users, can have interactions with users based simply on a supplied username. As a result, clients don't need to deal with, or even be aware of, the format in which usernames are stored in the data sources. In this way, your managers provide data format transparency. Of course, this same principle could be applied to allowing complete names to be specified ("Mike Rhyner" would become "Mike" and "Rhyner" as first and last names), state conversions ("Texas" would become "TX"), and so forth. In these ways, manager components can allow you to simplify the job of a client.

Getting To It

You're now ready to look at the code that composes the UserManager component, and everything will become crystal clear. As always, I start with the remote interface. This is very similar to the OfficeManager interface in the standard methods that it provides for working with Forethought users. However, as users are a bit different than other Forethought entities, you will notice a few extra methods, as shown in the code listing in Example 8-4. In addition to providing two flavors of user creation (one with an office, and one without), there are methods to authenticate a user and to change a user's password. Both of these deal specifically with the authentication credentials of a user, and are common tasks in any application in which security is used. Of course, these are fairly trivial "pass-through" style methods, in which calls are made to the LDAPManager component to achieve the requested result.


Example 8-4: The UserManager Remote Interface

package com.forethought.ejb.user;
 
import java.rmi.RemoteException;
import javax.ejb.EJBObject;
 
// Office bean
import com.forethought.ejb.office.OfficeInfo;
 
// UserType bean
import com.forethought.ejb.userType.UnknownUserTypeException;
 
// LDAPManager component
import com.forethought.ldap.UserNotFoundException;
 
public interface UserManager extends EJBObject {
 
    public UserInfo get(String username) throws RemoteException;
 
    public UserInfo add(String username, String password, 
                        String firstName, String lastName, 
                        String userType)
        throws RemoteException, UnknownUserTypeException;
 
    public UserInfo add(String username, String password, 
                        String firstName, String lastName, 
                        String userType, OfficeInfo officeInfo)
        throws RemoteException, UnknownUserTypeException;
 
    public void update(UserInfo userInfo)
        throws RemoteException, UnknownUserTypeException;
 
    public boolean setPassword(String username, String oldPassword, 
                               String newPassword)
        throws RemoteException, UserNotFoundException;
 
 
    public boolean authenticate(String username, String password)
        throws RemoteException, UserNotFoundException;
 
    public boolean delete(String username) throws RemoteException;
    public boolean delete(UserInfo userInfo) throws RemoteException;
}

Example 8-5 shows the home interface for the UserManager component.


Example 8-5: The UserManager Home Interface

package com.forethought.ejb.user;
 
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
 
public interface UserManagerHome extends EJBHome {
 
    public UserManager create(  ) throws 
	  CreateException, RemoteException;
}

Note that several of these methods throw a UserNotFoundException; I mentioned this class and its use in Chapter 7. However, I left the details of putting the class into use in the LDAPManager component to you, as an exercise. Here's my modified version of the isValidUser( ) method on that class, which issues this exception if authentication is attempted with a nonexistent username:

public boolean isValidUser(String username, String password) 
    throws UserNotFoundException {
        
    try {
        DirContext context = 
            getInitialContext(hostname, port, getUserDN(username), 
                              password);
        return true;
    } catch (NamingException e) {
        // See if this was a missing user
        if (e instanceof javax.naming.AuthenticationException) {
            javax.naming.AuthenticationException ae = 
                (javax.naming.AuthenticationException)e;
            if (ae.getResolvedObj(  ) == null) {
                throw new UserNotFoundException(username);
            }
        }
        // Any error indicates couldn't log user in
        return false;
    }
}

There are certainly other ways to handle this problem that return the same result, but this was the simplest I found. Since users with invalid passwords will have related resolved objects, a test against null determines if the authentication problem was in the supplied password or the supplied username. You should make an equivalent change in your own LDAPManager component before coding the UserManager's implementation class.

Additionally, you'll notice that this new manager has a method to update a user's password, setPassword( ). This makes perfect sense; however, no such method exists on the LDAPManager component. You'll need to add this method into that class, as shown here:

public boolean updatePassword(String username, 
        String oldPassword, String newPassword) 
    throws UserNotFoundException {
        
    // Ensure this is a valid user, with 
	// a valid (old) password
    boolean isValidUser = isValidUser(username, 
	    oldPassword);
    if (!isValidUser) {
        return false;
    }
    
    try {
        // Get the user
        DirContext userContext = 
            (DirContext)context.lookup(getUserDN(username));
        
        ModificationItem[] mods = new ModificationItem[1];
 
        // Create new password attribute
        mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
            new BasicAttribute("userPassword", newPassword));
        
        // Replace old with new
        userContext.modifyAttributes("", mods);
        
        return true;
    } catch (NamingException e) {
        e.printStackTrace(  );
        return false;
    }
}

Why All the Back and Forth?

If you are following along with the code in this book, you may be wondering why I am doing a lot of "back and forth" with adding methods, changing home interfaces, updating existing methods, and so on. I certainly could have made any later changes to my own code appear in earlier chapters (through the magic of editing and book production). However, this book is about enterprise application programming, and the constant refining of code is very much a part of that process. In other words, I'm trying to give you at least a semi-realistic view of how real-life programming works. Of course, you can download completed code for all classes online at http://www.newInstance.com if you don't want to deal with these issues.

All that's left at this point is the session bean's implementation class. There is very little explanation needed for this class; if you followed along in Chapters and , you can quickly pick up the code shown in Example 8-6. In a nutshell, the component acts as a client to various entity beans and LDAP components, piecing together disparate functions into one logical method. You should examine the groupings used for each method, and see how the underlying data sources and data formats are harnessed into easy-to-use methods for manager clients.


Example 8-6: The UserManager Implementation Class

package com.forethought.ejb.user;
 
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
 
import com.forethought.ejb.util.SessionAdapter;
 
// Office bean
import com.forethought.ejb.office.OfficeInfo;
 
// UserType bean
import com.forethought.ejb.userType.UnknownUserTypeException;
 
// LDAPManager component
import com.forethought.ldap.LDAPManager;
import com.forethought.ldap.UserNotFoundException;
 
public class UserManagerBean extends SessionAdapter {
    
    /** <p> Required method for allowing bean lookups. </p> */
    public void ejbCreate(  ) throws CreateException {
        // No action required for stateless session beans
    }
 
    public UserInfo get(String username) throws RemoteException {
        User user = getUser(username);
        if (user != null) {
            return user.getInfo(  );
        } else {
            return null;
        }
    }
 
    public UserInfo add(String username, String password, 
                        String firstName, String lastName, 
                        String userType)
        throws RemoteException, UnknownUserTypeException {
         
        // Simply delegate, without an office
        return add(username, password, firstName, lastName, userType, null);
    }
 
    public UserInfo add(String username, String password, 
                        String firstName, String lastName, 
                        String userType, OfficeInfo officeInfo)
        throws RemoteException, UnknownUserTypeException {
         
        boolean addedToDirectory = false;
        LDAPManager manager = null;
        try {
            // Add user to directory server
            manager = getLDAPManager(  );
            manager.addUser(username, firstName, lastName, password);
            addedToDirectory = true;
 
            // Get an InitialContext
            Context context = new InitialContext(  );
 
            // Add user to database
            UserHome userHome = (UserHome) 
                context.lookup("java:comp/env/ejb/UserHome");
            User user = userHome.create(LDAPManager.getUserDN(username),
                                        userType, firstName, lastName,
                                        officeInfo);
            
            return user.getInfo(  );
        } catch (NamingException e) {
            /*
             * If added to directory, but not to database, remove back from
             * directory server
             */
            if (addedToDirectory) {
                try {
                    manager.deleteUser(username);
                } catch (Exception ignored) {
                    // If this dies, we're done
                }
            }
        } catch (CreateException e) {
            if (addedToDirectory) {
                try {
                    manager.deleteUser(username);
                } catch (Exception ignored) {
                    // If this dies, we're done
                }
            }
        }
        
        // If we got here, things failed
        return null;
    }
 
    public void update(UserInfo userInfo)
        throws RemoteException, UnknownUserTypeException {
        
        // This only involves database fields, so no LDAP access needed
        User user = getUser(userInfo.getId(  ));
        user.setInfo(userInfo);
    }
 
    public boolean setPassword(String username, String oldPassword, 
                               String newPassword)
        throws UserNotFoundException {
            
        try {
            LDAPManager manager = getLDAPManager(  );
            return manager.updatePassword(username, oldPassword, newPassword);
        } catch (NamingException e) {
            return false;
        }
    }
 
    public boolean authenticate(String username, String password)
        throws UserNotFoundException {
            
        try {
            return getLDAPManager(  ).isValidUser(username, password);
        } catch (NamingException e) {
            return false;
        }
    }
 
    public boolean delete(String username){
        User user = getUser(username);
        return delete(user);
    }
 
    public boolean delete(UserInfo userInfo) {
        User user = getUser(userInfo.getId(  ));
        return delete(user);
    }
    
    private User getUser(int id) {
        try {
            // Get an InitialContext
            Context context = new InitialContext(  );
 
            // Look up the User bean
            UserHome userHome = (UserHome) 
                context.lookup("java:comp/env/ejb/UserHome");
            User user = userHome.findByPrimaryKey(new Integer(id));
            
            return user;
        } catch (Exception e) {
            // Any problems - just return null
            return null;
        }
    }
 
    private User getUser(String username) {
        try {
            // Get an InitialContext
            Context context = new InitialContext(  );
 
            // Look up the User bean
            UserHome userHome = (UserHome) 
                context.lookup("java:comp/env/ejb/UserHome");
            User user = userHome.findByUserDn(LDAPManager.getUserDN(username));
            
            return user;
        } catch (Exception e) {
            // Any problems - just return null
            return null;
        }
    }
 
    private boolean delete(User user) {
        if (user == null) {
            return true;
        }
        try {
            user.remove(  );
            return true;
        } catch (Exception e) {
            // Any problems - return false
            return false;
        }
    }
    
    private LDAPManager getLDAPManager(  ) throws NamingException {
        /**
         * This could be set up to read from a properties file, but I have
         * kept it simple for example purposes.
         */
        LDAPManager manager = 
            LDAPManager.getInstance("localhost", 389,
                                    "cn=Directory Manager",
                                    "forethought");
        return manager;
    }
}

Almost all of the code shown is fairly straightforward: simple JNDI lookups for entity beans and operations upon those beans make up most of the class. Use of the LDAPManager component makes up almost all of the rest of the code. As you can see, the process of using two (or more) data sources, formats, or types is all abstracted nicely in the UserManager bean, and allows the client to happily add, delete, and update users without worrying about physical data storage details.

Building Java Enterprise Applications

Related Reading

Building Java Enterprise Applications
By Brett McLaughlin

In the next article in this series, Brett covers State Design.

Brett McLaughlin had developed enterprise Java applications for Nextel Communications and Allegiance Telecom. When that became fairly mundane, Brett took on application servers. He then got hooked on open source software, and helped found several cool programming tools, like Jakarta turbine and JDOM.


View catalog information for Building Java Enterprise Applications Vol I: Architecture

Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.