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


Developing A White Pages Service with LDAP and JNDI

by Budi Kurniawan
05/21/2001

Lightweight Directory Access Protocol (LDAP) is increasingly popular because it simplifies what has been complex, namely, accessing directory services. In this article you will learn what you can do with LDAP and how Java handles LDAP with its Java Naming and Directory Interface (JNDI) API. At the end of the article, you will find a project that provides a white pages service built with LDAP and JNDI.

Naming and Directory Services

Before you start the project, you should first familiarize yourself the jargon of naming and directory services.

The Naming Service

A naming service lets you find an object in a system based on the name associated with the object. Naming services are easy to find. Take the filesystem in your computer, for instance. Every file in a computer has a name; to access a file you must know its name. In filesystems, files are objects associated with filenames. Another example is the Internet's DNS, which maps easy-to-remember names (such as onjava.com and jUpload.com) to IP addresses. When you work with Enterprise JavaBeans (EJB), you use a naming service to get a reference to a Bean. In short, a naming service allows you to look up an object by its name.

Each naming service has its own rules for making valid names. For example, the rules for valid filenames Linux are different from the rules in Windows.

The association of a name with an object is called a binding. In a filesystem, a filename is bound to a file. In a DNS server, domain names are bound to IP addresses.

Objects in some naming services cannot be stored directly inside the naming service. Instead, the name service stores pointers or references to objects. A reference contains an address, that is, specific information on how to access the object itself.

A Context

In a naming service, obviously you have more than one name-to-object binding. The set of bindings is called a context. There are two types of contexts: root and subcontext. A root context is the base name of an object. In a filesystem, the root context is the base from which all other directories and files are stored. In the Unix file system, the root context is /. Under Windows it is normally C:\.

A subcontext is a name that adds another level to the root context. For example, a directory, such as usr under / in a Unix filesystem, is a subcontext. In the Unix system, this subcontext is called a subdirectory. That is, in a directory, /usr, the directory usr is a subcontext of /. In another example, a DNS domain, like COM or NET, is a context. A DNS domain named relative to another DNS domain is a subcontext. For example, in the DNS domain brainysoftware.com, the DNS domain brainysoftware is a subcontext of COM.

The Directory Service

A directory service is an extension of a naming service. In a directory service, an object is also associated with a name. However, each object is allowed to have attributes. You can look up an object by its name; but you can also obtain the object's attributes or search for the object based on its attributes.

Going back to the Unix file system, it's not just a naming service but also a directory service. Each file can have attributes like owner and date. In real world applications, a directory object in a directory server can be used to represent anything: a printer, a computer, a network, or even a person in an organization.

Attributes

An attribute of a directory object is a property of the object. For example, a person can have the following attributes: last name, first name, user name, email address, telephone number, and so on. A printer can have attributes like resolution, color, and speed.

An attribute has an identifier which is a unique name in that object. Each attribute can have one or more values. For instance, a person object can have an attribute called LastName. LastName is the identifier of an attribute. An attribute value is the content of the attribute. For example, the LastName attribute can have a value like "Martin".

Searches and Search Filters

You can look up a directory object by supplying its name to a directory service. Alternatively, many directory services support searches for objects based on properties, not just names. You can supply a query consisting of a logical expression in which you specify the attributes that the object or objects must have. The query is called a search filter. This style of searching is sometimes called reverse lookup or content-based searching. The directory service searches for and returns the objects that satisfy the search filter.

LDAP

Directory services are very common these days. There already exist a plethora of directory service implementations:

Accessing a directory service and manipulating its objects used to be complex and difficult. The traditional protocol is X.500, a set of directory recommendations specified by the International Telecommunication Union. X.500 was enormous and complex.

LDAP is a direct descendant of X.500. LDAP was designed at the University of Michigan to simplify access to X.500 directories (hence the "L" for "lightweight" in "LDAP"). LDAP was designed to be powerful enough to solve basic problems in accessing a directory service but simple and light enough so more people can afford to use it.

LDAP, currently at version 3, is now a standard for directory information access. Many companies, including Microsoft, IBM, Novell, Computer Associates, and Sun, have agreed to support it. LDAP is now being used as an important part of a variety of services: authentication systems, mail systems, and e-commerce applications. To date more than 60 LDAP server implementations have been released; approximately 90% of which are standalone LDAP directory servers, 10% of which are components of other applications.

You probably already have an LDAP-aware client installed on your computer. Many email clients can access an LDAP directory for email addresses, including Outlook, Eudora, Netscape Communicator, QuickMail Pro, and Mulberry.

The LDAP naming convention orders components from right to left, delimited by a comma. LDAP arranges all directory objects in a tree, called a directory information tree (DIT). Within the DIT, an organization object, for example, might contain group objects that might in turn contain person objects. When directory objects are arranged in this way, they play the role of naming contexts in addition to being attribute containers.

Standard LDAP Operations

In addition to client connections and disconnections, an LDAP server must provide the following:

Note that the term binding in LDAP is different from its generic directory services meaning. Binding here refers to the authentication that a user is required to perform before accessing an entry in the directory.

Public LDAP Servers

There are several public LDAP servers you can use over the Internet; the popular of these is probably BigFoot's (ldap://ldap.bigfoot.com); others include ldap://ldap.four11.com and ldap://ldap.InfoSpace.com.

A number of universities in the US also provides LDAP service to search for students or staff members. For a list of university public LDAP services, see eMailman's Public LDAP Servers.

Choosing an LDAP Server

Your organization may already run a directory service, especially if its very large. If not, you probably need to do some research before deciding on one.

The most popular LDAP server today is iPlanet's Directory Server. Others include Novell's NDS eDirectory, Critical Path's Global Directory Server, Computer Associates' eTrust Directory, Siemens' DirX, and Oracle's Oracle Internet Directory. Deciding the one which is best for your situation is often tricky.

NetworkWorld Fusion published a good article last year which compares the performance of many LDAP servers. If it's to be believed, iPlanet is the best performer and also the fastest; it concludes that iPlanet's Directory Server is the best choice for commercial use.

If you only need an LDAP server for testing, you probably want to use something else. Downloads for the latest version of iPlanet's Directory Server (version 5.0 beta) range from 53 MB to 78 MB, depending on your operating system. For the project in this article, I chose the much slimmer LDAP server from OpenLDAP. Even though not the fastest, theis free product is only a 1.52 MB download. OpenLDAP's products are only available for Linux; but once you have seeded it with entries, you can use this article's project code to access any LDAP server on any operating system.

Installing OpenLDAP

You can download OpenLDAP from the project's site. The LDAP server is called slapd (a stand-alone LDAP server). The latest version of slapd is 2.0.7. Other programs downloadable from the Web site are the replication server, some libraries, and a variety of tools.

To install slapd, you first need to download openldap-2_0_7.tgz into the /usr/local/ directory of a Linux system. You can use another directory but you'll need to do some adjustment to the following instructions.

Installation takes the following steps:

  1. Extract the files --

    gunzip -c openldap-2_0_7.tgz | tar xvfB -

    -- into a subdirectory, openldap-2.0.7. If you are using a different version, this subdirectory is called openldap-VERSION.

  2. Assuming that your current working directory is /usr/local, run

    cd openldap-2.0.7
  3. Next, run

    ./configure
  4. Then the following commands:

    make depend
    make
    make test
  5. If everything goes smoothly, you are now ready to install, for which you'll need root access. Run

    su root -c 'make install'

Configuring slapd

If installation is as expected, you are now ready to configure slapd. The configuration file is called slapd.conf and can be found at the /usr/local/etc/openldap/ directory. Open this file with your favorite text editor.

You should see the following lines.

database ldbm 
suffix "dc=<MY-DOMAIN>,dc=<COM>" 
rootdn "cn=Manager,dc=<MY-DOMAIN>,dc=<COM>" 
rootpw secret 
directory /usr/local/var/openldap-ldbm

You need to edit the <MY-DOMAIN> and the <COM> parts to reflect your domain name. Using the correct names ensures that your LDAP server can be accessed from the Internet.

For example, for the brainysoftware.com domain, the configuration lines will look like

database ldbm 
suffix "dc=brainysoftware,dc=com" 
rootdn "cn=Manager,dc=brainysoftware,dc=com" 
rootpw secret 
directory /usr/local/var/openldap-ldbm

If your domain contains additional components -- like sandal.jepit.edu.au -- do something like

database ldbm 
suffix "dc=sandal,dc=jepit,dc=edu,dc=au" 
rootdn "cn=Manager,dc=sandal,dc=jepit,dc=edu,dc=au" 
rootpw secret 
directory /usr/local/var/openldap-ldbm

The fourth line (rootpw secret) contains the root password that you need to supply to the server to make changes to the entries and do some other functions.

Running slapd

Running slapd requires root access, so run

su root -c /usr/local/libexec/slapd

or

/usr/local/libexec/slapd

if you're already logged in as root.

To check that the server is running and configured correctly, you can search it with ldapsearch. By default, ldapsearch is installed as /usr/local/bin/ldapsearch. Use the following command:

ldapsearch -x -b '' -s base '(objectclass=*)' namingContexts

The Schema file

An important file in an LDAP server is the schema file, which, for slapd, is called core.schema, located at /usr/local/openldap-2.0.7/etc/openldap/schema/. It contains the directory schema of the LDAP server.

A directory schema specifies, among other things, the types of objects that a directory may have and the attributes that are mandatory and optional to that object. A directory schema also contains attribute type definitions, object class definitions, and other information which a server uses to determine how to match a filter or attribute value assertion against the attributes of an entry, and whether to permit add and modify operations.

The LDAP v3 schema is based on the X.500 standard for common objects found in a network like countries, localities, organizations, users/persons, groups and devices.

The LDAP v3 schema is specified in RFC 2252 and RFC 2256.

All LDAP entries in the directory are typed. Each entry belongs to object classes that identify the type of data represented by the entry. The object class specifies the mandatory and optional attributes that can be associated with an entry of that class. The object classes for all objects in the directory form a class hierarchy. The classes top and alias are at the root of the hierarchy. For example, the organizationalPerson object class is a subclass of the Person object class, which in turn is a subclass of top. When creating a new LDAP entry, you must always specify all of the object classes to which the new entry belongs. Because many directories do not support object class subclassing, you also should always include all of the superclasses of the entry.

Three types of object classes are possible:

For example, for an organizationalPerson object, you should list in its object classes the organizationalPerson, person, and top classes. The organizationalPerson, person, and top objects are listed as the following entries in the core.schema file.

objectclass ( 2.5.6.0 NAME 'top' ABSTRACT
  MUST objectClass )

objectclass ( 2.5.6.6 NAME 'person' SUP top STRUCTURAL
  MUST ( sn $ cn )
  MAY ( userPassword $ telephoneNumber $ seeAlso $ description )
)

objectclass ( 2.5.6.7 NAME 'organizationalPerson' SUP person 
STRUCTURAL
  MAY ( title $ x121Address $ registeredAddress $
        destinationIndicator $ preferredDeliveryMethod $ 
        telexNumber $ teletexTerminalIdentifier $
        telephoneNumber $ internationaliSDNNumber $
        facsimileTelephoneNumber $ street $ postOfficeBox $
        postalCode $ postalAddress $ physicalDeliveryOfficeName $ 
        ou $ st $ l 
  ) 
)

LDAP v3 specifies that each directory entry may contain an operational attribute that identifies its subschema subentry. A subschema subentry contains the schema definitions for the object classes and attribute type definitions used by entries in a particular part of the directory tree. If a particular entry does not have a subschema subentry, then the subschema subentry of the root DSE, which is named by the empty DN, is used. For more information about the schema, refer to RFCs 2252 and 2256.

Adding Entries

Adding entries to the server is the first thing you should do. To add entries to slapd, you use ldapadd, which reads the content of an ldif file, checks the validity of its entries, and adds the entries to the server if the entries are correct.

To add entries to the LDAP server, you need to pass the domain name and the password for the root user. For example, with the following command you pass the domain name (sendal.jepit.edu.au) and the password (secret) and the example.ldif containing the entries to be added.

ldapadd -x -D "cn=Manager ,dc=sendal,dc=jepit,dc=edu,dc=au" -w secret -f example.ldif

The argument list of ldapadd can be displayed by typing ldapadd with no arguments.

LDAP Data Interchange Format (LDIF)

As mentioned above, the LDIF is used to represent LDAP entries in text form. The basic syntax of an LDIF entry is

. 
[<id>]
dn: <distinguished name>
<attrtype>: <attrvalue>
<attrtype>: <attrvalue>
...

where <id> is the optional entry ID (a positive decimal number). Normally, you would not supply the <id>, allowing the database creation tools to do that for you. A line may be continued by starting the next line with a single space or tab character, as in

dn: cn=Frank Dominic, o=University of Michigan, c=US 

Multiple attribute values are specified on separate lines.

cn: Frank Dominic 
cn: Frank B Dominic

If an <attrvalue> contains a non-printing character, or begins with a space or a colon (:), the <attrtype> is followed by a double colon and the value is encoded in base 64 notation. e.g., the value " begins with a space" would be encoded like this:

cn:: IGJlZ2lucyB3aXRoIGEgc3BhY2U= 

Blank lines separate multiple entries within the same LDIF file.

Here is an example of an LDIF file containing three entries.

dn: cn=Barbara J Jensen, o=University of Michigan, c=US
cn: Barbara J Jensen
cn: Babs Jensen
objectclass: person
sn: Jensen 
dn: cn=Bjorn J Jensen, o=University of Michigan, c=US
cn: Bjorn J Jensen
cn: Bjorn Jensen
objectclass: person
sn: Jensen 
dn: cn=Jennifer J Jensen, o=University of Michigan, c=US
cn: Jennifer J Jensen
cn: Jennifer Jensen
objectclass: person
sn: Jensen
jpegPhoto:: /9j/4AAQSkZJRgABAAAAAQABAAD/2wBDABALD 
A4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQ 
ERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVG ... 

Notice that the jpegPhoto in Jennifer Jensen's entry is encoded in base 64.

Java Naming and Directory Interface (JNDI)

The JNDI is API for writing programs to access naming and directory services.

The JNDI is grouped into five packages.

For the project in this article you only need the javax.naming and javax.naming.directory packages.

JNDI is included in version 1.3 of Java 2 SDK. If you are using this version, you are in luck. For users of JDK 1.1 and Java 2 SDK version 1.2, the JNDI can be downloaded and installed separately. In the Java 2 SDK, version 1.3, you can find service providers for the following services:

If you are using an older version of Java, you must first download the JNDI as a Standard Extension on the JDK 1.1 and Java 2 SDK, version 1.2.

You must also download one or more service providers. These service providers act like JDBC drivers for database access.

Accessing A Naming Service

When accessing a naming service, you first need a service provider. The first thing to do is to get the initial context, which is the starting position into the namespace. You acquire the initial context before you do any other operation. This is because all operations on naming and directory services are performed relative to some context. If you specify that your initial context when accessing a filesystem is the /usr/local directory when you call the list() method, then it's the contents of the /usr/local directory that will be returned. You can think of the initial context as the application default directory.

To obtain the initial context, you call the InitialContext() constructor, passing all the necessary environment information in a Hashtable object:

Hashtable env = new Hashtable();

Into the Hashtable, you then put the service provider. For example, if you are using the filesystem service provider from Sun, this is the line of code you need.

env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory");

The filesystem service provider can be downloaded here.

If you are using a different service provider, replace put()'s second argument.

Another important environment property that you need to get the initial context is the PROVIDER_URL. This property is assigned the location of the initial context. This could be a URL on the Internet or it could just be a directory in a file system. For instance, if you decide that your initial context when accessing a Unix filesystem is the /usr/local directory, then you need the following line of code.

env.put(Context.PROVIDER_URL, "file:/usr/local");

Or, on a Windows system, if you want the C:\data directory to be the initial context, your code would look like the following.

env.put(Context.PROVIDER_URL, "file:C:\\data");

And, optionally, you can also put the user credentials such as the username and password.

env.put(Context.SECURITY_PRINCIPAL, "james"); 
env.put(Context.SECURITY_CREDENTIALS, "secret");

Having the environment information ready, you can now create the initial context.

Context ctx = new InitialContext(env);

If the object is created successfully, you can use the resulting Context object to access the naming service. The lookup method of the Context interface can be used to retrieve an object by passing its name.

Object obj = ctx.lookup("info.txt");

For example, the following code prepares an environment Hashtable object, creates an initial context, and retrieves the info.txt file.

import java.util.Hashtable;
import javax.naming.*;
import java.io.File;

public class Naming {
  public static void main(String[] args) {

    Hashtable env = new Hashtable();
    env.put(Context.INITIAL_CONTEXT_FACTORY,
      "com.sun.jndi.fscontext.RefFSContextFactory");
    env.put(Context.PROVIDER_URL,  
      "file:C:\\123data\\MyArticles\\WhitePagesWithLDAP");
    env.put(Context.SECURITY_PRINCIPAL, "james");
    env.put(Context.SECURITY_CREDENTIALS, "secret");

    try {
      Context ctx = new InitialContext(env);
      File f = (File)ctx.lookup("info.txt");
    }
    catch (NamingException e) {
      System.out.println(e.toString());
    }
  }
}

The Object object from the lookup method is cast to a File object. If the object is a Printer, you can do something similar:

  Printer printer = (Printer) ctx.lookup("BigMomma");
  printer.print(report);

Some of the code is in a try-catch wrapper because many methods in the JNDI packages can throw a NamingException.

Other useful methods of the Context interface include the following.

Every naming method in the Context interface has two overloads: one that accepts a Name argument and one that accepts a java.lang.String name. Name is an interface that represents a generic name; an ordered sequence of zero or more components.

The overloads that accept Name are useful for applications that need to manipulate names, that is, composing them, comparing components, and so on.

A java.lang.String name argument represents a composite name. The overloads that accept java.lang.String names are likely to be more useful for simple applications, such as those that read in a name and look up the corresponding object.

Accessing A Directory Service

When you access a directory service, there are several initial steps to perform. The first is to prepare an environment Hashtable object to get the initial context.

Hashtable env = new Hashtable();

One of the environment properties you need to set is the INITIAL_CONTEXT_FACTORY. For example, if you are accessing an LDAP service, you can use the service provider from Sun. The code would then look like the following.

env.put(Context.INITIAL_CONTEXT_FACTORY,
  "com.sun.jndi.ldap.LdapCtxFactory");

If you are using a service provider from another vendor, just replace the second argument to put(). Next, you supply the location of the service. For example, the following specifies a location of an LDAP server at ldap://sendal.jepit.edu.au:389 (389 is the default port for the LDAP service).

env.put(Context.PROVIDER_URL, 
  "ldap://sendal.jepit.usyd.edu.au:389");

You can then acquire an initial context by passing the environment Hashtable. However, unlike accessing a naming system, you use the DirContext interface instead of the Context interface.

DirContext ctx = new InitialDirContext(env);

Having a DirContext object, you can access the directory service using the methods of the DirContext interface; the important methods of which include getAttributes, getSchema and search.

Developing a White Pages Service

A white pages service for locating a person in an LDAP server. As mentioned previously, I use the LDAP server from OpenLDAP. In order to keep the project simple, I use the person object defined in the core.schema file.

For convenience, the person object in the core.schema file is re-presented here.

objectclass ( 2.5.6.6 NAME 'person' SUP top STRUCTURAL
  MUST ( sn $ cn )
  MAY ( userPassword $ telephoneNumber $ seeAlso $ description )
)

The person object has two mandatory attributes: sn and cn, and four optional attributes:

Adding Some Entries

To test the code in this project, you need to populate the directory:

ldapadd -x -D "cn=Manager ,dc=sendal,dc=jepit,dc=edu,dc=au" -w 
secret -f example.ldif

This reads the example.ldif file and insert its content as entries to the server. The example.ldif file contains the following.

dn: cn=Bulbul, dc=sedal,dc=usyd,dc=edu,dc=au
objectclass: person
cn: Bulbul Kurniawan
sn: Kurniawan
userPassword: secret
telephoneNumber: +61 98371313

dn: cn=boni, dc=sedal,dc=usyd,dc=edu,dc=au
objectclass: person
cn: Boni Milliken
sn: Milliken
userPassword: dog
telephoneNumber: +61 9555 1212

dn: cn=boy, dc=sedal,dc=usyd,dc=edu,dc=au
objectclass: person
cn: Boy Milliken
sn: Milliken
userPassword: taboo
telephoneNumber: +61 98989898

Make sure that you have installed the correct service provider and your CLASSPATH variable contains the path to the JNDI packages.

The Code

The code for the white pages service is given in Listing 1. The Java code allows you to access the LDAP server and search a person or persons by passing a surname. The code starts by preparing a environment Hashtable object and setting the necessary properties for the environment.

Hashtable env = new Hashtable();

env.put(Context.INITIAL_CONTEXT_FACTORY,
  "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, 
  "ldap://sendal.jepit.edu.au:389");

And then, as explained above, you need a DirContext object as the initial context, which is done by calling the InitialDirContext constructor, passing the environment Hashtable.

DirContext ctx = new InitialDirContext(env);

Once you have a DirContext object, you can use it to access the LDAP service. To start searching, use the search method by passing a SearchControls object.

SearchControls constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration persons =
  ctx.search("dc=sendal,dc=jepit,dc=edu,dc=au",
        "(objectclass=person)", constraints);

Then, display the search result, i.e., the attributes of all the person objects that match the search criteria.

For each person object found, you use the getAttributes method to retrieve the object's attributes. This method returns the Attributes object. You can then use the get method of the Attributes object to obtain the value of an attribute by passing the attribute name.

attributes.get( attributeName );

The part of the code that displays the attribute names of the person objects found is given below.

System.out.println("Distinguished Name \t| " +
  "Common Name \t| Surname \t| Phone");

while (persons != null && persons.hasMore()) {
  SearchResult sr = (SearchResult) persons.next();  
  System.out.print( sr.getName() + "\t| ");  // distinguised name
  Attributes attrs = sr.getAttributes();
  attrs.put(new BasicAttribute("sn", searchedSurname));
  // attrs.put(new BasicAttribute("cn", "boy"));
  System.out.print(attrs.get("cn") + "\t| "); // common name
  System.out.print(attrs.get("sn") + "\t| "); // surname
  System.out.println(attrs.get("telephoneNumber")); // phone

} // end of while

If you run the code in Listing 1, you can see the result that looks something like the following.

Distinguished Name  |  Common Name  |  Surname  |  Phone

cn=Boni Milliken  |cn: boy  |sn: Milliken  | +61 9555 1212
cn=Boy Milliken  |cn: boy  |sn: Milliken  | +61 98989898

Summary

Naming and directory services are important, providing a way to find objects based on their name or other attributes. A directory service is an extension of a naming service in which object has various attributes. So you can you look up an object by its name, and you can get the object's attributes or search for the object based on its attributes. Using a directory service such as an LDAP server, you can create many applications, including the white pages service described above.

Budi Kurniawan is a senior J2EE architect and author.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.