Skip to content

Fun with Spring, Ldap and Java Annotations

April 20, 2007

I thought it would be neat to have a way to persist Java objects to and from an Ldap directory. Kinda like “Hibernate lite” for directories. I saw several inquiries on the Hibernate forums discussing a custom mapper for Ldap – but nothing has ever been implemented. The concensus seems to be that it ought to be possible – but perhaps the relational model that Hibernate is based on is not a great fit for Ldap.

In any event, I created a very simple package which I have Dubbed “Slapper”. The readme is presented below. If you think this has value, drop me a note and I may extend the implementation…

Slapper

What does it stand for? How about “Simple Ldap Mapper”. OK, yes it is kinda lame

In a nutshell, Slapper is a utility package that uses annotations to persist Java POJOs to and from an Ldap directory. This is a very simplistic mapper – It currently does not handle any relationship navigation. This package uses Spring Ldap.

To use Slapper, you first create a POJO (well almost a POJO, as it must implement the DirObject interface) that has the appropriate annotations. Here is an example:


@ObjectClass({"inetorgperson", "organizationalperson", "person", "top"})
public class UserAccount implements DirObject {
private Name name;
private String commonName;
private String lastName;
private byte[] userPassword;

@DirectoryAttribute("userPassword" )
public byte[] getUserPassword() {
return userPassword;
}

public void setUserPassword(byte[] userPassword) {
this.userPassword = userPassword;
}


@DirectoryAttribute(value="cn")
public String getCommonName() {
return commonName;
}

/**
*
* @param commonName
*/
public void setCommonName(String commonName) {
this.commonName = commonName;
}


.....

The next step is to configure your Spring beans.xml to create a DAO object that can read and write to this object type. Here is an
example:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="contextSource" class="org.springframework.ldap.support.LdapContextSource">
<property name="url" value="ldap://localhost:389" />
<property name="base" value="dc=example,dc=com" />
<property name="userName" value="cn=Directory Manager" />
<property name="password" value="passw0rd" />
</bean>

<bean id="ldapTemplate" class="org.springframework.ldap.LdapTemplate">
<constructor-arg ref="contextSource" />
</bean>

<bean name="userMarshaller" class="com.my2do.slapper.Marshaller">
<constructor-arg value="com.my2do.slapper.example.UserAccount"/>
</bean>

<bean id="userDAO" class="com.my2do.slapper.LdapDAOImpl">
<property name="ldapTemplate" ref="ldapTemplate" />
<property name="marshaller" ref="userMarshaller"/>
</bean>

</beans>

The ldapTemplate is standard Spring Ldap code (see the Spring docs for the details). The only tricky bit of the above is the Marshaller. This is a class that knows how to marshal annotated POJOs
to and from directory attributes. The marshaller processes @DirectoryAttribute annotations and maps them to the appropriate Ldap attribute. In the above example, the userDAO bean is configured to read and write beans of type UserAccount.

The DAO interface is fairly simple at this point:


public interface LdapDAO {

/**
* Retrieve an ldap object from the directory
* @param dn String Dn of the object
* @return Returns an object populated with directory attributes. The
* type of the object will be that created by the underlying Marshaller
*/
public Object getObject(String dn);
/**
* Retrieve an ldap object from the directory
* @param name (ldap dn) of the object to fetch
* @return Returns an object populated with directory attributes. The
* type of the object will be that created by the underlying Marshaller
*/
public Object getObject(Name name);

/**
* Create the object in the directory. The Dn of the object
* must be set properly (getName() must return a valid Dn). The objects
* attributes will be extracted with the Marsheller instance and sent
* to the directory.
* @param obj object to create.
*
*/
public void create(DirObject obj) ;

/**
* Delete the object from the directory. The objects getName() must
* return a valid Dn.
*
* @param obj Object to delete
*/
public void delete(DirObject obj) ;

/**
* Updated the given object.
* The object will first be read back from the directory, and only
* changed attributes will be modified.
* @param obj Object to update
*/
public void update(DirObject obj) ;

/**
* Setter for marhsaller. This will normally be injected by Spring,
* but is exposed as part of the interface in case you want
* to control the Marshalling strategy
* @param m Marshaller instance that knows how to marshal directory
* attributes to/from an object
*/
public void setMarshaller(Marshaller m);

/**
* Very simple search function. Will search at the base dn and subtrees for
* objects which meet the filter criteria. Objects will be marshalled into the
* list.
*/
public List search(Name base, String filter );

}


Putting it all together, here is a sample test that shows how the API
is used (some code has been elided for brevity…)

...

public class SlapperTest {
BeanFactory factory;
LdapDAO userDAO;

public SlapperTest() {
Resource r = new FileSystemResource("test/beans.xml");
factory = new XmlBeanFactory(r);
userDAO = (LdapDAO) factory.getBean("userDAO");
}


@Test
public void crudTest() throws InvalidNameException {
UserAccount ua = new UserAccount();
Name name= new LdapName("uid=test2, ou=People");

ua.setName(name );
ua.setCommonName("Fred Flinstone");
ua.setLastName("Flinstone");
userDAO.create(ua);

// try to create the user twice - should get an error
try {
userDAO.create(ua);
fail("Expected to get an exception");
} catch(DataIntegrityViolationException ex) {
System.out.println("OK - Got expected exception" + ex);
}

UserAccount ua2 = (UserAccount)userDAO.getObject(name);

System.out.println("Got user back " + ua2 + " is equal " + ua2.equals(ua) );

assertNotNull(ua2);

// Dn compare is broken due to bug in LdapName.equals() .....
//assertEquals( ua, ua2);
// so compare some other attribute
assertEquals( ua.getCommonName(), ua2.getCommonName());

// update the user. Only the changes will get sent to the directory
ua.setMobile("555-1212");
ua.setLastName("Rubble");
userDAO.update(ua);
// read it back and make sure the mobile got set
ua2 = (UserAccount)userDAO.getObject(name);
assertEquals( ua.getMobile(), ua2.getMobile());

// delete the user
userDAO.delete(ua);
System.out.println("deleted user " + ua);
// deleting twice seems to be silentluy ignored. OK?
userDAO.delete(ua);
}

@Test
public void simpleSearchTest() throws InvalidNameException {

LdapName base = new LdapName("ou=People");

//String filter = "& (objectclass=inetorgperson) (uid=user*)";

// build a filter that matches all inetorgperson objects whose
// uid starts with user*
// These are the sample users created by the OpenDS installer
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "inetorgperson"));
filter.and(new WhitespaceWildcardsFilter("uid", "user "));
System.out.println("Using filter " + filter.encode() );

// List l will be populated with UserAccount objects
// Assumes we have some sample users in the directory
List l = userDAO.search(base, filter.encode());
assertEquals( l.get(0).getClass(), UserAccount.class );

System.out.println("Got search result =" + l);

}

}

Powered by ScribeFire.

Advertisements

Comments are closed.

%d bloggers like this: