DekGenius.com
[ Team LiB ] Previous Section Next Section

9.2 Java Naming and Directory Interface

The Java Naming and Directory Interface API provides a single set of classes for accessing any kind of naming or directory service. If you are intent on learning just one enterprise API, you should learn JNDI with it; it is the door through which you will have to work to program in an enterprise environment.

BEST PRACTICE: If you intend to work with the J2EE APIs, make sure you learn JNDI.

9.2.1 Naming and Directory Services

Naming and directory services are among the most fundamental tools of computing. They enable us to access computing resources using human-friendly names. It would be hard to imagine using a computer in which you had to access a file on a hard drive by its physical location or select a printer for a print job based on its I/O port. Instead, you have a filesystem that lets you use a name that is automatically translated to a physical location on request. Before you can access just about any resource—local or remote—you need to access a naming or directory service.

A naming service is nothing more than a database that associates familiar names with technical values. A very simple example of a naming service is the Internet Domain Name Service (DNS). DNS associates computer names with IP addresses. If you want to access my web site, you type in www.imaginary.com and the application checks with a DNS server to translate that name value to the IP address that provides the actual location of my server on the Internet. Not only is www.imaginary.com easier to remember than a quad of numbers; the use of an easy-to-remember name enables me to change the physical location of the server without impacting your ability to access it.

A directory service is simply an extension of a naming service that enables you to structure data in a hierarchical namespace. In other words, not only does a directory service support access to some network object by an easy-to-remember name, but it also enables you to create a tree of information in which that object is stored. A domain object, for example, could contain many hosts. Microsoft's Active Directory Service (ADS) is among the newest examples of directory services. In ADS, Windows 2000 stores a variety of network resources, including users, computers, domains, and printers.

You may be wondering what the differences are between a directory service storing user information and a relational database storing user information. Both directory services and relational databases are specific kinds of databases. A directory service stores information in a hierarchical format and a relational database in a more complex format consistent with relational theory. A directory service is best suited to read-heavy, hierarchical data. By read-heavy, I mean that access to that data is mostly read access with occasional writes.

When writing database applications, you will deal with a variety of directory services. The most common directory services include:

  • ActiveDirectory

  • LDAP

  • NIS

The most common directory service you will access, however, is the directory service built into your J2EE application server. It may use LDAP or some other directory service, or it may follow a proprietary format. You will use it to look up J2EE resources such as JDBC data sources.

9.2.2 JNDI Architecture

JNDI is the J2EE gateway into different naming and directory services. Using JNDI, an application can store information in and retrieve information from naming and directory services. Like other Java enterprise APIs, the beauty of JNDI is that the application does not care what kind of naming or directory service is being used. The same API serves to access LDAP directories, OpenDirectory, ADS, NIS+, NDS, DNS, and more. Sun even provides an implementation of JNDI that stores information in a regular filesystem.

Some of the J2EE APIs serve as abstractions for common kinds of architectural components in enterprise systems. JNDI is one of these architectural abstractions. Enterprise applications talk to a JNDI service provider using the generic JNDI API. Figure 9-1 illustrates this architecture.

Figure 9-1. The JNDI architecture
figs/jdbp_0901.gif

No matter what naming or directory service you are using, your application will use the exact same JNDI calls to perform the exact same functions. The JNDI classes know how to find the service providers for different services based on the application's runtime configuration. These service providers implement an API called the JNDI Server Provider Interface (SPI). The SPI is specifically a set of Java interfaces that a service provider must implement in order to give JNDI access to its directory service. The advantage of this approach is that you can literally switch an application from NIS to ActiveDirectory simply by changing configuration information—no code changes are required. For the most current list of service providers, visit http://java.sun.com/products/jndi/serviceproviders.html.

9.2.3 The Basics of JNDI Programming

A Java application basically wants to do one of two things with a directory service:

  • Find objects stored in the directory service

  • Bind new or modified objects to the directory service

Because a directory service is a read-heavy data store, applications really spend most of the time looking up objects stored in the directory.

9.2.3.1 InitialContext

The first JNDI code you write in any JNDI application is code that creates an initial context. A context is simply a base from which everything is considered relative. In your local phone book, for example, the context is your country code and often an area code. The numbers in the phone book do not mention their country code or area code—you just assume those values from the context. A JNDI context performs the exact same function. The initial context is simply a special context to get you started with a particular naming and directory service. The simple form of initial context construction looks like this:

Context ctx = new InitialContext( );

In this case, JNDI grabs its initialization information from your system properties. In using this format, you make it possible for an application to be directory service-independent. You can, however, specify your own initialization values by passing the properties to the InitialContext constructor:

Properties props = new Properties( );
Context ctx;
   
// Specify the name of the class that will serve
// as the context factory
props.put(Context.INITIAL_CONTEXT_FACTORY,
          "com.sun.jndi.fscontext.RefFSContextFactory");
ctx = new InitialContext(props);

This code will create an initial context for the filesystem provider. You can now use this context to bind Java objects to the filesystem or to look them up. The most common configuration properties are:


java.naming.factory.initial (Context.INITIAL_CONTEXT_FACTORY)

This property identifies the service provider to be used by specifying the fully qualified class name of the factory class that creates the initial context object.


java.naming.language (Context.LANGUAGE)

This property stores the language preferences of the user accessing the naming or directory service. This value can be a colon-separated list of language tags. If left unspecified, the service provider determines the language preference.


java.naming.security.authentication (Context.SECURITY_AUTHENTICATION)

This property stores the security level to be used by the service provider. Its value must be one of the following strings: "none", "simple", or "strong". The security level is dependent on the service provider when this property is not specified.


java.naming.security.credentials (Context.SECURITY_CREDENTIALS)

This property stores whatever data will help authenticate the user (principal) to the naming or directory service. For example, this property could store a user's password or X.509 certificate.


java.naming.security.principal (Context.SECURITY_PRINCIPAL)

This property identifies the principal using the naming or directory service. The actual value depends on the authentication scheme being used. For example, with username/password authentication, this property will store the username.


java.naming.security.protocol (Context.SECURITY_PROTOCOL)

This property stores text identifying the security protocol to be used. For example, it might contain the text "ssl" to specify SSL (Secure Sockets Layer). If left unspecified, the service provider can interpret this property as it sees fit.

When you create a new InitialContext, the InitialContext class—which implements the Context interface—asks the service provider's initial context factory for an initial context. That object then delegates to the service provider's initial context to handle any operations.

9.2.3.2 Lookups

Lookups under JNDI are very simple. The following code finds a printer using the fileservice provider:

Printer p = (Printer)ctx.lookup("printers/laser");

This code will do a search in the directory for the object with the matching DN. If the matching object is not a printer, then you will see a ClassCastException.

Names are one of the tricky parts of JNDI. Specifically, each naming and directory service has its own name format. Examples from different domains include:

  • cn=George Reese,ou=Web,dc=imaginary,dc=com

  • c=us,o=imaginary,ou=Web,cn=Sal

  • /usr/local/bin/python

Of course, there are also names that span multiple directory services. The URL http://www.imaginary.com/Java/index.html, for example, references the file /prd/www/html/Java/index.html (a filename) on the machine www.imaginary.com (a DNS name). JNDI provides the Name class to help abstract away from naming service-specific conventions. All Context methods that look for a name as an argument will accept either a String representation of the name or a Name object representation.

Before you can look and object up in JNDI, it must first be bound to the directory. The task of assigning an object to a DN is called binding:

Printer = new Printer( );
Context ctx = new InitialContext(props);
   
p.setManufacturer("HP");
p.setModel("LaserJet 4ML");
ctx.bind("printers/Laser", p);
ctx.close( );
9.2.3.3 References

This code shows JNDI returning a Java Printer object from the directory. JNDI supports two different ways of storing Java objects in a directory:

  • Directly via Java serialization

  • Indirectly using special reference objects

The direct method stores the Java object in a directory as binary data representing the serialized form of the Java object. Many directory services, however, do not understand serialization. Furthermore, not all applications accessing a directory service are written in Java. JNDI therefore supports an alternate mechanism for storing Java objects in a directory in the form of references.

The JNDI Reference class enables a directory service to save the internal state of a Java object to a directory. A Reference also knows how to instantiate a copy of the desired Java object from data in the directory. In order to enable your Java objects to be stored by reference instead of serialization, they need to implement the Referenceable interface. This interface prescribes a single method: getReference( ). The job of this method is to create a Reference instance populated with the attributes to be stored. Example 9-1 shows how a User object might accomplish this task.

Example 9-1. Implementing Referenceable for storage in JNDI
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
   
public class User implements Referenceable {
    private String email  = null;
    private String userID = null;
   
    public User(String uid, String em) {
        super( );
        userID = uid;
        em = email;
    }
   
    public String getEmail( ) {
        return email;
    }
   
    public Reference getReference( ) throws NamingException {
        String cname = UserFactory.class.getName( );
        Reference ref = 
           new Reference(getClass( ).getName( ),     cname, null);
   
        ref.add(new StringRefAddr("email", email));
        ref.add(new StringRefAddr("userID", userID));
        return ref;
    }
   
    public String getUserID( ) {
        return userID;
    }
}

The UserFactory class referenced in the User class is used by the service provider to create a User instance when an application reads a User object from the directory service. Example 9-2 provides an implementation of this class.

Example 9-2. A factory for User instances
import java.util.Hashtable;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
   
public class UserFactory extends ObjectFactory { 
    public UserFactory( ) { 
       super( );
    }
   
    public Object getObjectInstance(Object ob,m Name nom,
                                       Context ctx, Hashtable env) {
        if( ob instanceof Reference ) {
            Reference ref = (Reference)ob;
   
            if( ref.getClassName( ).equals(User.class.getName( )) ) {
                RefAddr tmp = ref.get("userID");
                String uid, em;
   
                if( tmp != null ) {
                    uid = (String)tmp.getContent( );
                }
                tmp = ref.get("email");
                if( tmp != null ) {
                    em = (String)tmp.getContent( );
                }
                return new User(uid, em);
            }
        }
        return null;
    }
}
9.2.3.4 Attribute manipulation

Reading from a directory means more than doing straight lookups. An application will also look up object attributes. In such cases, your application should get a specific kind of context, the directory context. A directory context is represented by the JNDI class DirContext:

DirContext ctx = new InitialDirectoryContext( );

You can then grab the attributes associated with a DN using the following code:

Attributes atts =
   ctx.getAttributes("cn=Sal,ou=Web,dc=imaginary,dc=com");

The Attributes class is a collection holding all of the attributes associated with an object in the directory. You can get a specific attribute from the collection using the get( ) method:

Attribute pw = attrs.get("password");

Finally, you can access the actual attribute value using the Attribute class's get( ) method:

System.out.println("Your password is: " + pw.get( ));

Though getting attributes from a directory is pretty straightforward, changing them can be downright bizarre. You have to create a ModificationItem instance with an Attribute representing the attribute to modify. Finally, you tell the directory context to make the change:

ModificationItem[  ] changes = new ModificationItem[1];
BasicAttribute attr = new BasicAttribute("password", "secret");
   
changes[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                                    attr);
ctx.modifyAttributes("cn=Sal,ou=Web,dc=imaginary,dc=com",
                        changes);

All of this code does nothing more than change Sal's password to "secret". Under this paradigm, however, you can change multiple attributes for the same object at once. You need only add more elements to the changes array. In addition to replacing an attribute, you can use this API to add a new attribute or delete an obsolete attribute:

ModificationItem[  ] changes = new ModificationItem[2];
BasicAttribute tel, cell;
   
tel = new BasicAttribute("telephone", "+1.763.555.1778");
cell = new BasicAttribute("cellphone");
changes[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE,
                                    tel);
changes[1] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
                                    cell);
ctx.modifyAttributes("cn=Sal,ou=Web,dc=imaginary,dc=com",
                        changes);
9.2.3.5 Searching the directory

Attributes instances are also critical to searching the directory for objects. To perform a simple search, you construct an instance of BasicAttributes—which implements the Attributes interface—and specify the attributes on which you wish to search:

DirContext ctx = new DirContext( );
Attributes attrs = new BasicAttributes(true);
NamingEnumeration res;
   
attrs.put(new BasicAttribute("favoriteColor", "red"));
res = ctx.search("ou=Web", attrs);
while( res.hasMoreElements( ) ) {
    // process matching element
}

The result, of course, is a list of all of the people in the web department whose favorite color is red. The true parameter passed to the BasicAttributes constructor says that we do not want case sensitivity in our attribute matching. Each element in the resulting enumeration is actually an instance of a class called SearchResult. The SearchResult represents the object bound in the directory service. If you want the actual object, you can call getObject( ). On the other hand, you can just grab its attributes if that is all you are after:

while( res.hasMoreElements( ) ) {
    SearchResult sr = (SearchResult)res.nextElement( );
    Attributes attrs = sr.getAttributes( );
    Attribute cn = attrs.get("cn");
    
    System.out.println(cn.get( ) + " likes red!");
}

This simple search is not particularly interesting. You may want to ask questions like "Who has a Social Security number starting with 042?" or "Who has a last name of Smith?" For these more complex searches, you need to use search filters. If you are familiar with regular expressions from languages like Perl or Python, JNDI search filters will be at least somewhat familiar to you. Specifically, JNDI relies on RFC 2254 for defining its matching rules. Table 9-1 lists the symbols included in this RFC with their meanings.

Table 9-1. JNDI search filter symbols from RFC 2254

Symbol

Name

Description

&

Conjunction

The expression is true if all component expressions are true.

|

Disjunction

The expression is true if only one component expression is true.

!

Negation

Negates the truth-value of the expression.

=

Equality

The expression is true if the attribute matches the specified value in accordance with the matching rules for that attribute.

~=

Approximate equality

The expression is true if the attribute comes close to matching the specified value in accordance with the matching rules for that attribute.

>=

Greater than

The expression is true if the attribute is greater than the specified value in accordance with the matching rules for that attribute.

<=

Less than

The expression is true if the attribute is less than the specified value in accordance with the matching rules for that attribute.

=*

Presence

The attribute has a value, but the actual value is unimportant.

*

Wildcard

Matches zero or more characters in its position.

\

Escape

Escapes special symbols, including ( and ), when they appear in a filter.

The format of the search filters is a bit unintuitive. Each expression is enclosed by parentheses. Two expressions may be joined by a & or | symbol. For example:

(&(cn=* Reese)(favoriteColor=red))

This expression translates to all objects in which the cn ends with "Reese" (i.e., a last name of Reese) and the favoriteColor value is "red". You can end up with some fairly LISP-like[1] expressions on complex search filters. The following code performs a search using a more complex search filter:

[1] If you missed out on the joys of LISP in college, it is a rather odd programming language in which all notation is in reverse-polish notation, the format shown in the examples.

NamingResult res;
String flt;
   
flt = "(&(favoriteColor=red)(|(cn=* Reese)(cn=* Viega)))";
res = ctx.search("ou=Web", flt, null);
while( res.hasMoreElements( ) ) {
    // process results
}

9.2.4 Access to Enterprise Components via JNDI

If you have done any JDBC programming, you know what a nightmare it can be to connect to the database. Your application must both register a vendor-specific class with JDBC and provide connection information specific to that driver, including such information as the driver-specific URL, user ID, and password. In short, your application needs to know a lot about the specific connection requirements of a specific vendor tool to access a specific database. The result is a cumbersome connection process that is too easily tied to proprietary components.

In a robust architecture, components reference one another by name only. Just as the IP address for my web server can change without affecting your ability to reach my web site because you are using a name via a naming service, you can code your application to find other architectural components by name by registering them with some sort of naming service. For JDBC, you can store database configuration information in a directory service under a specific name. An application seeking a database connection then accesses the desired database by name. As a result, system administrators can change configuration information in the directory service without any impact on the production application.

JNDI is probably the single most important API on the J2EE platform exactly because most APIs rely on a directory service to store information about enterprise resources. The best way to access JDBC and RMI is through JNDI. The only way to access EJB is through JNDI.

9.2.4.1 JNDI and JDBC

On the J2EE platform, an application gains access to a database via JDBC's DataSource class. A DataSource instance contains all of the information necessary to make a connection. Using this class, an application can connect using the following simple code:

Connection conn;
DataSource ds;
   
// some code to get a DataSource instance goes here
conn = ds.getConnection( );

So where does the application get a DataSource instance? Naturally, it retrieves it from a directory service via JNDI. At deployment time, a system administrator goes into the administrative tool appropriate for the directory service being used and enters information about the database to which the DataSource should connect. The system administrator then assigns a name to this DataSource and saves the configuration.

How the configuration actually works is naturally dependent on the directory service being used. Ideally, this information will be stored in a serialized DataSource object in the directory service. If the directory service is incapable of storing serialized Java objects, then the configuration tool will use an alternative mechanism. Either way, your application does not care. The earlier missing code looks like this:

Context ctx = new InitialContext( );
Connection conn;
DataSource ds;
   
ds = (DataSource)ctx.lookup("enterpriseExamples");
conn = ds.getConnection( );
9.2.4.2 JNDI and EJB

EJB, like JDBC, has a special class through which applications access the system. Unlike JDBC, an application does not seek access to the EJB application server as a whole, but instead to specific business objects within the application server. The special class through which these business objects are accessed is called their home. A Flight business object, for example, would have a FlightHome home.

Homes are configured during the process of deploying an Enterprise JavaBeans application. The EJB application server takes the information from the deployment process and creates an entry in its directory service. A client then uses the application server's JNDI implementation to gain access to the deployed EJB.

In order to access a specific Flight instance, you need to know only the name under which the home is registered and the primary key for the flight being sought:

Context ctx = new InitialContext( );
FlightHome home;
Flight flight;
   
home = (FlightHome)ctx.lookup("FlightHome");
flight = home.findByPrimaryKey(somePK);
9.2.4.3 JNDI and other enterprise components

You should see a pattern emerging here. Everywhere one enterprise component needs to access another, it does so via JNDI. All of the information required for that access is stored in a JNDI-supported directory. This architecture enables components to change drastically without any effect on the other components in the system. The freedom to reconfigure components in a runtime is critical to scalability. For example, an application that was launched with the web server, application server, and database server all on the same physical machine can be reconfigured to run on three different machines without requiring any code changes or regression testing.

    [ Team LiB ] Previous Section Next Section