JNDI—Java Naming and Directory Interface By Mark Wutka
Date: Apr 5, 2002
Enterprise-level applications use a lot of different directory services-lookup services that locate resources associated with a particular name. When you use RMI, for example, you locate objects with a directory service called the RMI Registry. When you use CORBA, you use the COS Naming facility (CORBA's naming service) to locate objects. When you convert a hostname to an IP address, you usually use a directory service called DNS (Domain Name Service). There are also general directory services that use protocols, such as X.500 (the CCITT directory standard) and LDAP (Lightweight Directory Access Protocol). These directory services can hold many kinds of data. Although most people tend to use the terms "naming service" and "directory service" interchangeably, there is a difference. A naming service associates a single name with a specific resource. A directory service associates a name with a set of attributes and resources. When you search a naming service, you can only search for a specific name. When you search a directory, you can search for items matching a specific set of attributes. One of the interesting things about all these types of naming and directory services is that they generally perform the same task-mapping a name to some set of attributes or objects. Of course, not all directory services are created equally. Some of them have a flat namespace, whereas others offer a tree structure for the names. Some of them allow you to store specific types of objects, whereas others allow you to store almost any kind of object. The Java Naming and Directory Interface (JNDI) draws a distinction between naming services and directory services. A naming service maps a name to an object. The RMI Registry and the CORBA Naming Service are both examples of naming services. You can only store an RMI object in the RMI Registry and you can only store a CORBA object in the CORBA Naming Service. A directory service also stores objects, but these objects can have associated attributes that the directory service recognizes. You can search a directory using the item attributes. For example, you can search an LDAP directory for everyone in a specific department or everyone named Smith. JNDI provides a uniform way to access naming and directory services. It supports flat namespaces as well as tree namespaces, and it allows you to store many different types of objects. The beauty of JNDI lies it its simplicity and uniformity. After you know the basic JNDI API calls, you can read data out of any kind of directory as long as there is a JNDI service provider for that directory. You have already encountered JNDI in several earlier chapters. You use JNDI to locate Enterprise JavaBeans and JDBC connection pools from within your EJB container. You might have implemented simple lookup schemes before in your applications; that is, you create a class with static lookup methods or store a Hashtable in a static field somewhere. You might choose to use JNDI to replace these kinds of local storage mechanisms, although you might need to write your own service provider. JNDI is also extremely useful in the area of configuration. If many applications use common configuration data, you might consider storing the data in a directory service, such as LDAP, instead of in a file or database. LDAP is especially good if the configuration information is hierarchical-that is, if it is more like a tree structure than a flat list of values. One of the hidden benefits of directory services is the fact that there are a lot of directory service browsers and editors-especially for LDAP. You can view the contents of the directory and edit them using an off-the-shelf tool. That saves you from having to write a custom configuration editor.
JNDI—Java Naming and Directory Interface
1
JNDI Basics The Context class is the core of the JNDI API. You use it to perform any lookup and to add any new name-value associations. When you use JNDI, you typically create an InitialContext object first: Context ctx = new InitialContext();
The InitialContext constructor looks for a system property called java.naming.factory. initial that contains the name of the class that creates the InitialContext. Sometimes, you must supply this value yourself. Some EJB containers, like the one that comes with Sun's J2EE SDK, already have this property set. JDK 1.3 comes with three built-in service providers: RMI, CORBA, and LDAP. The classnames for the different initial context factories are com.sun.jndi.rmi.registry.RegistryContextFactory com.sun.jndi.cosnaming.CNCtxFactory com.sun.jndi.ldap.LdapCtxFactory
Note Don't worry about setting defining the class for the initial context factory unless you get an error telling you there's no initial context factory. When you run your program, you can specify the initial context factory on the command-line using the -D option: java -Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory
usingj2ee.naming.JNDIDemo You can also specify the initial context factory in a Hashtable that you can pass to the InitialContext constructor: Hashtable props = new Hashtable (); props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); Context ctx = new InitialContext(props);
Bear in mind that if you specify the initial context factory using a Hashtable object, you might be limiting the portability of your classes. For example, most WebLogic examples tell you to create the InitialContext this way: Hashtable props = new Hashtable(); props.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory"); props.put(Context.PROVIDER_URL, "t3://localhost:7001"); Context = new InitialContext(props);
The problem here is that if you want to run your code with another application server, you'll have to recompile your code with a new set of properties. It's better to set these items on the command line: java Djava.naming.factory.initial=weblogic.jndi.WLInitialContextFactory
-Djava.naming.provider.url=t3://localhost:7001 MyTestClient
JNDI—Java Naming and Directory Interface
2
Tip Rather than specifying the initial factory on the command line, you can put these associations in a file called jndi.properties, which can be located somewhere in your classpath. When you develop Enterprise Java Beans, you can usually count on the environment being set up properly ahead of time, so you normally don't need to initialize any properties or set any system properties. When you run your client programs to test the EJBs, however, you usually need to specify an initial context factory. Although most people use the InitialContext object as their first entry point into JNDI, there is an alternative. You can use the javax.naming.spi.NamingManager class to create a service-specific context for you based on a URL prefix. A fully qualified JNDI name is of the form service://itemname, where service is a name such as iiop, rmi, ldap, and so on, and itemname is the name of the item in that service. The NamingManager class lets you create a Context object based on the service name. For example, to create an LDAP Context object, you can call: Context ctx = NamingManager.getURLContext("ldap", null);
One thing to keep in mind when you use this technique is that the Context you get back usually doesn't understand names for other services. For example, if you create an initial context that is a CORBA naming service, you can still do an LDAP lookup like this: Object ob = context.lookup("ldap://localhost/dc=wutka,dc=com");
The InitialContext object knows how to resolve references that use other kinds of services. If you try this with a context returned by getURLContext, however, you'll get an error telling you that the name isn't valid for the context you are using. Okay, now that you have a Context object, you can use the lookup method to locate an object. For example, when you locate an EJB, you usually make a call like this: Object personHomeRef = context.lookup( "java:comp/env/ejb/Person");
Tip Don't forget, if you need to cast the result from context.lookup to a specific Remote or Home interface type, you must use PortableRemoteObject.narrow. The java service is available only within an EJB container, and it acts as a local directory service for other objects within the same EJB environment. To create a new name-value association, use the bind method: ctx.bind("rmi://localhost/MyRemoteObject", remoteObject);
If the object already exists in the directory, bind throws a NameAlreadyBoundException. The rebind method does the same thing as bind except that it doesn't care whether the object already exists: ctx.rebind("rmi://localhost/MyRemoteObject", remoteObject);
rebind doesn't throw an exception if the object doesn't exist; that is, you can use rebind to create a new association as well as to overwrite an old one.
JNDI—Java Naming and Directory Interface
3
To remove an association, call unbind: ctx.unbind("rmi://localhost/MyRemoteObject");
To rename an association, call rename: ctx.rename("rmi://localhost/MyRemoteObject", "rmi://localhost/MyNewRemoteObject");
You can close the InitialContext by calling the close method: ctx.close();
Because the context uses resources in the naming or directory service, you should close the context when you are done with it. Note Make sure each EJB client creates its own InitialContext, especially if you are using EJB security credentials. The credentials are tied to the InitialContext, and if you aren't careful, one client may be using another client's credentials. Normally this isn't a problem if the clients are running as separate processes. If you're writing a Web application, however, on a server that acts as multiple clients, you must be careful to keep the contexts separated. Directory Operations JNDI has directory-specific extensions for performing directory operations as opposed to the simple name-value operations in most naming services. The DirContext interface and InitialDirContext classes provide additional methods for dealing with directories. The directory-specific classes are all contained within the javax.naming.directory package. If you need to perform directory operations, create an InitialDirContext instead of an InitialContext. For example DirContext dirCtx = new InitialDirContext();
All the same rules apply to InitialDirContext as far as the property names for choosing an initial context factory. The Attribute interface and the BasicAttribute class represent an attribute of an object stored in a directory. An attribute might have more than one value, but it only has a single name. For example, a directory entry representing a person might have an attribute called children that could contain any number of names. A person might also have an age attribute containing a single number. Most of the methods in the Attribute interface are for dealing with multi-valued attributes. There are add methods and remove methods, as well as get and set methods: public public public public public public public
void add(int index, Object value) boolean add(Object value) Object remove(int index) boolean remove(Object value) Object get() Object get(int index) Object set(index, Object value)
The Attributes interface and the BasicAttributes class encapsulate all the attributes. It's easier to manage single-valued attributes from the Attributes class than it is to first get an Attribute and then perform the manipulation. The main methods you use are get, put, and remove:
JNDI—Java Naming and Directory Interface
4
public public public public
Attribute Attribute Attribute Attribute
get(String attrName) put(Attribute attr) put(String attrName, Object attrValue) remove(String attrName)
The Attribute returned by put is the attribute being replaced by the new attribute-that is, the attribute previously stored under the same name as the new attribute. If there was no attribute with that name, put returns null. The remove method returns the Attribute that is being removed, or null if no such attribute exists. The bind and rebind methods in the DirContext interface let you bind an object with a specific set of attributes: public public public public
void void void void
bind(String name, Object ob, Attributes attrs) bind(Name name, Object ob, Attributes attrs) rebind(String name, Object ob, Attributesattrs) rebind(Name name, Object ob, Attributes attrs)
The DirContext interface also provides several variations of a search method. One of the things that distinguishes a directory service from a naming service is that you can search for items based on a set of attributes and not a specific name. For instance, find all people with age greater than 18. The various search methods are public NamingEnumeration search(Name name, Attributes[] matchAttributes) public NamingEnumeration search(Name name, Attributes[] matchAttributes, String[] attributesToReturn) public NamingEnumeration search(Name name, String searchFilter, SearchControls controls) public NamingEnumeration search(Name name, String searchFilter, Object[] filterArgs, SearchControls controls) public NamingEnumeration search(String name, Attributes[] matchAttributes) public NamingEnumeration search(String name, Attributes[] matchAttributes, String[] attributesToReturn) public NamingEnumeration search(String name, String searchFilter, SearchControls controls) public NamingEnumeration search(String name, String searchFilter, Object[] filterArgs, SearchControls controls)
Using LDAP with JNDI Of the directory services supported by JDK 1.3, LDAP is by far the most flexible. You can store a wide variety of items in an LDAP directory and you can get LDAP servers for a wide variety of operating systems. A good place to get a free LDAP server for Linux and Unix is http://www.openldap.org. They are also working on a version for Windows NT. LDAP stores data in a hierarchical (tree) structure. You refer to an entry in the tree by listing the names of the nodes in the tree, starting at the one you want, working backward to the top of the tree. LDAP paths look confusing at first, but after you understand the notation, it's not so bad. Figure 18.1 shows an example LDAP tree.
JNDI—Java Naming and Directory Interface
5
Figure 18.1. LDAP stores its entries in a tree structure.
Each node in the tree has a unique name of the form nodetype=value. That is, the name includes the type of the node, at least to some extent. For example, the top part of the tree in Figure 18.1 has nodes that represent the LDAP server's domain. These topmost nodes are domain components. For a domain of http://wutka.com, you have two domain components: wutka and com. Node type for a domain component is dc, so the topmost nodes are named dc=wutka and dc=com. Underneath the wutka domain component is an organization called Wutka Consulting. An organization has a node type of o, so the Wutka Consulting node has a name of o=Wutka Consulting. Now, if you're using JNDI to access the wutkaconsulting node, you must list the node names starting from the one you want and working backward to the top. In other words, the name you want is o=Wutka Consulting,dc=wutka,dc=com. Listing 18.1 shows a program that reads the Wutka Consulting object and prints out its attributes. Listing 18.1 Source Code for ShowWC.java package usingj2ee.naming; import javax.naming.*; import javax.naming.directory.*; public class ShowWC { public static void main(String[] args) { try { // Get the initial context InitialDirContext ctx = new InitialDirContext(); // Locate the Wutka Consulting object on the server running // at ldap.wutka.com Attributes attrs = ctx.getAttributes( "ldap://ldap.wutka.com/o=Wutka Consulting, dc=wutka, dc=com"); // Get the attributes for the object NamingEnumeration e = attrs.getAll(); while (e.hasMoreElements()) { // Get the next attribute Attribute attr = (Attribute) e.nextElement(); // Print out the attribute's value(s) System.out.print(attr.getID()+" = "); for (int i=0; i < attr.size(); i++) { if (i > 0) System.out.print(", "); System.out.print(attr.get(i)); } JNDI—Java Naming and Directory Interface
6
}
}
System.out.println();
} catch (Exception exc) { exc.printStackTrace(); }
}
Figure 18.2 shows the output from the ShowWC program. Figure 18.2. It's easy to print the attributes in an LDAP object. Note Due to possible network changes, you may not be able to access http://ldap.wutka.com in the future. You might need to set up your own LDAP server to run the example. LDAP Classes and Attributes Although LDAP entries are really just a collection of attributes, LDAP has the concept of classes. Every LDAP entry has an attribute called objectClass that lists the class hierarchy for an object. Not only does objectClass contain the object's class, it must contain the entire list of superclasses all the way back to the top class. Fortunately, the classes aren't nested too deeply, so the objectClass list is usually fairly small. One other thing to keep in mind: The class hierarchy doesn't dictate the structure of the directory tree. A node in the directory tree can contain one of its superclasses as a child. Table 18.1 lists some of the common LDAP classes. The complete set of classes is defined in the standard RFC2256, which you can view at http://www.ietf.org/rfc/rfc2256.txt. Table Some Common LDAP Classes Classname
Parent Class
Required Attribute(s)
top
None
ObjectClass
country
top
c
locality
top
none
organization
top
o
organizationalUnit
top
ou
person
top
sn, cn
organizationalPerson
top
none
The LDAP specification also lists some common attribute names. These attribute names tend to look confusing at first glance because many of them are only one or two characters long. You see these attributes in other places too, such as in X.509 certificates (for digital signatures and encryption). One of the reasons for the similarity is that LDAP uses many of the items defined in the X.500 series of recommendations (standards), which includes X.509. Table 18.2 lists some of the common attributes and their meanings. JNDI—Java Naming and Directory Interface
7
Table Some Common LDAP Attributes Attribute Name
Meaning
objectClass
The classname of the object and its superclasses
dc
A domain context-a part of a domain name
cn
Common name, usually the name of the object
sn
Surname-a person's family name (the last name in most Western cultures)
c
The standard two-letter country code
l
Locality (city, county, or other region)
st
State or province
o
Organization
ou
Organizational unit
title
A person's job title
personalTitle
A person's personal (not job-related) title
description
A description of the object
mail
A person's email address
One other concept you should be aware of is that a context is really a set of names. You can create a context that is a subset of names by calling createSubcontext in the DirContext object. Essentially, a subcontext is just the set of names starting at a particular node in the directory tree. The interesting thing is, you create a new node in the tree by creating a new subcontext. Listing 18.2 shows a program that adds two entries to the LDAP directory. Notice that the program must supply a username in the form of a SECURITY_PRINCIPAL and a password in the form of SECURITY_CREDENTIALS to make changes to the LDAP directory. Most servers let you read the directory anonymously but require a username and password to make changes. Listing 18.2 Source Code for AddPerson.java package usingj2ee.naming; import java.util.*; import javax.naming.*; import javax.naming.directory.*; public class AddPerson { public static void main(String[] args) { try { // Pass the security information to the directory context // The LDAP server requires a username (SECURITY_PRINCIPAL) // and password (SECURITY_CREDENTIALS) to add/remove // items. JNDI—Java Naming and Directory Interface
8
Hashtable props = new Hashtable(); props.put(Context.SECURITY_PRINCIPAL, "cn=Manager,dc=wutka,dc=com"); props.put(Context.SECURITY_CREDENTIALS, "secret"); // Get the initial context InitialDirContext ctx = new InitialDirContext(props); // Create a new set of attributes BasicAttributes attrs = new BasicAttributes(); // The item is an organizationalPerson, which is a subclass of person. // Person is a subclass of top. Store the class hierarchy in the // objectClass attribute Attribute classes = new BasicAttribute("objectclass"); classes.add("top"); classes.add("person"); classes.add("organizationalPerson"); // Add the objectClass attribute to the attribute set attrs.put(classes); // Store the other attributes in the attribute set attrs.put("sn", "Tippin"); attrs.put("title", "Computer Expert"); attrs.put("mail", "[email protected]"); // Add the new entry to the directory server ctx.createSubcontext("ldap://ldap.wutka.com/cn=Samantha Tippin,"+ "o=Wutka Consulting,dc=wutka,dc=com", attrs); // Create another set of attributes attrs = new BasicAttributes(); // Use the same objectClass attribute as before attrs.put(classes); // Set the other attributes attrs.put("sn", "Tippin"); attrs.put("title", "Computer Expert"); attrs.put("mail", "[email protected]"); // Add another entry to the directory server ctx.createSubcontext("ldap://ldap.wutka.com/cn=Kaitlynn Tippin,"+ "o=Wutka Consulting,dc=wutka,dc=com", attrs); } catch (Exception exc) { exc.printStackTrace(); } } }
JNDI—Java Naming and Directory Interface
9
It's fairly easy to search through an LDAP directory using JNDI. You just call the search method in the DirContext. There are two main ways to search: by specifying either a set of attributes to match or an LDAP filter string. Attribute matching is very straightforward, as you can see in Listing 18.3. Listing 18.3 Source Code for Name Search.java package usingj2ee.naming; import javax.naming.*; import javax.naming.directory.*; public class NameSearch { public static void main(String[] args) { try { // Get the initial context InitialDirContext ctx = new InitialDirContext(); // Create the search attributes - look for a surname of Tippin BasicAttributes searchAttrs = new BasicAttributes(); searchAttrs.put("sn", "Tippin"); // Search for items with the specified attribute starting // at the top of the search tree NamingEnumeration objs = ctx.search( "ldap://ldap.wutka.com/o=Wutka Consulting, dc=wutka, dc=com", searchAttrs); // Loop through the objects returned in the search while (objs.hasMoreElements()) { // Each item is a SearchResult object SearchResult match = (SearchResult)objs.nextElement(); // Print out the node name System.out.println("Found "+match.getName()+":"); // Get the node's attributes Attributes attrs = match.getAttributes(); NamingEnumeration e = attrs.getAll(); // Loop through the attributes while (e.hasMoreElements()) { // Get the next attribute Attribute attr = (Attribute) e.nextElement(); // Print out the attribute's value(s) System.out.print(attr.getID()+" = "); for (int i=0; i < attr.size(); i++) { if (i > 0) System.out.print(", "); System.out.print(attr.get(i)); } System.out.println(); JNDI—Java Naming and Directory Interface
10
}
}
System.out.println("---------------------------------------"); } } catch (Exception exc) { exc.printStackTrace(); }
}
Searching by filter is a little more complicated. Any LDAP filter string must be surrounded by parentheses. To match all objects in the directory, you can use a filter string, such as (objectClass=*). You can do comparisons using =, >=, <=, and ~= (approximately), like (age>=18). The syntax for and, or, and not is a little strange. If you want to test age>=18 and sn=Smith, the expression is (&(age>=18)(sn=Smith)). Use & for and, | for or, and ! for not. For and and or, you can list as many expressions as you want to after the & or | characters. For not, you can only have a single expression. For example, because you can only do a greater-than-or-equal-to comparison (>=), you do a greaterthan by doing not-less-than-or-equal-to. For example, if age must be strictly greater than 18, use (!(age<=18)). If you need to combine the and and or operators, you must use parentheses to separate the expressions. For example, you might want to search for age>=18 or (age >=13 and parentalPermission=true). The expression would be (|(age>=18)(&(age>=13)(parentalPermission=true))). The two expressions being ored together are (age>=18) and (&(age>=13)(parentalPermission=true)). You can find a full definition of the LDAP search filter syntax in RFC1558 (http://www.ietf.org/rfc/rfc1558.txt). Listing 18.4 shows a program that performs a simple filter search to dump out the entire contents of the directory. Listing 18.4 Source Code for AllSearch.java package usingj2ee.naming; import javax.naming.*; import javax.naming.directory.*; public class AllSearch { public static void main(String[] args) { try { // Get the initial context InitialDirContext ctx = new InitialDirContext(); SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE ); // Search for items with the specified attribute starting // at the top of the search tree NamingEnumeration objs = ctx.search( "ldap://ldap.wutka.com/o=Wutka Consulting, dc=wutka, dc=com", JNDI—Java Naming and Directory Interface
11
"(objectClass=*)", searchControls); // Loop through the objects returned in the search while (objs.hasMoreElements()) { // Each item is a SearchResult object SearchResult match = (SearchResult) objs.nextElement(); // Print out the node name System.out.println("Found "+match.getName()+":"); // Get the node's attributes Attributes attrs = match.getAttributes(); NamingEnumeration e = attrs.getAll(); // Loop through the attributes while (e.hasMoreElements()) { // Get the next attribute Attribute attr = (Attribute) e.nextElement(); // Print out the attribute's value(s) System.out.print(attr.getID()+" = "); for (int i=0; i < attr.size(); i++) { if (i > 0) System.out.print(", "); System.out.print(attr.get(i)); } System.out.println(); } System.out.println("--------------------------------------"); } } catch (Exception exc) { exc.printStackTrace(); } } }
Troubleshooting Initial Context Q: Why do I get an error when I create the initial context? A: You might need to specify an initial context factory property. You may also need to specify a PROVIDER_URL or other properties. Consult the documentation for your JNDI implementation to see what properties it requires. General Errors Q: Why can't I store my object in the naming service? A: Some services only allow certain types of objects. The RMI Registry only accepts RMI object and the CORBA Naming service only accepts CORBA objects. Other services might require that your object is serializable.
JNDI—Java Naming and Directory Interface
12