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 Spring LDAP library
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.
The DefaultDirObjectFactory
is registered with
the ContextSource
by default, which means that whenever
a context is found in the LDAP tree, its Attributes
and
Distinguished Name (DN) will be used to construct a
DirContextAdapter
. This enables us to use a
ContextMapper
instead of an
AttributesMapper
to transform found values:
Example 3.1. Searching using a ContextMapper
package com.example.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. This is particularly useful when working with multi-value attributes. Extracting values from
multi-value attributes normally requires looping through a NamingEnumeration
of
attribute values returned from the Attributes
implementation. The
DirContextAdapter
can do this for you, using the getStringAttributes()
or getObjectAttributes()
methods:
Example 3.2. Getting multi-value attribute values using getStringAttributes()
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"));
// The roleNames property of Person is an String array
p.setRoleNames(context.getStringAttributes("roleNames"));
return p;
}
}
Spring LDAP provides an abstract base implementation of ContextMapper
,
AbstractContextMapper
. This automatically takes care of the casting of the supplied
Object
parameter to DirContexOperations
.
The PersonContextMapper
above can thus be re-written as follows:
Example 3.3. Using an AbstractContextMapper
private static class PersonContextMapper extends AbstractContextMapper { public Object doMapFromContext(DirContextOperations ctx) { Person p = new Person(); p.setFullName(context.getStringAttribute("cn")); p.setLastName(context.getStringAttribute("sn")); p.setDescription(context.getStringAttribute("description")); return p; } }
While very useful when extracting attribute values, DirContextAdapter
is even more
powerful for hiding attribute details when binding and modifying data.
This is an example of an improved implementation of the create DAO method. Compare it with the previous implementation in Section 2.4.1, “Binding Data”.
Example 3.4. Binding using DirContextAdapter
package com.example.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(context);
}
}
Note that we use the DirContextAdapter
instance
as the second parameter to bind, which should be a Context
.
The third parameter is null
, since we're not using any
Attributes
.
Also note the use of the setAttributeValues()
method when setting the
objectclass
attribute values. The objectclass
attribute is
multi-value, and similar to the troubles of extracting muti-value attribute data, building multi-value
attributes is tedious and verbose work. Using the setAttributeValues()
mehtod you can
have DirContextAdapter
handle that work for you.
The code for a rebind
would be pretty much
identical to Example 3.4, “Binding using DirContextAdapter
”, except
that the method called would be rebind
. As we saw in
Section 2.5.2, “Modifying using modifyAttributes
” a more correct approach would be to
build a ModificationItem
array containing the actual
modifications you want to do. This would require you to determine the actual
modifications compared to the data present in the LDAP tree. Again, this
is something that DirContextAdapter
can help you with; the
DirContextAdapter
has the ability to keep track of
its modified attributes. The following example takes advantage of this
feature:
Example 3.5. Modifying using DirContextAdapter
package com.example.dao; public class PersonDaoImpl implements PersonDao { ... public void update(Person p) { Name dn = buildDn(p); DirContextOperations context = ldapTemplate.lookupContext(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(context); } }
When no mapper is passed to a ldapTemplate.lookup()
operation,
the result will be a DirContextAdapter
instance.
While the lookup
method returns an Object
, the convenience
method lookupContext
method automatically casts the return value to
a DirContextOperations
(the interface that DirContextAdapter
implements.
The observant reader will see that we have duplicated 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:
Example 3.6. Binding and modifying using DirContextAdapter
package com.example.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(context); } public void update(Person p) { Name dn = buildDn(p); DirContextOperations context = ldapTemplate.lookupContext(dn); mapToContext(person, context); ldapTemplate.modifyAttributes(context); } protected void mapToContext (Person p, DirContextOperations context) { context.setAttributeValues("objectclass", new String[] {"top", "person"}); context.setAttributeValue("cn", p.getFullName()); context.setAttributeValue("sn", p.getLastName()); context.setAttributeValue("description", p.getDescription()); } }
To illustrate the power of Spring LDAP, here is a complete Person DAO implementation for LDAP in just 68 lines:
Example 3.7. A complete PersonDao class
package com.example.dao; import java.util.List; import javax.naming.Name; import javax.naming.NamingException; import javax.naming.directory.Attributes; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.ContextMapper; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.support.DistinguishedName; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.filter.WhitespaceWildcardsFilter; public class PersonDaoImpl implements PersonDao { private LdapTemplate ldapTemplate; public void setLdapTemplate(LdapTemplate ldapTemplate) { this.ldapTemplate = ldapTemplate; } public void create(Person person) { DirContextAdapter context = new DirContextAdapter(buildDn(person)); mapToContext(person, context); ldapTemplate.bind(context); } public void update(Person person) { Name dn = buildDn(person); DirContextOperations context = ldapTemplate.lookupContext(dn); mapToContext(person, context); ldapTemplate.modifyAttributes(context); } public void delete(Person person) { ldapTemplate.unbind(buildDn(person)); } public Person findByPrimaryKey(String name, String company, String country) { Name dn = buildDn(name, company, country); return (Person) ldapTemplate.lookup(dn, getContextMapper()); } public List findByName(String name) { AndFilter filter = new AndFilter(); filter.and(new EqualsFilter("objectclass", "person")).and(new WhitespaceWildcardsFilter("cn",name)); return ldapTemplate.search(DistinguishedName.EMPTY_PATH, filter.encode(), 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 person) { return buildDn(person.getFullname(), person.getCompany(), person.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 person, DirContextOperations context) { context.setAttributeValues("objectclass", new String[] {"top", "person"}); context.setAttributeValue("cn", person.getFullName()); context.setAttributeValue("sn", person.getLastName()); context.setAttributeValue("description", person.getDescription()); } private static class PersonContextMapper extends AbstractContextMapper { public Object doMapFromContext(DirContextOperations context) { Person person = new Person(); person.setFullName(context.getStringAttribute("cn")); person.setLastName(context.getStringAttribute("sn")); person.setDescription(context.getStringAttribute("description")); return person; } } }
In several cases the Distinguished Name (DN) of an object is
constructed using properties of the object. E.g. in the above example,
the country, company and full name of the Person
are
used in the DN, which means that updating any of these properties will
actually require moving the entry in the LDAP tree using the
rename()
operation in addition to updating the
Attribute
values. Since this is highly implementation
specific this is something you'll need to keep track of yourself -
either by disallowing the user to change these properties or performing
the rename()
operation in your
update()
method if needed.