Friday, July 17, 2009

Using Spring-iBatis to access LDAP - Part 2

Introduction

In the first half of this series, I described our experience with the latest Spring-LDAP module, concluding that an alternative mechanism would be needed due to deployment issues. I promised an alternative iBatis-over-LDAP solution, and in this article I provide demonstration code samples.
With this approach in place, we can view an LDAP repository as an ordinary SQL-based database, albeit with its own variant of SQL. Other advantages include the standard iBatis benefits, in particular the ability to declaratively specify SQL queries and as such manage them from a single point of change.

What follows is a description of how I put a quick-and-dirty prototype together, in particular using Spring 3.0.

Dependencies

The key to this solution is a JDBC driver that wraps LDAP repositories, created by Octet String (and now owned by Oracle). Once we have a JDBC driver, in theory we should be able to layer iBatis over it.

Download version 2.1 of that driver:
https://sourceforge.net/projects/myvd/files/

Put these jars from that download into your runtime classpath:

jdbcLdap.jar
ldap.jar

You'll need the standard set of jars for Spring 3.0 - for the minimal Spring 3.0 prototyping I've done so far, here is the set that has worked for me:

commons-lang.jar
commons-logging.jar
antlr-3.0.1.jar
org.springframework.asm-3.0.0.M3.jar
org.springframework.beans-3.0.0.M3.jar
org.springframework.context-3.0.0.M3.jar
org.springframework.core-3.0.0.M3.jar
org.springframework.expression-3.0.0.M3.jar

Add in iBatis:

iBatis-2.3.4.jar

Add what's needed to support Spring 3.0 over iBatis:

commons-dbcp.jar 
com.springsource.org.apache.commons.pool-1.4.0.jar
org.springframework.jdbc-3.0.0.M3.jar
org.springframework.orm-3.0.0.M3.jar
org.springframework.transaction-3.0.0.M3.jar

The last jar listed above is the Spring 3.0 version of transactions. The 2.5.6 version is required for Spring-LDAP, and since the classes in both jars are largely (if not completely) the same, this raises a warning flag about classloading conflicts. I didn't get far enough into a test execution to hit any problems; instead I found that deploying Spring-LDAP with Spring 3.0 AOP causes a runtime error out of the gate (during Spring initialization), and at that point I bailed out on use of Spring LDAP (please refer to my first article for the details).

Finally, though it's not necessary for this example, I include the Spring-AOP libraries, if for nothing else to demonstrate that we won't see the same runtime error as happened using Spring-LDAP with Spring 3.0. You can safely exclude these for your own prototyping of this solution (but if so, just be sure to remove the AOP references in the Spring configuration file described below):

com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.runtime-1.6.5.RELEASE.jar
com.springsource.org.aspectj.weaver-1.6.5.RELEASE.jar
org.springframework.aop-3.0.0.M3.jar


Code Artifacts


Now you're ready to do some coding. First, construct the domain object:

package com.mybiz.model;

public class User {

    private String uid;

    public User() {}
   
    public String getUid() { return uid; }
    public void setUid(String uid) { this.uid = uid; }

    @Override
    public String toString() { return this.uid; }
}

Construct the DAO for the domain object. I recommend using an interface and its implementation - because (1) using interfaces is in general a good idea for testability, polymorphism, and etc., and (2) in particular with AOP-proxied classes in Spring, I'd favor this approach because it facilitates use of JDK dynamic proxies instead of CGLIB proxies. Use of CGLIB involves these things: (1) more library dependencies, (2)  final methods cannot participate in AOP, and (3) constructors are called twice due to CGLIB implementation details. Again, use of an interface is an optional step; if you exclude it, you'll need the CGLIB jars (which I haven't used - and plan to avoid using - so I can't point you to the exact set needed).

In either event, here's the DAO interface and implementation:

package com.mybiz.model;

import java.util.List;

public interface UserDao {
    public List getAllUsers();
}

package com.mybiz.model;

import com.ibatis.sqlmap.client.SqlMapClient;

import java.sql.SQLException;
import java.util.List;

public class MyUserDao implements UserDao { 

    // injected via Spring
    private SqlMapClient sqlMapClient;
    public void setSqlMapClient(SqlMapClient sqlMapClient) { this.sqlMapClient = sqlMapClient; }
    private SqlMapClient getSqlMapClient() { return this.sqlMapClient; }

    public List<user> getAllUsers()
    {
        String query = "selectAll";
        try {
            return (List<user>)getSqlMapClient().queryForList(query);
        } catch (SQLException e) {
            System.err.println("Query '" + query + "' failed: " + e);
            throw new IllegalStateException(e);
        }
    }
}

The database properties are described in a standalone file, to facilitate easy modifications to suit different environments (test, development, production). The key here is to specify the ignore_transactions parameter in the URL; adding the search_scope as a default scope is an optional convenience so that this won't be needed in every one of your SQL queries. Let's name the file database.properties in the com/mybiz/model directory; you'll of course need to adjust the parameters here to match your environment:

database.driver=com.octetstring.jdbcLdap.sql.JdbcLdapDriver
database.url=jdbc:ldap://127.0.0.1:389/dc=mybiz,dc=com?search_scope:=subTreeScope&ignore_transactions:=true
database.user=cn=Manager,dc=mybiz,dc=com
database.password=verycleverpassword

Following through with my convention from the first half of this series, I've bold-faced the relevant configuration pieces so you can compare how things are specified. Above, we see that the database connection parameters (LDAP URL, base node specification, principal and password) have moved from the Spring configuration (when using Spring-LDAP, as per my previous article) and into this file.

Next, layer iBatis over the database connection. We need first the SQL Map Configuration file; it doesn't need to do anything but point to your collection of SQL Maps. Let's call it SqlMapConfig.xml in the com/mybiz/model directory:

<!DOCTYPE sqlMapConfig
        PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
        "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <sqlMap resource="com/mybiz/model/SqlMap.xml"/>
</sqlMapConfig>

Here is the specified SQL Map. As per the above specification, it should be named SqlMap.xml in the com/mybiz/model directory:

<!DOCTYPE sqlMap
    PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
    "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="user">
  <typeAlias alias="user" type="com.mybiz.model.User"/>
  <select id="selectAll" resultClass="user">
    SELECT uid  FROM dc=mybiz,dc=com  where objectclass='person'
  </select>
</sqlMap>

Here, we have bold-faced the LDAP attribute to be fetched, the node from which the search begins, and the search qualifier. While these query parameters plus the database connection parameters were all in one place using Spring-LDAP, here they've been split up into separate files. It's your judgment as to whether this is a good thing or not; personally, I consider these things as separate concerns, so I prefer it. However, you might prefer single point of change for all things database-related and/or all things configuration-related. More importantly, we can now use iBatis' parameterized substitution to declare a query, keeping it readably intact while varying its parameter values dynamically. With Spring-LDAP, we can likewise achieve dynamic query construction, but the model is more procedural.

In the above SQL Map, you'll notice the interesting use of an LDAP base specification instead of a table name (and again, you'll need to adjust this query to match up with your LDAP repository structure). The docs for the JDBC-LDAP bridge do mention that one can map things to an aliased table name, but for our purposes here, transparency and simplicity are more important.

The next step is to configure iBatis with Spring. I included transactional and AOP support, though these are optional for this exercise. Some explanations:
  • Though LDAP itself is not transactional (hence the ignore_transactions parameter above), you might be in an environment where you'll also be accessing normal relational databases, for which you'll want Spring's transactional support. Since we're now solely Spring 3.0, there is in either event no fear of classloading conflicts. However, to make this a bare-bones proof-of-concept, you can exclude the tx namespace declaration in the beans tag, the transaction manager bean and the tx:annotation-driven tag.
  • This Spring config file also configures in AOP solely to demonstrate that there is no longer any deployment conflict as was seen with combining Spring-LDAP and Spring-3.0-AOP; it's not essential for the iBatis-over-LDAP solution per se. As I mentioned above, if you specify AOP in this Spring configuration, you'll need the Spring-AOP runtime jars. Again, to make this a bare-bones proof-of-concept, you can exclude the aop namespace declaration in the beans tag and the aop:aspectj-autoproxy tag.
  • For that matter, Spring itself is not essential to layering iBatis over LDAP - I was able to remove Spring from the stack entirely and get the same successful result. However, Spring provides various benefits to an iBatis application, so I include it here for completeness (and, frankly, because I rely on Spring to help me). If you exclude it, you'll need to configure iBatis manually; I do not include those details here.
Let's name the file beans.xml, in the com/mybiz/model directory:

<beans xmlns="http://www.springframework.org/schema/beans"   
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

    <!-- OPTIONAL: AOP -->
    <aop:aspectj-autoproxy/>

    <!-- support for referencing properties from the database.propeties file  in the data source bean, below -->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:com/mybiz/model/database.properties"/>
    </bean>

    <!-- OPTIONAL: transaction manager - needed more for regular databases, not for LDAP -->
    <bean id="txnMgr" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dbcpDataSource"/>
    </bean>
    <!-- OPTIONAL -->
    <tx:annotation-driven transaction-manager="txnMgr"/>

    <!-- Apache DBCP data source, configured with properties from database.properties file -->
    <bean id="dbcpDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${database.driver}"/>
        <property name="url" value="${database.url}"/>
        <property name="username" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>

    <!-- iBatis SQL Map client, configured with the config-file location and the data source   -->
    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="configLocation" value="classpath:com/mybiz/model/SqlMapConfig.xml"/>
        <property name="dataSource" ref="dbcpDataSource"/>
    </bean>

    <!-- inject the iBatis SQL Map client into the application DAO -->
    <bean id="userDao" class="com.mybiz.model.MyUserDao">
        <property name="sqlMapClient" ref="sqlMapClient"/>
    </bean>
    
</beans>

You'll notice there are no bold-faced sections in the Spring configuration file (i.e., no application-specific configurations are specified). The database connection and database query configuration information are now in separate files dedicated to these responsibilities alone, vs being embedded in what's potentially a rather large Spring configuration. Again, your take on this may vary from mine: I consider this a good thing, but you might prefer all configuration information to be in a single file.

Finally, we provide a test driver program:

package com.mybiz.demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mybiz.model.User;
import com.mybiz.model.UserDao;
import java.util.List;

public class IBatisOverLdapDemo {

    public static void main(String[] args) {

        ApplicationContext context = new ClassPathXmlApplicationContext(
                "com/mybiz/model/beans.xml");
        UserDao dao = (UserDao) context.getBean("userDao");

        List<user> users = dao.getAllUsers();
        for (User user : users) {
            System.out.println(user);
        }
    }
}

Executing this program, with database connection information, queries, and etc. tailored to your LDAP environment, should yield a list of attribute values as expected.

Conclusion

Using iBatis-over-LDAP instead of JNDI-LDAP, you can:
  • configure, connect to and query both RDBMS and LDAP databases in the same manner
  • be unencumbered by JNDI-LDAP programming models
  • gain all other benefits that iBatis has to offer, since the LDAP-ness has been (mostly) abstracted away. For example, fine-tuning queries for performance can be done without changing Java code.
Using iBatis-over-LDAP instead of Spring-LDAP, you gain all of the above advantages, plus these:
  • all SQL is established declaratively (vs being constructed programmatically), facilitating maintenance and debugging
  • as a result, the DAO needs fewer properties - i.e., with Spring LDAP, the DAO needs things like the LDAP base, LDAP attribute(s) to be returned and a search-filter qualifier to configure the search method. However, using iBatis, all of these things are simply part of the SQL statement specified in the SQL Map file.
Spring-LDAP has a lot to offer, including numerous features that this solution lacks, and for many applications will be very suitable. In my opinion, its strongest value-add is the compensating transactional control in an LDAP environment. If our application was more demanding than it is, I'd be motivated to dig further into the Spring-LDAP feature set and would likely be all the more impressed - in my opinion, what the Spring team has been doing is the best thing to ever happen to J2EE, and their LDAP work is likely no exception. But for our application needs - very simple CRUD + Spring 3.0 AOP - the Spring LDAP module is not the right choice. The iBatis-over-LDAP solution is clean and simple, meeting our requirements with a minimum of fuss.

Resources

Using Spring-iBatis to access LDAP - Part 1
, by Gary Horton, July 2009
MyVD Virtual Directory: JDBC-LDAP Bridge
Spring LDAP 1.3.x Reference Documentation, by Mattias Arthursson, Ulrik Sandberg, Eric Dalquist, 2009 Spring Framework
Spring Framework 3.0.M3 Reference Documentation, by Rod Johnson, et. al, 2009 Spring Framework

No comments:

Post a Comment