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.
<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.
- 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.
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 , , 2009 Spring Framework
Spring Framework 3.0.M3 Reference Documentation, by Rod Johnson, et. al, 2009 Spring Framework