LDAP and JNDI: Together forever

Learn how LDAP and JNDI combine to form a powerful directory and Java object storage facility

The Lightweight Directory Access Protocol (LDAP), which traces its roots to the X.500 protocol, was developed in the early 1990s as a standard directories protocol. LDAP defines how clients should access data on the server, not how that data is stored on the server. This allows LDAP to become a frontend to any type of data store.

(Note: To download this article's complete source code, see the Resources section below.)

LDAP's basic structure is based on a simple information tree metaphor called a directory information tree (DIT). Each leaf in the tree is an entry; the first or top-level entry is the root entry. An entry includes a distinguished name (DN) and any number of attribute/value pairs. The DN, which is the name of an entry, must be unique. It shows the relationship between the entry and the rest of the DIT in a manner similar to the way in which a file's full path name shows its relationship with the rest of the files in a filesystem. While a path to a file reads left to right, a DN, in contrast, reads from right to left. Here is an example of an DN:

uid=styagi,ou=people,o=myserver.com

The leftmost part of the DN, called a relative distinguished name (RDN), is made up of an attribute/value pair. In the above example, this pair would be uid=styagi. LDAP attributes often use mnemonics, some examples of which are listed in Table 1.

oOrganization
ouOrganizational unit
cnCommon name
snSurname
givennameFirst name
uidUserid
dnDistinguished name
mailEmail address
Table 1. Some common LDAP attributes

Information about attributes, attribute matching rules, and relationships between objectclasses are defined in the server's schema. Any attribute can have one or more values, depending on how it is defined the schema. A user, for example, can have more than one email address. There is also a special attribute called an objectclass that specifies the required and allowed attributes for a particular entry. Like objects in Java, objectclasses in LDAP can be extended to retain existing attributes and add new ones.

A naming service associates names with objects and finds objects based on their given names. (The RMI registry is a good example of a naming service.) Many naming services are extended with a directory service. While a naming service allows a lookup of an object based on its name, a directory service also allows such objects to have attributes. As a result, with a directory service we can look up an object's attributes or search for objects based on their attributes.

So where does JNDI fit into this LDAP jargon? JNDI does for LDAP what JDBC does for Oracle -- it provides a standard API for interacting with naming and directory services using a service provider interface (SPI), which is analogous to an JDBC driver. LDAP is a standard way to provide access to directory information. JNDI gives Java applications and objects a powerful and transparent interface to access directory services like LDAP. Table 2 below outlines common LDAP operations and their JNDI equivalents. (For a detailed look at the JNDI specification, see Resources.)

OperationWhat it doesJNDI equivalent
SearchSearch directory for matching directory entriesDirContext.search()
CompareCompare directory entry to a set of attributesDirContext.search()
AddAdd a new directory entryDirContext.bind(), DirContext.createSubcontext()
ModifyModify a particular directory entryDirContext.modifyAttributes()
DeleteDelete a particular directory entryContext.unbind(), Context.destroySubcontext()
RenameRename or modify the DNContext.rename()
BindStart a session with an LDAP servernew InitialDirContext()
UnbindEnd a session with an LDAP serverContext.close()
AbandonAbandon an operation previously sent to the serverContext.close(), NamingEnumneration.close()
ExtendedExtended operations commandLdapContext.extendedOperation()
Table 2. Common LDAP operations and JNDI equivalents

Manipulate objects in the LDAP server

Let's cut to the chase and see how to manipulate objects in the LDAP server. The standard LDAP operations include:

  • Connect to the server
  • Bind to the server (think of this as authentication)
  • Add new entries in the LDAP server
  • Modify an entry
  • Delete an entry
  • Search the server for an entry

We'll examine each of these steps in the sections below, with examples.

Before executing the examples, you will need to install the LDAP server, the JNDI classes, and (unless you want to disable schema checking) the Java schema. You can find install information in the JNDI zip file's schema directory. Our examples use Netscape Directory Server 4.1 and JDK 2. (To install these packages, see Resources.)

Connect to the server

To connect to the server, you must obtain a reference to an object that implements the DirContext interface. In most applications, this is done by using an InitialDirContext object that takes a Hashtable as an argument. The Hashtable contains various entries, such as the hostname, port, and JNDI service provider classes to use:

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389");
DirContext ctx = new InitialDirContext(env);

Bind to the Server

Once connected, the client may need to authenticate itself; this process is also known as binding to the server. (Be aware that the word binding can also refer to the act of adding something to the directory.)

In LDAP version 2, all clients had to authenticate while connecting, but version 3 defaults to anonymous and, if the default values are used, the connections are anonymous as well. LDAP servers maintain rights using access control lists (ACLs) that determine what particular access is available to an entry by an application. LDAP supports three different security types:

  • Simple: Authenticates fast using plain text usernames and passwords.
  • SSL: Authenticates with SSL encryption over the network.
  • SASL: Uses MD5/Kerberos mechanisms. SASL is a simple authentication and security layer-based scheme

The client authenticates itself to the server by specifying values for different environment variables in the Context interface, as seen below:

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389");
env.put(Context.SECURITY_AUTHENTICATION,"simple");
env.put(Context.SECURITY_PRINCIPAL,"cn=Directory Manager"); // specify the username
env.put(Context.SECURITY_CREDENTIALS,"password");           // specify the password
DirContext ctx = new InitialDirContext(env);

Add new entries in the LDAP server: The options

The LDAP directory server can act as a repository for Java objects. JNDI provides an object-oriented view of this directory, which means that Java objects can be added to and retrieved from the directory without the client needing to manage data representation issues.

Objects can be stored in three ways:

  • Store the Java objects themselves
  • Store a reference to the object
  • Store information as attributes

Let's take a look at each of these in more detail.

Store the Java objects themselves

If a class implements the java.io.Serializable interface, it can be serialized and deserialized from storage media. If we need a simple name-object binding (as in the RMI registry), then the Context.bind() method can store the object. But if we need the more powerful technique of associating attributes with the stored object, we'd employ the DirConext.bind() method instead. Whichever method we use, the object's state is serialized and stored in the server:

MyObject obj = new MyObject(); 
ctx.bind("cn=anobject", obj);

Once stored, we can retrieve the object by looking up its name in the directory:

MyObject obj = (MyObject)ctx.lookup("cn=anobject");

When an application serializes an object by writing it to an object stream, it records information that identifies the object's class in the serialized stream. However, the class's definition, which is contained in the classfile, is not itself recorded. The system that deserializes the object is responsible for determining how to locate and load the necessary class files.

Alternatively, the application can record the codebase with the serialized object in the directory, either when the binding occurs or by subsequently adding an attribute using DirContext.modifyAttributes(). (We'll examine this second technique later in this article.) Any attribute can record the codebase as long as the application reading back the object is aware of the attribute name. As another option, we can employ the attribute "javaCodebase" specified in the LDAP schema for storing Java objects if schema checking is enabled on the server.

The above example can be modified to supply a codebase attribute containing the location of the MyObject class definition:

// Create object to be bound
MyObject obj = new MyObject();
// Perform bind and specify codebase
BasicAttribytes battr = new BasicAttributes("javaCodebase","http://myserver.com/classes")
ctx.bind("cn=anobject", obj, battr);

AddSeriaize.java demonstrates how to add 10 instances of java.util.Vector, which implements java.io.Serializable; the result can be seen in Figure 1.

Figure 1. Screen shots of the server console after running the ADDSerialize example

Store a reference to the object

Instead of storing the entire serialized state of an object, you can store a reference to that object instead. JNDI's javax.naming.Reference class records address information about objects not directly bound to the directory service. The reference to an object contains the following information:

  • The class name of the referenced object
  • A vector of javax.naming.RefAddr objects that represents the addresses
  • The name and location of the object factory to use during reconstruction

The javax.naming.RefAddr abstract class, seen in Figure 2 below, contains information indicating the ways in which you can contact the object (e.g., via a location in memory, a lookup on another machine, etc.) or recreate it with the same state. The class defines an association between content and type. The content (an object) stores information required to rebuild the object and the type (a string) identifies the purpose of the content.

Figure 2. The relation between a Reference, RefAddr, Type, and Content

RefAddr also overrides the java.lang.Object.equals(Object obj) and java.lang.Object.hashcode() methods to ensure that two references are equal if the content and type are equal. RefAddr has two concrete subclasses: javax.naming.StringRefAddr, which stores strings, and javax.naming.BinaryRefAddr, which stores an array of bytes. For example, a string reference address could be an IP, URL, hostname, or something similar.

Consider the example of a referenceable Apartment class. The ADDReference.java example creates a few instances of Apartment and stores them in the server. What happens internally? Since the object is referenceable, a reference is stored and not the serialized object. When the example tries to look up an apartment belonging to styagi, it gets a reference from the server that contains information about the factory class needed, the apartment size, and its location. It then requests that the factory create an Apartment object with the right size and location and return that object. All this happens transparently to the user.

Context ctx = new InitialContext(env);

ctx.bind("apartment=styagi,ou=JavaObjects,o=myserver.com",new Apartment("studio","Mill Complex"));

ctx.bind("apartment=mojoe,ou=JavaObjects,o=myserver.com", new Apartment("2 room","Farm House Apartments"));

ctx.bind("apartment=janedoe,ou=JavaObjects,o=myserver.com",new Apartment("1 room","Pond Side"));

ctx.bind("apartment=rogerp,ou=JavaObjects,o=myserver.com", new Apartment("3 room","Mill Complex"));

ctx.bind("apartment=jamesm,ou=JavaObjects,o=myserver.com", new Apartment("studio","Fox Hill Apartments"));

ctx.bind("apartment=paulh,ou=JavaObjects,o=myserver.com", new Apartment("duplex","Woodbridge"));

ctx.bind("apartment=vkevink,ou=JavaObjects,o=myserver.com",new Apartment("1 room","Woodgate Apartments"));

Apartment apt = (Apartment)ctx.lookup("apartment=styagi,ou= JavaObjects,o=myserver.com");

System.out.println(apt);

The Apartment class would look something like:

public class Apartment implements Referenceable{
private String size;
public String location;
public Apartment(String size,String location){
 this.size=size;
 this.location=location;
}
public Reference getReference() throws NamingException{
 String classname = Apartment.class.getName();
 StringRefAddr classref =
             new StringRefAddr("Apartment details", size+ ":" +location);
 String classfactoryname=ApartmentFactory.class.getName();
 Reference ref = new Reference(classname,classref,classfactoryname,null);
 return ref;
}
public String toString(){
   return ("This apartment is "+size+ " and is located at " +location);
}
}

The factory used to recreate the Apartment objects, ApartmentFactory, is stored in the Reference, as shown above. The ApartmentFactory implements the javax.naming.spi.ObjectFactory interface, which contains the getObjectInstance() method that returns a newly constructed object based on the reference, as seen below:

1 2 3 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more