CHAPTER 2

image

Java Support for LDAP

In this chapter, we will discuss:

  • » Basics of JNDI
  • » LDAP enabling applications using JNDI
  • » JNDI drawbacks

The Java Naming and Directory Interface (JNDI), as the name suggests provides a standardized programming interface for accessing naming and directory services. It is a generic API and can be used to access a variety of systems including file systems, EJB, CORBA, and directory services such as Network Information Service and LDAP. JNDI’s abstractions to directory services can be viewed as similar to JDBC’s abstractions to relational databases.

The JNDI architecture consists of an Application Programming Interface or API and a Service Provider Interface or SPI. Developers program their Java applications using the JNDI API to access directory/naming services. Vendors implement the SPI with details that deal with actual communication to their particular service/product. Such implementations are referred to as service providers. Figure 2-1 shows the JNDI architecture along with a few naming and directory service providers. This pluggable architecture provides a consistent programming model and prevents the need to learn a separate API for each product.

9781430263975_Fig02-01.jpg

Figure 2-1. JNDI Architecture

The JNDI has been part of the standard JDK distribution since Java version 1.3. The API itself is spread across the following four packages:

  • » javax.naming package contains classes and interfaces for looking up and accessing objects in a naming service.
  • » javax.naming.directory package contains classes and interfaces that extend the core javax.naming package. These classes can be used to access directory services and perform advanced operations such as filtered searching.
  • » javax.naming.event package has functionality for event notification when accessing naming and directory services.
  • » javax.naming.ldap package contains classes and interfaces that support the LDAP Version 3 controls and operations. We will be looking at controls and operations in the later chapters.

The javax.naming.spi package contains the SPI interfaces and classes. Like I mentioned above, service providers implement SPI and we will not be covering these classes in this book.

LDAP Using JNDI

While JNDI allows access to a directory service, it is important to remember that JNDI itself is not a directory or a naming service. Thus, in order to access LDAP using JNDI, we need a running LDAP directory server. If you don’t have a test LDAP server available, please refer to steps in Chapter 3 for installing a local LDAP server.

Accessing LDAP using JNDI usually involves the following three steps:

  • » Connect to LDAP
  • » Perform LDAP operations
  • » Close the resources

Connecting to LDAP

All the naming and directory operations using JNDI are performed relative to a context. So the first step in using JNDI is to create a context that acts as a starting point on the LDAP server. Such a context is referred to as an initial context. Once an initial context is established, it can be used to look up other contexts or add new objects.

The Context interface and InitialContext class in the javax.naming package can be used for creating an initial naming context. Since we are dealing with a directory here, we will be using a more specific DirContext interface and its implementation InitialDirContext. Both DirContext and InitialDirContext are available inside the javax.naming.directory package. The directory context instances can be configured with a set of properties that provide information about the LDAP server. The following code in Listing 2-1 creates a context to an LDAP server running locally on port 11389.

Listing 2-1.

Properties environment =  new  Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");

environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:11389");
DirContext context  =  new  InitialDirContext(environment);

In the above code, we have used the INITIAL_CONTEXT_FACTORY constant to specify the service provider class that needs to be used. Here we are using the sun provider com.sun.jndi.ldap.LdapCtxFactory, which is part of the standard JDK distribution. The PROVIDER_URL is used to specify the fully qualified URL of the LDAP server. The URL includes the protocol (ldap for non secure or ldaps for secure connections), the LDAP server host name and the port.

Once a connection to the LDAP server is established it is possible for the application to identify itself by providing authentication information. Contexts like the one created in Listing 2-1, where authentication information is not provided are referred to as anonymous contexts. LDAP servers usually have ACLs (access list controls) in place that restrict operations and information to certain accounts. So it is very common in enterprise applications to create and use authenticated contexts. Listing 2-2 provides an example of creating an authenticated context. Notice that we have used three additional properties to provide the binding credentials. The SECURITY_AUTHENTICATION property is set to simple indicating that we will be using plain text user name and password for authentication.

Listing 2-2.

Properties environment =  new  Properties();
environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");

environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:11389");
environment.setProperty(DirContext.SECURITY_AUTHENTICATION, "simple");
environment.setProperty(DirContext.SECURITY_PRINCIPAL, "uid=admin,ou=system");
environment.setProperty(DirContext.SECURITY_CREDENTIALS, "secret");
DirContext context  =  new  InitialDirContext(environment);

Any problems that might occur during the creation of the context will be reported as instances of javax.naming.NamingException. NamingException is the super class of all the exceptions thrown by the JNDI API. This is a checked exception and must be handled properly for the code to compile. Table 2-1 provides a list of common exceptions that we are likely to encounter during JNDI development.

Table 2-1. Common LDAP Exceptions

Exception Description
AttributeInUseException Thrown when an operation tries to add an existing attribute.
AttributeModification Exception Thrown when an operation tries to add/remove/update an attribute and violates the attribute’s schema or state. For example, adding two values to a single valued attribute would result in this exception.
CommunicationException Thrown when an application fails to communicate (network problems for example) with the LDAP server.
InvalidAttributesException Thrown when an operation tries to add or modify an attribute set that has been specified incompletely or incorrectly. For example, attempting to add a new entry without specifying all the required attributes would result in this exception.
LimitExceededException Thrown when a search operation abruptly terminates as a user or system specified result limit is reached.
InvalidSearchFilterException Thrown when a search operation is given a malformed search filter.
NameAlreadyBoundException Thrown to indicate that an entry cannot be added as the associated name is already bound to a different object.
PartialResultException Thrown to indicate that only a portion of the expected results is returned and the operation cannot be completed.

LDAP Operations

Once we obtain an initial context, we can perform a variety of operations on LDAP using the context. These operations can involve looking up another context, creating a new context and updating or removing an existing context. Here is an example of looking up another context with DN   uid=emp1,ou=employees,dc=inflinx,d c=com.

DirContext anotherContext  =  context.lookup("uid=emp1,ou=employees,
dc=inflinx,dc=com");

We will take a closer look at each of these operations in the coming section.

Closing Resources

After all the desired LDAP operations are complete, it is important to properly close the context and any other associated resources. Closing a JNDI resource simply involves calling the close method on it. Listing 2-3 shows the code associated with closing a DirContext. From the code you can see that the close method also throws a NamingException that needs to be properly handled.

Listing 2-3.

try {
   context.close();
}
catch  (NamingException e)  {
   e.printstacktrace();
}

Creating a New Entry

Consider the case where a new employee starts with our hypothetical Library and we are asked to add his information to LDAP. As we have seen earlier, before an entry can be added to LDAP, it is necessary to obtain an InitialDirContext. Listing 2-4 defines a reusable method for doing this.

Listing 2-4.

private  DirContext getContext() throws NamingException{
   Properties environment =  new  Properties();  
   environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.
   LdapCtxFactory");
   environment.setProperty(DirContext.PROVIDER_URL, "ldap://localhost:10389");
   environment.setProperty(DirContext.SECURITY_PRINCIPAL, "uid=admin,ou=system");
   environment.setProperty(DirContext.SECURITY_CREDENTIALS, "secret"); 
   DirContext context  =  new  InitialDirContext(environment);
   return context;
}

Once we have the initial context, adding the new employee information is a straightforward operation as shown in Listing 2-5.

Listing 2-5.

public  void addEmploye(Employee employee)  {
   DirContext context  =  null;
   try  {
      context =  getContext();
      // Populate the attributes
      Attributes attributes  =  new  BasicAttributes();
      attributes.put(new  BasicAttribute("objectClass", "inetOrgPerson"));
      attributes.put(new BasicAttribute("uid", employee.getUid()));
      attributes.put(new BasicAttribute("givenName", employee.getFirstName()));
      attributes.put(new BasicAttribute("surname", employee.getLastName()));
      attributes.put(new BasicAttribute("commonName", employee.getCommonName()));
      attributes.put(new BasicAttribute("departmentNumber",
      employee.getDepartmentNumber()));

      attributes.put(new  BasicAttribute("mail", employee.getEmail()));
      attributes.put(new BasicAttribute("employeeNumber",
      employee.getEmployeeNumber()));

      
      Attribute  phoneAttribute = new  BasicAttribute("telephoneNumber");
      for(String phone : employee.getPhone())  {
         phoneAttribute.add(phone);
      }
      attributes.put(phoneAttribute);
 
       // Get the fully  qualified DN
     String dn   =  "uid="+employee.getUid() +  "," +  BASE_PATH;
 
      // Add  the entry
      context.createSubcontext("dn", attributes);
   }
   catch(NamingException e)  {
      // Handle the exception properly
        e.printStackTrace();

   }
   finally  {
      closeContext(context);
   }
}

As you can see, the first step in the process is to create a set of attributes that needs be added to the entry. JNDI provides the javax.naming.directory.Attributes interface and its implementation javax.naming.directory.BasicAttributes to abstract an attribute collection. We then add the employee’s attributes one at a time to the collection using JNDI’s javax.naming.directory.BasicAttribute class. Notice that we have taken two approaches in creating the BasicAttribute class. In the first approach we have added the single valued attributes by passing the attribute name and value to BasicAttribute’s constructor. To handle the multi-valued attribute telephone, we first created the BasicAttribute instance by just passing in the name. Then we individually added the telephone values to the attribute. Once all the attributes are added, we invoked the createSubcontext method on the initial context to add the entry. The createSubcontext method requires the fully qualified DN of the entry to be added.

Notice that we have delegated the closing of the context to a separate method closeContext. Listing 2-6 shows its implementation.

Listing 2-6.

private  void closeContext(DirContext context)  {
   try  {
      if(null != context)  {
      context.close();
      }
   }
   catch(NamingException e)  {
      // Ignore the  exception
   }
}

Updating an Entry

Modifying an existing LDAP entry can involve any of the following operations:

  • » Add a new attribute and value(s) or add a new value to an existing multi valued attribute.
  • » Replace an existing attribute value(s).
  • » Remove an attribute and its value(s).

In order to allow modification of the entries, JNDI provides an aptly named javax.naming.directory.ModificationItem class.

A ModificationItem consists of the type of modification to be made and the attribute under modification. The code below creates a modification item for adding a new telephone number.

Attribute telephoneAttribute =  new  BasicAttribute("telephone", "80181001000");
ModificationItem modificationItem  =  new  ModificationItem(DirContext.
ADD_ATTRIBUTE,  telephoneAttribute);

Notice that in the above code, we have used the constant ADD_ATTRIBUTE to indicate that we want an add operation. Table 2-2 provides the supported modification types along with their descriptions.

Table 2-2. LDAP Modification Types

Modification Type Description
ADD_ATTRIBUTE Adds the attribute with the supplied value or values to the entry. If the attribute does not exist then it will be created. If the attribute already exists and the attribute is a multi-valued then this operation simply adds the specified value(s) to the existing list. However, this operation on an existing single valued attributes will result in the AttributeInUseException.
REPLACE_ATTRIBUTE Replaces existing attribute values of an entry with the supplied values.  If the attribute does not exist then it will be created. If the attribute already exists, then all of its values will be replaced.
REMOVE_ATTRIBUTE Removes the specified value from the existing attribute. If no value is specified then the attribute in its entirety will be removed. If the specified value does not exist in the attribute, the operation will throw a NamingException. If the value to be removed is the only value of the attribute, then the attribute is also removed.

The code for updating an entry is provided in Listing 2-7. The modifyAttributes method takes the fully qualified DN of the entry to be modified and an array of modification items.

Listing 2-7.

public void update(String  dn, ModificationItem[] items)  {
   DirContext context  =  null;
   try  {
      context =  getContext();
      context.modifyAttributes(dn, items);
   }
   catch  (NamingException e)  {
      e.printStackTrace();
   }
   finally  {
     closeContext(context);
   }
}

Removing an Entry

Removing an entry using JNDI is again a straightforward process and is shown in Listing 2-8. The destroySubcontext method takes the fully qualified DN of the entry that needs to be deleted.

Listing 2-8.

public  void remove(String dn) {
   DirContext context  =  null;
   try  {
      context =  getContext();
      context.destroySubcontext(dn);
   }
   catch(NamingException e)  {
      e.printStackTrace();
   finally  {
      closeContext(context);
   }
}

Many LDAP servers don’t allow an entry to be deleted if it has child entries. In those servers, deleting a non-leaf entry would require traversing the sub tree and deleting all the child entries. Then the non-leaf entry can be deleted. Listing 2-9 shows the code involved in deleting a sub tree.

Listing 2-9.

public  void removeSubTree(DirContext ctx, String root)
throws  NamingException {
NamingEnumeration enumeration  =  null;
try  {
     enumeration =  ctx.listBindings(root);
     while (enumeration.hasMore())  {
         Binding childEntry =(Binding)enumeration.next();
         LdapName childName  =  new  LdapName(root);
        childName.add(childEntry.getName());
 
         try  {
              ctx.destroySubcontext(childName);
         }
         catch  (ContextNotEmptyException e)  {
        removeSubTree(ctx, childName.toString());
        ctx.destroySubcontext(childName);
         }
     }
}
catch  (NamingException e)  {
     e.printStackTrace();
}
finally  {
         try  {
               enumeration.close();
         }
         catch (Exception e)  {
               e.printStackTrace();
         }
     }
}

image Note   The OpenDJ LDAP server supports a special sub tree delete control that when attached to a delete request can cause the server to delete the non-leaf entry and all its child entries. We will look at the using LDAP controls in Chapter 7.

Searching Entries

Searching for information is usually the most common operation performed against an LDAP server. In order to perform a search, we need to provide information such as the scope of the search, what we are looking for, and what attributes need to be returned. In JNDI, this search metadata is provided using the SearchControls class. Listing 2-10 provides an example of a search control with subtree scope and returns the givenName and telephoneNumber attributes. The subtree scope indicates that the search should start from the given base entry and should search all its subtree entries. We will look at different scopes available in detail in Chapter 6.

Listing 2-10.

SearchControls searchControls  =  new  SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); 
searchControls.setReturningAttributes(new String[]{"givenName",

"telephoneNumber"});

Once we have the search controls defined, the next step is to invoke one of the many search methods in the DirContext instance. Listing 2-11 provides the code that searches all the employees and prints their first name and telephone number.

Listing 2-11.

public void search() {
   DirContext context  =  null;
   NamingEnumeration<SearchResult> searchResults =  null;
   try
   {
      context =  getContext();
      // Setup Search meta data
      SearchControls searchControls  =  new  SearchControls();
       searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
       searchControls.setReturningAttributes(new
String[]      
       {"givenName",  "telephoneNumber"});

      searchResults =  context.search("dc=inflinx,dc=com",      
      "(objectClass=inetOrgPerson)", searchControls);

        while (searchResults.hasMore())  {
         SearchResult result =  searchResults.next();
         Attributes attributes  =  result.getAttributes();
         String firstName =  (String)attributes.get("givenName").get();
         // Read the  multi-valued  attribute
         Attribute  phoneAttribute =  attributes. get("telephoneNumber");
         String[] phone =  new  String[phoneAttribute.size()];
         NamingEnumeration phoneValues  =  phoneAttribute.getAll();
         for(int i =  0;  phoneValues.hasMore(); i++) {
         phone[i] =  (String)phoneValues.next();
         }
      System.out.println(firstName +    ">   " +  Arrays.toString(phone));
      }
     }
   catch(NamingException e)  {
      e.printStackTrace();
   }
   finally  {
       try  {
          if (null  != searchResults) {
          searchResults.close();
          }
       closeContext(context);
       }  catch  (NamingException e)  {
       // Ignore this
       }
   }
}

Here we used the search method with three parameters: a base that determines the starting point of the search, a filter that narrows down the results, and a search control. The search method returns an enumeration of SearchResults. Each search result holds the LDAP entry’s attributes. Hence we loop through the search results and read the attribute values. Notice that for multi valued attributes we obtain another enumeration instance and read its values one at a time. In the final part of the code, we close the result enumeration and the context resources.

JNDI Drawbacks

Though JNDI provides a nice abstraction for accessing directory services, it does suffer from several of the following drawbacks:

  • » Explicit Resource Management
  • The developer is responsible for closing all the resources. This is very error prone and can result in memory leaks.
  • » Plumbing Code
  • The methods we have seen above have lot of plumbing code that can be easily abstracted and reused. This plumbing code makes testing harder and the developer has to learn the nitty-gritty of the API.
  • » Checked Exceptions
  • The usage for checked exceptions especially in irrecoverable situations is questionable. Having to explicitly handle NamingException in those scenarios usually results in empty try catch blocks.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset