LdapTemplate: LDAP Programming in Java Made Simple

The Java Naming and Directory Interface (JNDI) is for LDAP programming what Java Database Connectivity (JDBC) is for SQL programming. There are several similarities between JDBC and JNDI/LDAP (Java LDAP). Despite being two completely different APIs with different pros and cons, they share a number of less flattering characteristics:

  • They require extensive plumbing code, even to perform the simplest of tasks.
  • All resources need to be correctly closed, no matter what happens.
  • Exception handling is difficult.

The above points often lead to massive code duplication in common usages of the APIs. As we all know, code duplication is one of the worst code smells. All in all, it boils down to this: JDBC and LDAP programming in Java are both incredibly dull and repetitive.

Spring JDBC, a part of the Spring framework, provides excellent utilities for simplifying SQL programming. We need a similar framework for Java LDAP programming.

The Traditional Way

Consider, for example, a method that should search some storage for all persons and return their names in a list. Using JDBC, we would create a connection and execute a query using a statement. We would then loop over the result set and retrieve the column we want, adding it to a list. In contrast, using Java LDAP, we would create a context and perform a search using a search filter. We would then loop over the resulting naming enumerationattribute we want, adding it to a list. and retrieve the

The traditional way of implementing this person name search method in Java LDAP is this:

package se.jayway.dao;

public class TraditionalPersonDaoImpl implements
PersonDao {
public List getAllPersonNames() {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL,
"ldap://localhost:389/dc=jayway,dc=se");

DirContext ctx;
try {
ctx = new InitialDirContext(env);
} catch (NamingException e) {
throw new RuntimeException(e);
}

LinkedList list = new LinkedList();
NamingEnumeration results = null;
try {
SearchControls controls =
new SearchControls();
controls.setSearchScope(
SearchControls.SUBTREE_SCOPE);
results = ctx.search(
"", "(objectclass=person)", controls);

while (results.hasMore()) {
SearchResult searchResult =
(SearchResult) results.next();
Attributes attributes =
searchResult.getAttributes();
Attribute attr = attributes.get("cn");
String cn = (String) attr.get();
list.add(cn);
}
} catch (NameNotFoundException e) {
// The base context was not found.
// Just clean up and exit.
} catch (NamingException e) {
throw new RuntimeException(e);
} finally {
if (results != null) {
try {
results.close();
} catch (Exception e) {
// Never mind this.
}
}
if (ctx != null) {
try {
ctx.close();
} catch (Exception e) {
// Never mind this.
}
}
}
return list;
}
}

The method above produces a list containing the cnperson objects found in the database. It is important to always close the naming enumeration and the context. (common name) attribute of the

Introducing LdapTemplate

LdapTemplate is a framework for simpler LDAP programming in Java, built on the same principles as the JdbcTemplate in Spring JDBC. It completely eliminates the need to worry about creating and closing DirContext and looping through NamingEnumeration. It also provides a more comprehensive unchecked exception hierarchy, built on Spring's DataAccessException. As a bonus, it also contains classes for dynamically building LDAP filters and distinguished names. An LDAP filter corresponds somewhat to the WHERE clause in a SQL SELECT statement. A distinguished name (dn) can be seen as a handle or the path to a specific object in the LDAP database. If the dn is available, an object can be looked up directly, rather than having to be searched for.

This article will walk you through how to perform different tasks using LdapTemplate. We will start off by going through the basic setup. Then we will show how to perform basic searches, filtering, and distinguished name management, as well as updates. Towards the end, we will display some powerful additional features that can be very useful in some cases.

We will use the syntax of the Spring context configuration file to display how objects and classes relate to each other. The following example shows the two objects someBean and anotherBean, where someBean has a reference to anotherBean:


class="se.jayway.SomeClass">
name="someProperty" value="42" />
name="anotherProperty" ref="anotherBean" />


class="se.jayway.AnotherClass">


A reader that is not familiar with Spring and the context file format might find the above format intimidating. However, the same configuration can also be performed manually using plain Java code:

SomeClass someBean = new SomeClass();
someBean.setSomeProperty(42);
AnotherClass anotherBean = new AnotherClass("someValue");
someBean.setAnotherProperty(anotherBean);

Basic Setup

In order to use LdapTemplate, this is what you need to do.

  1. Include the following libraries in your project:

    • commons-logging-1.0.4.jar
    • commons-lang-2.1.jar
    • commons-collections-3.1.jar

    LdapTemplate also requires Spring:

    • spring-1.2.7.jar

    Alternatively, just pick the necessary Spring components:

    • spring-core-1.2.7.jar
    • spring-beans-1.2.7.jar
    • spring-context-1.2.7.jar
    • spring-dao-1.2.7.jar
  2. Create an LdapContextSource and an LdapTemplate object. You can do it in plain Java, or use the Spring context file:


    class="net.sf.ldaptemplate.support.LdapContextSource">






    class="net.sf.ldaptemplate.LdapTemplate">


  3. Add a property and a corresponding setter for the LdapTemplate in your Data Access Object (DAO) class:

    public class PersonDaoImpl implements PersonDao {
    private LdapTemplate ldapTemplate;

    public void setLdapTemplate(LdapTemplate ldapTemplate) {
    this.ldapTemplate = ldapTemplate;
    }
    }
  4. Inject the LdapTemplate into your DAO by calling the setLdapTemplate, or, if you use the Spring context file:


    ...
    class="se.jayway.dao.PersonDaoImpl">


Searches and Lookups using AttributesMapper

In this example, we will use an AttributesMapper to easily build a list of all common names of all person objects. This is exactly what we did in the earlier example using the traditional way.

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
private LdapTemplate ldapTemplate;

public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}

public List getAllPersonNames() {
return ldapTemplate.search(
"", "(objectclass=person)",
new AttributesMapper() {
public Object mapFromAttributes(Attributes attrs)
throws NamingException {
return attrs.get("cn").get();
}
});
}
}

The inline implementation of AttributesMapper just gets the desired attribute value from the Attributes and returns it. Internally, LdapTemplate iterates over all entries found, calling the given AttributesMapper for each entry, and collects the results in a list. The list is then returned by the search method.

Note that the AttributesMapper implementation could easily be modified to return a full Person object:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
private LdapTemplate ldapTemplate;
...
private static class PersonAttributesMapper
implements AttributesMapper {
public Object mapFromAttributes(Attributes attrs)
throws NamingException {
Person person = new Person();
person.setFullName((String)attrs.get("cn").get());
person.setLastName((String)attrs.get("sn").get());
person.setDescription((String)attrs.get("description").get());
return person;
}
}

public List getAllPersons() {
return ldapTemplate.search(
"", "(objectclass=person)",
new PersonAttributesMapper();
}
}

If you have the distinguished name (dn) that identifies an entry, you can retrieve the entry directly, without searching for it. This is called a lookup in Java LDAP. The following example shows how a lookup results in a Person object:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
private LdapTemplate ldapTemplate;
...
public Person findPerson(String dn) {
return (Person) ldapTemplate.lookup(dn,
new PersonAttributesMapper());
}
}

This will look up the specified dn and pass the found attributes to the supplied AttributesMapper, in this case resulting in a Person object.

Building Dynamic Filters

We can build dynamic filters to use in searches, using the classes from the net.sf.ldaptemplate.support.filter(&(objectclass=person)(sn=?)), where we want the ?lastName. This is how we do it using the filter support classes: package. Let's say that we want the following filter: to be replaced with the value of the parameter

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
public static final String BASE_DN = "dc=jayway,dc=se";
...
public List getPersonNamesByLastName(String lastName) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person"));
filter.and(new EqualsFilter("sn", lastName));
return ldapTemplate.search(
BASE_DN,
filter.encode(),
new AttributesMapper() {
public Object mapFromAttributes(Attributes attrs)
throws NamingException {
return attrs.get("cn").get();
}
});
}
}

To perform a wildcard search, it's possible to use the WhitespaceWildcardsFilter:

AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person"));
filter.and(new WhitespaceWildcardsFilter("cn", cn));

If cn is john doe, the above code would result in the following filter: (&(objectclass=person)(cn=*john*doe*)).

Building Dynamic Distinguished Names

The standard Name interface represents a generic name, which is basically an ordered sequence of components. The Name interface also provides operations on that sequence; e.g., add or remove. LdapTemplate provides an implementation of the Name interface: DistinguishedName. Using this class greatly simplifies building distinguished names, especially considering the sometimes complex rules regarding escapings and encodings. The following example illustrates how DistinguishedName can be used to dynamically construct a distinguished name:

package se.jayway.dao;

import net.sf.ldaptemplate.support.DistinguishedName;
import javax.naming.Name;

public class PersonDaoImpl implements PersonDao {
public static final String BASE_DN = "dc=jayway,dc=se";
...
protected Name buildDn(Person p) {
DistinguishedName dn = new DistinguishedName(BASE_DN);
dn.add("c", p.getCountry());
dn.add("ou", p.getCompany());
dn.add("cn", p.getFullname());
return dn;
}
}

Assuming that a Person has the following attributes:

  • country: Sweden
  • company: Some Company
  • fullname: Some Person

then the result of the code above would be the following dn: cn=Some Person, ou=Some Company, c=Sweden, dc=jayway, dc=se

In Java 5, there is an implementation of the NameLdapName. If you are in the Java 5 world, you might as well use LdapName. However, it is still possible to use DistinguishedName if you so wish. interface:

Binding Data

Inserting data in Java LDAP is called binding. In order to do that, a distinguished name that uniquely identifies the new entry is required. The following example shows how data is bound using LdapTemplate:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
...
public void create(Person p) {
Name dn = buildDn(p);
ldapTemplate.bind(dn, null, buildAttributes(p));
}

private Attributes buildAttributes(Person p) {
Attributes attrs = new BasicAttributes();
BasicAttribute ocattr = new BasicAttribute("objectclass");
ocattr.add("top");
ocattr.add("person");
attrs.put(ocattr);
attrs.put("cn", "Some Person");
attrs.put("sn", "Person");
return attrs;
}
}

The Attributes building is--while dull and verbose--sufficient for many purposes. It is, however, possible to simplify the binding operation further, which will be described in the section DirObjectFactory and the DirContextAdapter.

Unbinding Data

Removing data in Java LDAP is called unbinding. A distinguished name (dn) is required to identify the entry, just as in the binding operation. The following example shows how data is unbound using LdapTemplate:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
...
public void delete(Person p) {
Name dn = buildDn(p);
ldapTemplate.unbind(dn);
}
}

Modifying Data

In Java LDAP, data can be modified in two ways: either using rebind or modifyAttributes.

A rebind is a very crude way to modify data. It's basically an unbind followed by a bind. It looks like this:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
...
public void update(Person p) {
Name dn = buildDn(p);
ldapTemplate.rebind(dn, null, buildAttributes(p));
}
}

If only the modified attributes should be replaced, there is a method called modifyAttributes that takes an array of modifications:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
...
public void updateDescription(Person p) {
Name dn = buildDn(p);
Attribute attr =
new BasicAttribute("description", p.getDescription())
ModificationItem item =
new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr);
ldapTemplate.modifyAttributes(dn,
new ModificationItem[] {item});
}
}

Building Attributes and ModificationItem arrays is a lot of work, but as you will see in DirObjectFactory and the DirContextAdapter, the update operations can be simplified.

DirObjectFactory and the DirContextAdapter

A little-known--and probably underestimated--feature of the Java LDAP API is the ability to register a DirObjectFactory to automatically create objects from found contexts. One of the reasons why it is seldom used is that you will need an implementation of DirObjectFactory that creates instances of a meaningful implementation of DirContext. The LdapTemplate framework provides the missing pieces: a default implementation of DirContext called DirContextAdapter, and a corresponding implementation of DirObjectFactory called DefaultDirObjectFactory. Used together with DefaultDirObjectFactory, the DirContextAdapter can be a very powerful tool.

First of all, we need to register the DefaultDirObjectFactory with the ContextSource. This is done using the dirObjectFactory property:


...
class="net.sf.ldaptemplate.support.LdapContextSource" >




value="net.sf.ldaptemplate.support.DefaultDirObjectFactory">

Now, whenever a context is found in the LDAP tree, its Attributes will be used to construct a DirContextAdapter. This enables us to use a ContextMapper instead of an AttributesMapper to transform found values:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
...
private static class PersonContextMapper
implements ContextMapper {
public Object mapFromContext(Object ctx) {
DirContextAdapter context = (DirContextAdapter)ctx;
Person p = new Person();
p.setFullName(context.getStringAttribute("cn"));
p.setLastName(context.getStringAttribute("sn"));
p.setDescription(context.getStringAttribute("description"));
return p;
}
}

public Person findByPrimaryKey(
String name, String company, String country) {
Name dn = buildDn(name, company, country);
return ldapTemplate.lookup(dn, new PersonContextMapper());
}
}

The above code shows that it is possible to retrieve the attributes directly by name, without having to go through the Attributes and BasicAttribute classes.

The DirContextAdapter can also be used to hide the Attributes when binding and modifying data. This is an example of an improved implementation of the createBinding Data. DAO method. Compare it with the previous implementation in

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
...
public void create(Person p) {
Name dn = buildDn(p);
DirContextAdapter context = new DirContextAdapter(dn);

context.setAttributeValues("objectclass",
new String[] {"top", "person"});
context.setAttributeValue("cn", p.getFullname());
context.setAttributeValue("sn", p.getLastname());
context.setAttributeValue("description",
p.getDescription());

ldapTemplate.bind(dn, context, null);
}
}

Obviously, the code would be pretty much identical for a rebind. However, let's say that you don't want to remove and re-create the entry, but instead update only the attributes that have changed. The DirContextAdapter has the ability to keep track of its modified attributes. The following example takes advantage of this feature:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
...
public void update(Person p) {
Name dn = buildDn(p);
DirContextAdapter context =
(DirContextAdapter)ldapTemplate.lookup(dn);

context.setAttributeValues("objectclass",
new String[] {"top", "person"});
context.setAttributeValue("cn", p.getFullname());
context.setAttributeValue("sn", p.getLastname());
context.setAttributeValue("description",
p.getDescription());

ldapTemplate.modifyAttributes(dn,
context.getModificationItems());
}
}

The observant reader will see that we have duplicate code in the create and update methods. This code maps from a domain object to a context. It can be extracted to a separate method:

package se.jayway.dao;

public class PersonDaoImpl implements PersonDao {
private LdapTemplate ldapTemplate;

...
public void create(Person p) {
Name dn = buildDn(p);
DirContextAdapter context = new DirContextAdapter(dn);
mapToContext(p, context);
ldapTemplate.bind(dn, context, null);
}

public void update(Person p) {
Name dn = buildDn(p);
DirContextAdapter context =
(DirContextAdapter)ldapTemplate.lookup(dn);
mapToContext(person, context);
ldapTemplate.modifyAttributes(dn,
context.getModificationItems());
}

protected void mapToContext (Person p,
DirContextAdapter context) {
context.setAttributeValues("objectclass",
new String[] {"top", "person"});
context.setAttributeValue("cn", p.getFullName());
context.setAttributeValue("sn", p.getLastName());
context.setAttributeValue("description",
p.getDescription());
}
}

Conclusion

We have seen how LdapTemplate can provide substantial improvements compared to code using JNDI directly. There is no longer any need for creating contexts manually or looping through naming enumerations. Nor is there any risk of forgetting to close any of those.

By now it should be clear that the LdapTemplate framework will be of great help for any Java project that communicates with LDAP. For further examples, consult the integration unit tests in the source (src/iutest) and the Javadocs. If you have any comments, problems or questions, please let us know. Subscribe to our mailing list, or just drop us a note. drop us a note.

To illustrate the power of LdapTemplate, here is a complete Person DAO implementation for LDAP in just 82 lines:

package se.jayway.dao;

import java.util.List;

import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;

import net.sf.ldaptemplate.AttributesMapper;
import net.sf.ldaptemplate.ContextMapper;
import net.sf.ldaptemplate.LdapTemplate;
import net.sf.ldaptemplate.support.DirContextAdapter;
import net.sf.ldaptemplate.support.DistinguishedName;
import net.sf.ldaptemplate.support.filter.EqualsFilter;

public class PersonDaoImpl implements PersonDao {
private LdapTemplate ldapTemplate;

public void setLdapTemplate(LdapTemplate l) {
ldapTemplate = l;
}

public void create(Person p) {
DirContextAdapter context = new DirContextAdapter();
mapToContext(p, context);
ldapTemplate.bind(buildDn(p), context, null);
}

public void update(Person p) {
Name dn = buildDn(p);
DirContextAdapter context =
(DirContextAdapter)ldapTemplate.lookup(dn);
mapToContext(person, context);
ldapTemplate.modifyAttributes(dn,
context.getModificationItems());
}

public void delete(Person p) {
ldapTemplate.unbind(buildDn(p));
}

public Person findByPrimaryKey(
String name, String company, String country) {
Name dn = buildDn(name, company, country);
return (Person) ldapTemplate.lookup(dn,
getContextMapper());
}

public List findAll() {
EqualsFilter filter = new EqualsFilter(
"objectclass", "person");
return ldapTemplate.search(
DistinguishedName.EMPTY_PATH,
filter.encode(), getContextMapper());
}

protected ContextMapper getContextMapper() {
return new PersonContextMapper();
}

protected Name buildDn(Person p) {
return buildDn(p.getFullname(),
p.getCompany(), p.getCountry());
}

protected Name buildDn(String fullname,
String company, String country) {
DistinguishedName dn = new DistinguishedName();
dn.add("c", country);
dn.add("ou", company);
dn.add("cn", fullname);
return dn;
}

protected void mapToContext(Person p,
DirContextAdapter context) {
context.setAttributeValues("objectclass",
new String[] {"top", "person"});
context.setAttributeValue("cn", p.getFullName());
context.setAttributeValue("sn", p.getLastName());
context.setAttributeValue("description",
p.getDescription());
}

private static class PersonContextMapper
implements ContextMapper {
public Object mapFromContext(Object ctx) {
DirContextAdapter context = (DirContextAdapter)ctx;
Person p = new Person();
p.setFullName(context.getStringAttribute("cn"));
p.setLastName(context.getStringAttribute("sn"));
p.setDescription(
context.getStringAttribute("description"));
return p;
}
}
}

Resources

  • Sample code for this article
  • The LdapTemplate framework is available on SourceForge.
  • Sun's JNDI pages
  • OpenLDAP.org website
  • The RFC 1777 for LDAP version 2
  • The RFC 2251 for LDAP version 3

你可能感兴趣的:(java,attributes,string,class,spring,object)