So far, we have only discussed JNDI in the context of naming services. Now it's time to turn to directory services. At its root, a directory is merely a naming service whose objects have attributes as well as names. Programming for a directory service, such as an LDAP directory, is roughly as hard as programming for a relational database.
As we've seen, a binding in JNDI is an association between a name and an object. While this association is sufficient for some naming services, a directory service needs to be able to associate more than just a name with an object. Attributes associate specialized data with an object. In JNDI, an object with attributes as well as a name is called a directory entry.
We've been talking about the filesystem as though it were a naming system because that is how Sun's filesystem provider implements it. But if you think about it, a filesystem is really a directory system; files and directories have attributes like permissions, user IDs, and group IDs (we just can't get at these attributes using Sun's filesystem provider).
Most of the directories you'll interact with using JNDI are based on the X.500 directory services standard. For example, both standard LDAP directories and Novell's NDS directories have been influenced by X.500. As such, it is important that you know a little bit about X.500, so that you can understand how these directories work.
X.500 is a directory services standard that was developed through a collaboration between ISO and CCITT in the late 1980s. It is the "big daddy" of most directories in use today. Like all such collaborations between standards bodies and treaty organizations, the X.500 specification has the bulk of an earthmover and is about as maneuverable. But, like an earthmover, it can really get the big jobs done.
A large contributor to X.500's bulk is its schema, which is the directory type system. A directory schema is a set of rules that govern the layout of the objects in the directory. The schema determines what classes of objects can reside in a directory system, what classes of children and kinds of attributes an object is permitted to have, and what classes of values those attributes can have. If you have worked with databases, be careful not to confuse a directory schema with a database schema. A database schema is the layout of tables in the database, while a directory schema is the set of rules that control the directory layout, not the layout itself.
During the mid 1990s, researchers at the University of Michigan began to examine ways of reducing the complexity of the X.500 Directory Access Protocol (DAP). These researchers came up with the "lightweight" DAP, or LDAP, which significantly slimmed down the protocol's bulk. LDAP has gathered considerable support in the industry, so that it is now considered the standard Internet directory access protocol. Netscape is in part responsible for the acceptance of LDAP, as it declared LDAP the preferred method for accessing address books incorporated into its product line and developed the Netscape Directory Server, which is the most popular general-purpose LDAP-based directory server in use today. Note that while the LDAP protocol is simpler than the X.500 protocol, an LDAP directory still uses a directory schema.
Novell's NDS is another X.500-based directory. In the early 1990s, Novell released NetWare 4.0, which included something called NetWare Directory Services (NDS), a directory that was heavily influenced by X.500. NDS provides information about various networking services, such as printing and file services. As Novell ported NDS to other non-NetWare platforms, the name of the directory morphed into Novell Directory Services, and then NDS became its official name. As further proof of the acceptance of the LDAP protocol, even Novell has declared that the LDAP protocol is the preferred directory access protocol for NDS.
JNDI supports the X.500-based notion of a directory schema. But it can just as easily support non-X.500 schemae, such as the informal schema of a filesystem. Keep in mind that what we are discussing in this section applies to all directory services, not just X.500, LDAP, or NDS directories. As with naming services, to access a particular directory service, all you need is a service provider for that service.
javax.naming.directory.DirContext is JNDI's directory services interface. It extends Context and provides modified methods that support operations involving attributes. Like a Context, a DirContext encapsulates a set of name-to-object bindings. In addition, a DirContext contains a javax.naming.directory.Attributes object for each bound object that holds the attributes and values for that object.
The names of objects in X.500-based directories look a little different from the names we've seen so far for filesystems. If you've worked with an LDAP directory, you've probably seen names like "cn=Billy Roberts, o=Acme Products". This name is actually a compound name, while something like "o=Acme Products" is an atomic name. By convention, in an LDAP directory, the part of the name before the equals sign (e.g., "cn", "o") is stored as an attribute of the directory entry, and the rest of the name (e.g., "Billy Roberts", "Acme Products") is stored as its value. This attribute is called the key attribute. Table 6-2 lists some commonly used key attributes. Note that when a DirContext is used with an LDAP directory, it knows its name, unlike a Context.
Attribute |
Meaning |
---|---|
"c" |
A country, such as the United States or Lithuania |
"o" |
An organization or corporation, such as the Humane Society or Omni Consumer Products |
"ou" |
A division of an organization, such as the Public Relations Department or the Robotic Peace Officer Division |
"cn" |
The common name of an entity (often a user, where it can be a first name or a full name) |
"sn" |
The surname (last name) of a user |
The key attribute is closely tied to the directory entry's object class definition, otherwise known as its type. For example, in an LDAP directory, an entry that has a key attribute of "cn" has an object class of "user", while the key attribute "o" has an object class of "organization". The schema for a directory controls the object classes that can be used in the directory. The object class of a directory entry is stored as an attribute. Note that the values used for object classes are directory-dependent, so a user entry from one directory might have a different object class than a user entry from another directory even though both have the high-level notion of a user entry.
The Attributes interface represents the set of attributes for a directory entry. It has accessor methods that enable access to the entire set, as well as to specific attributes. In X.500-based directories, the name of an attribute (also called an attribute ID), such as "name", "address", or "telephonenumber", determines the type of the attribute and is called the attribute type definition. An attribute type definition is part of a directory's schema; the corresponding attribute syntax definition specifies the syntax for the attribute's value and whether it can have multiple values, among other things.
We can retrieve all the attributes of a directory entry by calling the getAttributes() method of DirContext, followed by the getAll() method of Attributes. getAttributes() returns an Attributes object. Calling the getAll() method of this object returns a NamingEnumeration of javax.naming.directory.Attribute objects, one for each attribute of the directory entry.
Example 6-10 shows the implementation of a listattrs command for NamingShell. This command prints the attributes of a directory entry, as well as string representations of the attribute values.
import java.util.Vector; import javax.naming.*; import javax.naming.directory.*; class listattrs implements Command { public void execute(Context c, Vector v) throws CommandException { String name = ""; // An empty string is OK for a listattrs operation // as it means list attributes of the current context if (!(v.isEmpty())) name = (String)v.firstElement(); if (NamingShell.getCurrentContext() == null) throw new CommandException(new Exception(), "No current context"); try { // Get the Attributes and then get enumeration of Attribute objects Attributes attrs = ((DirContext)c).getAttributes(name); NamingEnumeration allAttr = attrs.getAll(); while (allAttr.hasMore()) { Attribute attr = (Attribute)allAttr.next(); System.out.println("Attribute: " + attr.getID()); // Note that this can return human-unreadable garbage NamingEnumeration values = attr.getAll(); while (values.hasMore()) System.out.println("Value: " + values.next()); } } catch (NamingException e) { throw new CommandException(e, "Couldn't list attributes of " + name); } catch (ClassCastException cce) { throw new CommandException(cce, "Not a directory context"); } } public void help() { System.out.println("Usage: listattrs [name]"); } }
To use the listattrs command, you need to have access to a live directory server. To experiment with a live LDAP directory server, you might try the University of Michigan's server at ldap://ldap.itd.umich.edu/ or Novell's test server at ldap://nldap.com/. Another option is to download and compile the OpenLDAP source code from http://www.openldap.org/ and get an LDAP server running on your local network. To use the University of Michigan's LDAP server with NamingShell, you need to create a properties file that contains the following properties:
java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory java.naming.provider.url=ldap://ldap.itd.umich.edu/
Make sure that the JAR file for the LDAP service provider is in the classpath of NamingShell when you use this initial context information.
Once you have NamingShell set up to use a directory server, here's how you might use the listattrs command:
o=NOVELL% listattrs cn=admin Attribute: groupMembership Value: cn=DEVNET SYSOP,ou=Groups,o=NOVELL Attribute: revision Value: 235 Attribute: uid Value: admin Attribute: objectClass Value: top Value: person Value: organizationalPerson Value: inetOrgPerson Attribute: sn Value: admin Attribute: cn Value: admin
The following code in listattrs retrieves the Attributes object of the named directory context and enumerates the individual Attribute objects:
Attributes attrs = ((DirContext)c).getAttributes(name); NamingEnumeration allAttr = attrs.getAll();
Calling getAttributes() with the name of a directory entry returns an Attributes object that contains all the attributes for that entry. Another variation of getAttributes() allows you to pass the name of a directory entry and an array of attribute names (as String objects). This method returns an Attributes object that contains only the specified attributes. For example:
String[] attrIDs = {"name", "telephonenumber"}; Attributes partialAttrs = dirContext.getAttributes(name, attrIDs);
In listattrs, we used the getAll() method of Attributes to return an enumeration of Attribute objects. The Attributes interface also provides a getIDs() method that returns an enumeration of just the attribute names (or IDs) for the directory entry. If you know the attribute you want, you can specify the attribute name in a call to the get() method, which returns a single Attribute object. For example:
Attribute addr = attrs.get("address");
The Attribute interface represents a single directory attribute. We've already seen this interface in the listattrs command, where we used it to print the names and values of all the attributes of a directory context.
An attribute can have a single value or multiple values, as specified in the schema for the directory. For example, a "name" attribute might have a single value (e.g., "Billy"), while a "telephonenumber" attribute might have multiple values (e.g., "800 555 1212" and "303 444 6633").
JNDI provides several methods for working with values in an attribute. For instance, we can get one or more values, add or remove a single value, remove all values, and determine if a particular value is present.
The get() method of Attribute returns a single attribute value as a java.lang.Object. If the attribute has only a single value, get() returns that value. If the attribute has multiple values, the service provider determines the value that is returned. The following code shows how to get a single value from an attribute:
DirContext user ... ; // Created somewhere else in the program Attributes attrs = user.getAttributes(""); Attribute attr = attrs.get("telephonenumber"); Object onePhoneNumber = attr.get();
The getAll() method returns multiple attribute values as a NamingEnumeration of objects, as we saw in listattrs. Here's how to print all values stored in an attribute:
Attribute attr = attrs.get("telephonenumber"); NamingEnumeration phoneNumbers = attr.getAll(); while (phoneNumbers.hasMore()) System.out.println(phoneNumbers.next());
The add() method of Attribute enables us to add another value to an attribute:
Attribute attr = attrs.get("telephonenumber"); attr.add("520 765 4321"); // Add a new number
If we try to add a value to an attribute that doesn't support multiple values, the method does not throw an exception. The attribute simply does not accept the new value. By the same token, you can use the remove() method to remove a value from an attribute.
Attribute attr = attrs.get("telephonenumber"); attr.remove("303 444 6633"); // Remove the old number
To remove all the values from an attribute, you can call the clear() method. Note that none of these method calls actually affect the directory entry; they simply modify the local Attribute object. To make a permanent change, you have to call the modifyAttributes() method of DirContext and provide it with a modified Attribute object, as discussed in the next section.
The contains() method lets you determine whether an attribute has a certain value, while size() returns the number of values the attribute has:
Attribute attr = attrs.get("telephonenumber"); boolean itsThere = attr.contains("800 555 1212"); // Check for certain value int valuesItHas = attr.size(); // Check how many values it has
Copyright © 2001 O'Reilly & Associates. All rights reserved.