CHAPTER 5

image

Advanced Spring LDAP

In this chapter, you will learn

  • The basics of JNDI object factories.
  • DAO implementation using object factories.

JNDI Object Factories

JNDI provides the notion of object factories, which makes dealing with LDAP information easier. As the name suggests, an object factory transforms directory information into objects that are meaningful to the application. For example, using object factories it is possible to have a search operation return object instances like Patron or Employee instead of plain javax.naming.NamingEnumeration.

Figure 5-1 depicts the flow involved when an application performs LDAP operations in conjunction with an object factory. The flow starts with the application invoking a search or a lookup operation. The JNDI API will execute the requested operation and retrieves entries from LDAP. These results are then passed over to the registered object factory, which transforms them into objects. These objects are handed over to the application.

9781430263975_Fig05-01.jpg

Figure 5-1. JNDI/object factory flow

Object factories dealing with LDAP need to implement the javax.naming.spi.DirObjectFactory interface. Listing 5-1 shows a Patron object factory implementation that takes the passed-in information and creates a Patron instance. The obj parameter to the getObjectInstance method holds reference information about the object. The name parameter holds the name of the object. The attrs parameter contains the attributes associated with the object. In the getObjectInstance you read the required attributes and populate the newly created Patron instance.

Listing 5-1.

package com.inflinx.book.ldap;
 
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.spi.DirObjectFactory
import com.inflinx.book.ldap.domain.Patron;
 
public class PatronObjectFactory implements DirObjectFactory {
 
   @Override
   public Object getObjectInstance(Object obj, Name name, Context nameCtx,Hashtable<?, ?> environment, Attributes attrs) throws Exception {
      Patron patron = new Patron();
      patron.setUid(attrs.get("uid").toString());
      patron.setFullName(attrs.get("cn").toString());
      return patron;
   }
 
   @Override
   public Object getObjectInstance(Object obj, Name name, Context nameCtx,Hashtable<?, ?> environment) throws Exception {
      return getObjectInstance(obj, name, nameCtx, environment, new BasicAttributes());
   }
}

Before you can start using this object factory, it must be registered during Initial Context creation. Listing 5-2 shows an example of using PatronObjectFactory during lookups. You register the PatronObjectFactory class using the DirContext.OBJECT_FACTORIES property. Notice that the context’s lookup method now returns a Patron instance.

Listing 5-2.

package com.inflinx.book.ldap;
 
import java.util.Properties;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import com.inflinx.book.ldap.domain.Patron;
 
public class JndiObjectFactoryLookupExample {
 
   private LdapContext 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:11389");
      environment.setProperty(DirContext.SECURITY_PRINCIPAL,"cn=Directory Manager");
      environment.setProperty(DirContext.SECURITY_CREDENTIALS, "opends");
      environment.setProperty(DirContext.OBJECT_FACTORIES, "com.inflinx.book.ldap.PatronObjectFactory");
      
         return new InitialLdapContext(environment, null);
   }
    
   public Patron lookupPatron(String dn) {
      Patron patron = null;
      try {
         LdapContext context = getContext();
         patron = (Patron) context.lookup(dn);
      }
      catch(NamingException e) {
        e.printStackTrace();
      }
      return patron;
   }
 
   public static void main(String[] args) {
      JndiObjectFactoryLookupExample jle = new JndiObjectFactoryLookupExample();
      Patron p = jle.lookupPatron("uid=patron99,ou=patrons," + "dc=inflinx,dc=com");
      System.out.println(p);
      }
}

Spring and Object Factories

Spring LDAP provides an out-of-the-box implementation of DirObjectFactory called org.springframework.ldap.core.support.DefaultDirObjectFactory. As you saw in the previous section, the PatronObjectFactory creates instances of Patrons from the contexts found. Similarly, the DefaultDirObjectFactory creates instances of org.springframework.ldap.core.DirContextAdapter from found contexts.

The DirContextAdapter class is generic in nature and can be viewed as a holder of LDAP entry data. The DirContextAdapter class provides a variety of utility methods that greatly simplify getting and setting attributes. As you will see in later sections, when changes are made to attributes, the DirContextAdapter automatically keeps track of those changes and simplifies updating LDAP entry’s data. The simplicity of the DirContextAdapter along with DefaultDirObjectFactory enables you to easily convert LDAP data into domain objects, reducing the need to write and register a lot of object factories.

In the next sections, you will be using the DirContextAdapter to create an Employee DAO that abstracts read and write access of Employee LDAP entries.

DAO DESIGN PATTERN

Most Java and JEE applications today access a persistent store of some type for their everyday activities. The persistent stores vary from popular relational databases to LDAP directories to legacy mainframe systems. Depending on the type of persistent store, the mechanism to obtain and manipulate data will vary greatly. This can result in tight coupling between the application and data access code, making it hard to switch between the implementations. This is where a Data Access Object or DAO pattern can help.

The Data Access Object is a popular core JEE pattern that encapsulates access to data sources. Low-level data access logic such as connecting to the data source and manipulating data is cleanly abstracted to a separate layer by the DAO. A DAO implementation usually includes the following:

  1. A DAO interface that provides the CRUD method contract.
  2. Concrete implementation of the interface using a data source-specific API.
  3. Domain objects or transfer objects that are returned by the DAO.

With a DAO in place, the rest of the application need not worry about the underlying data implementation and can focus on high-level business logic.

DAO Implementation Using Object Factory

Typically the DAOs you create in Spring applications have an interface that serves as the DAO’s contract and an implementation that contains the actual logic to access the data store or directory. Listing 5-3 shows the EmployeeDao interface for the Employee DAO you will be implementing. The DAO has create, update, and delete methods for modifying the employee information. It also has two finder methods, one that retrieves an employee by its id and another that returns all the employees.

Listing 5-3.

package com.inflinx.book.ldap.repository;
import java.util.List;
import com.inflinx.book.ldap.domain.Employee;
public interface EmployeeDao {
     public void create(Employee employee);
     public void update(Employee employee);
     public void delete(String id);
     public Employee find(String id);
     public List<Employee> findAll();
}

The previous EmployeeDao interface uses an Employee domain object. Listing 5-4 shows this Employee domain object. The Employee implementation holds all the important attributes of a Library employee. Notice that instead of using the fully qualified DN, you will be using the uid attribute as the object’s unique identifier.

Listing 5-4.

package com.inflinx.book.ldap.domain;
public class Employee {
     private String uid;
     private String firstName;
     private String lastName;
     private String commonName;
     private String email;
     private int departmentNumber;
     private String employeeNumber;
     private String[] phone;
     // getters and setters omitted for brevity
}

You start with a basic implementation of the EmployeeDao, as shown in Listing 5-5.

Listing 5-5.

package com.inflinx.book.ldap.repository;
 
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.simple.SimpleLdapTemplate;
import com.practicalspring.springldap.domain.Employee;
 
@Repository("employeeDao" )
public class EmployeeDaoLdapImpl implements EmployeeDao {
 
   @Autowired
   @Qualifier("ldapTemplate" )
   private SimpleLdapTemplate ldapTemplate;
 
   @Override
   public List<Employee> findAll() { return null; }
 
   @Override
   public Employee find(String id) { return null; }
 
   @Override
   public void create(Employee employee) {}
 
   @Override
   public void delete(String id) {}
 
   @Override
   public void update(Employee employee) {}
 
}

In this implementation, you are injecting an instance of SimpleLdapTemplate. The actual creation of the SimpleLdapTemplate will be done in an external configuration file. Listing 5-6 shows the repositoryContext.xml file with SimpleLdapTemplate and associated bean declarations.

Listing 5-6.

<?xml version="1.0" encoding="UTF-8"?>
 
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
 
   <context:component-scan base-package="com.inflinx.book.ldap" />
 
   <bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
      <property name="url" value="ldap://localhost:11389" />
      <property name="base" value="ou=employees,dc=inflinx,dc=com"/>
      <property name="userDn" value="uid=admin,ou=system" />
      <property name="password" value="secret" />
   </bean>
   <bean id="ldapTemplate" class="org.springframework.ldap.core.simple.SimpleLdapTemplate">
      <constructor-arg ref="contextSource" />
   </bean>
</beans>

This configuration file is similar to the one you saw in Chapter 3. You provide the LDAP server information to the LdapContextSource to create a contextSource bean. By setting the base to "ou=employees,dc=inflinx,dc=com", you have restricted all the LDAP operations to the employee branch of the LDAP tree. It is important to understand that a search operation on the branch "ou=patrons" will not be possible using the contexts created here. If the requirement is to search all of the branches of the LDAP tree, then the base property needs to be an empty string.

An important property of LdapContextSource is the dirObjectFactory, which can be used to set a DirObjectFactory to use. However, in Listing 5-6, you didn’t use this property to specify your intent to use DefaultDirObjectFactory. That is because by default LdapContextSource registers the DefaultDirObjectFactory as its DirObjectFactory.

In the final portion of the configuration file you have the SimpleLdapTemplate bean declaration. You have passed in the LdapContextSource bean as the constructor argument to the SimpleLdapTemplate.

Implementing Finder Methods

Implementing the findAll method of the Employee DAO requires searching LDAP for all the employee entries and creating Employee instances with the returned entries. To do this, you will be using the following method in the SimpleLdapTemplate class:

public <T> List<T> search(String base, String filter, ParameterizedContextMapper<T> mapper)

Since you are using the DefaultDirObjectFactory, every time a search or a lookup is performed, every context found in the LDAP tree will be returned as an instance of DirContextAdapter. Like the search method you saw in Listing 3-8, the above search method takes a base and filter parameters. Additionally, it takes an instance of ParameterizedContextMapper<T>. The above search method will pass the returned DirContextAdapters to the ParameterizedContextMapper<T> instance for transformation.

The ParameterizedContextMapper<T> and its parent interface, the ContextMapper, hold the mapping logic needed to populate domain objects from the passed-in DirContextAdapter. Listing 5-7 provides the context mapper implementation for mapping Employee instances. As you can see, the EmployeeContextMapper extends AbstractParameterizedContextMapper, which is an abstract class that implements ParameterizedContextMapper.

Listing 5-7.

package com.inflinx.book.ldap.repository.mapper;
 
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.simple.AbstractParameterizedContextMapper;
import com.inflinx.book.ldap.domain.Employee;
 
public class EmployeeContextMapper extends AbstractParameterizedContextMapper<Employee> {
 
   @Override
   protected Employee doMapFromContext(DirContextOperations context) {
 
   Employee employee = new Employee();
   employee.setUid(context.getStringAttribute("UID"));
   employee.setFirstName(context.getStringAttribute("givenName"));
   employee.setLastName(context.getStringAttribute("surname"));
   employee.setCommonName(context.getStringAttribute("commonName"));
   employee.setEmployeeNumber(context.getStringAttribute("employeeNumber"));
   employee.setEmail(context.getStringAttribute("mail"));
   employee.setDepartmentNumber(Integer.parseInt(context.getStringAttribute("departmentNumber")));
   employee.setPhone(context.getStringAttributes("telephoneNumber"));
    
   return employee;
   }
}

In Listing 5-7, the DirContextOperations parameter to the doMapFromContext method is an interface for DirContextAdapter. As you can see, the doMapFromContext implementation involves creating a new Employee instance and reading the attributes you are interested in from the supplied context.

With the EmployeeContextMapper in place, the findAll method implementation becomes trivial. Since all the employee entries have the objectClass inetOrgPerson, you will be using "(objectClass=inetOrgPerson)" as the search filter. Listing 5-8 shows the findAll implementation.

Listing 5-8.

@Override
public List<Employee> findAll() {
   return ldapTemplate.search("", "(objectClass=inetOrgPerson)", new EmployeeContextMapper());
}

The other finder method can be implemented in two ways: searching an LDAP tree with the filter (uid=<supplied employee id>) or performing an LDAP lookup with an employee DN. Since search operations with filters are more expensive than looking up a DN, you will be implementing the find method using the lookup. Listing 5-9 shows the find method implementation.

Listing 5-9.

@Override
public Employee find(String id) {
   DistinguishedName dn = new DistinguishedName();
   dn.add("uid", id);
   return ldapTemplate.lookup(dn, new EmployeeContextMapper());
}

You start the implementation by constructing a DN for the employee. Since the initial context base is restricted to the employee branch, you have just specified the RDN portion of the employee entry. Then you use the lookup method to look up the employee entry and create an Employee instance using the EmployeeContextMapper.

This concludes the implementation of both finder methods. Let’s create a JUnit test class for testing your finder methods. The test case is shown in Listing 5-10.

Listing 5-10.

package com.inflinx.book.ldap.repository;
 
import java.util.List;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ldapunit.util.LdapUnitUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.inflinx.book.ldap.domain.Employee;
 
@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration(locations={"classpath:repositoryContext-test.xml"})
public class EmployeeDaoLdapImplTest {
 
   private static final String PORT = "12389";
   private static final String ROOT_DN = "dc=inflinx,dc=com";
 
   @Autowired
   @Qualifier("employeeDao" )
   private EmployeeDao employeeDao;
 
   @Before
   public void setup() throws Exception {
      System.out.println("Inside the setup");
      LdapUnitUtils.loadData(new ClassPathResource("employees.ldif"), PORT);
   }
 
   @After
   public void teardown() throws Exception {
      System.out.println("Inside the teardown");
      LdapUnitUtils.clearSubContexts(new DistinguishedName(ROOT_DN), PORT);
   }
 
   @Test
   public void testFindAll() {
      List<Employee> employeeList = employeeDao.findAll();
      Assert.assertTrue(employeeList.size() > 0);
   }
 
   @Test
   public void testFind() {
      Employee employee = employeeDao.find("employee1");
      Assert.assertNotNull(employee);
   }
}

Notice that you have specified the repositoryContext-test.xml in the ContextConfiguration. This test context file is shown in Listing 5-11. In the configuration file you have created an embedded context source using the LdapUnit framework’s EmbeddedContextSourceFactory class. The embedded LDAP server is an instance of OpenDJ (as specified by the property serverType) and will run on port 12389.

The setup and teardown methods in the JUnit test case are implemented for loading and deleting test employee data. The employee.ldif file contains the test data you will be using throughout this book.

Listing 5-11.

<?xml version="1.0" encoding="UTF-8"?>
 
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
 
   <context:component-scan base-package="com.inflinx.book.ldap" />
    
   <bean id="contextSource" class="org.ldapunit.context.EmbeddedContextSourceFactory">
      <property name="port" value="12389" />
      <property name="rootDn" value="dc=inflinx,dc=com" />
      <property name="base" value="ou=employees,dc=inflinx,dc=com" />
      <property name="serverType" value="OPENDJ" />
   </bean>
   <bean id="ldapTemplate" class="org.springframework.ldap.core.simple.SimpleLdapTemplate">
      <constructor-arg ref="contextSource" />
   </bean>
</beans>

Create Method

SimpleLdapTemplate provides several bind methods for adding entries to LDAP. To create a new Employee you will use the following bind method variation:

public void bind(DirContextOperations ctx)

This method takes a DirContextOperations instance as its parameter. The bind method invokes the getDn method on the passed-in DirContextOperations instance and retrieves the fully qualified DN of the entry. It then binds all the attributes to the DN and creates a new entry.

The implementation of the create method in the Employee DAO is shown in Listing 5-12. As you can see, you start by creating a new instance of a DirContextAdapter. Then you populate the context’s attributes with employee information. Notice that the departmentNumber’s int value is being explicitly converted to a String. If this conversion is not done, the method will end up throwing an “org.springframework.ldap.InvalidAttributeValueException” exception. The last line in the method does the actual binding.

Listing 5-12.

@Override
public void create(Employee employee) {
     DistinguishedName dn = new DistinguishedName();
     dn.add("uid", employee.getUid());
 
     DirContextAdapter context = new DirContextAdapter();
     context.setDn(dn);      context.setAttributeValues("objectClass", new String[]
     {"top", "person", "organizationalPerson", "inetOrgPerson"});
     context.setAttributeValue("givenName", employee.getFirstName());
     context.setAttributeValue("surname", employee.getLastName());
     context.setAttributeValue("commonName", employee.getCommonName());
     context.setAttributeValue("mail", employee.getEmail());
     context.setAttributeValue("departmentNumber",
     Integer.toString(employee.getDepartmentNumber()));
     context.setAttributeValue("employeeNumber", employee.getEmployeeNumber());
     context.setAttributeValues("telephoneNumber",employee.getPhone());
 
    ldapTemplate.bind(context);
}

image Note   Compare the code in Listing 5-12 with the code in Listing 3-10. You can clearly see that DirContextAdapter does a great job simplifying attribute manipulation.

Let’s quickly verify the create method’s implementation with the JUnit test case in Listing 5-13.

Listing 5-13.

@Test
public void testCreate() {
   Employee employee = new Employee();
   employee.setUid("employee1000");
   employee.setFirstName("Test");
   employee.setLastName("Employee1000");
   employee.setCommonName("Test Employee1000");
   employee.setEmail("[email protected]" );
   employee.setDepartmentNumber(12356);
   employee.setEmployeeNumber("45678");
   employee.setPhone(new String[]{"801-100-1200"});
    
   employeeDao.create(employee);
}
  

Update Method

Updating an entry involves adding, replacing, or removing its attributes. The simplest way to achieve this is to remove the entire entry and create it with a new set of attributes. This technique is referred to as rebinding. Deleting and recreating an entry is obviously not efficient, and it makes more sense to just operate on changed values.

In Chapter 3, you used the modifyAttributes and ModificationItem instances for updating LDAP entries. Even though modifyAttributes is a nice approach, it does require a lot of work to manually generate the ModificationItem list. Thankfully, DirContextAdapter automates this and makes updating an entry a breeze. Listing 5-14 shows the update method implementation using DirContextAdapter.

Listing 5-14.

@Override
public void update(Employee employee) {
   DistinguishedName dn = new DistinguishedName();
   dn.add("uid", employee.getUid());
    
   DirContextOperations context = ldapTemplate.lookupContext(dn);
   context.setAttributeValues("objectClass", new String[] {"top", "person", "organizationalPerson", "inetOrgPerson"});
   context.setAttributeValue("givenName", employee.getFirstName());
   context.setAttributeValue("surname", employee.getLastName());
   context.setAttributeValue("commonName", employee.getCommonName());
   context.setAttributeValue("mail", employee.getEmail());
   context.setAttributeValue("departmentNumber", Integer.toString(employee.getDepartmentNumber()));
   context.setAttributeValue("employeeNumber", employee.getEmployeeNumber());
   context.setAttributeValues("telephoneNumber", employee.getPhone());
    
   ldapTemplate.modifyAttributes(context);
}

In this implementation, you will notice that you first look up the existing context using the employee’s DN. Then you set all the attributes like you did in the create method. (The difference being that DirContextAdapter keeps track of value changes made to the entry.) Finally, you pass in the updated context to the modifyAttributes method. The modifyAttributes method will retrieve the modified items list from the DirContextAdapter and perform those modifications on the entry in LDAP. Listing 5-15 shows the associated test case that updates the first name of an employee.

Listing 5-15.

@Test
public void testUpdate() {
   Employee employee1 = employeeDao.find("employee1");
   employee1.setFirstName("Employee New");
   employeeDao.update(employee1);
   employee1 = employeeDao.find("employee1");
   Assert.assertEquals(employee1.getFirstName(),"Employee New");
}
  

Delete Method

Spring LDAP makes unbinding straightforward with the unbind method in the LdapTemplate/SimpleLdapTemplate. Listing 5-16 shows the code involved in deleting an employee.

Listing 5-16.

@Override
public void delete(String id) {
    DistinguishedName dn = new DistinguishedName();
    dn.add("uid", id);
    ldapTemplate.unbind(dn);
}

Since your operations are all relative to the initial context with the base "ou=employees,dc=inflinx,dc=com", you create the DN with just uid, the entry’s RDN. Invoking the unbind operation will remove the entry and all its associated attributes.

Listing 5-17 shows the associated test case to verify the deletion of the entry. Once an entry is successfully removed, any find operation on that name will result in a NameNotFoundException. The test case validates this assumption.

Listing 5-17.

@Test(expected=org.springframework.ldap.NameNotFoundException.class)
public void testDelete() {
     String empUid = "employee11";
     employeeDao.delete(empUid);
     employeeDao.find(empUid);
}

Summary

In this chapter, you were introduced to the world of JNDI object factories. You then looked at the DefaultDirObjectFactory, Spring LDAP’s object factory implementation. You spent the rest of the chapter implementing an Employee DAO using DirContextAdapter and SimpleLdapTemplate.

In the next chapter, you will take a deep dive into the world of LDAP search and search filters.

..................Content has been hidden....................

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