Wednesday, December 3, 2008

Injecting JNDI datasource for JUnit test cases in Hibernate/JBoss environment


Recently a project in which I am working with had faced a little bit nuisance in having to maintain a seperate set of hibernate configuration for JUnit test cases.

The application is using JBoss application server, so the hibernate.cfg.xml has data source and transaction lookup class configuration specific to JBoss.

Since JUnit test cases are run outside the container, it had to be replaced with hibernate's default transaction and Database Connection pool configurations.

Snippet of Production configuration


<property name="hibernate.connection.datasource">java:/mysqldatasource</property>
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.transaction.factory_class">org.hibernate.transaction.JTATransactionFactory</property>
<property name="transaction.manager_lookup_class">org.hibernate.transaction.JBossTransactionManagerLookup</property>

What was used for the JUnit run

<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mysqldb</property>
<property name="hibernate.connection.username">someuser</property>
<property name="hibernate.connection.password">somepwd</property>

The problem with using the actual configuration is that for datasource and transaction manager jndi lookup, the respective object must be bound in a JNDI server.

The following blog explains how to do it.



I am going explain further in JBoss context.

It is a common practice that when Hibernate is used with JBoss the transaction manager used will be that of JBoss. This is done in Hibernate using the following hibernate properties.

 <property name="hibernate.transaction.factory_class">org.hibernate.transaction.JTATransactionFactory</property>
<property name="transaction.manager_lookup_class">org.hibernate.transaction.JBossTransactionManagerLookup</property>

JBossTransactionManagerLooup class in Hibernate does two jndi looups using "java:/TransactionManager" and "UserTransaction" for obvious objects. This necessitates that TransactionManager and UserTransaction implementation from JBoss has to be bound against those names respectively.

A few minutes of searching says following are the classes

import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple;
import com.arjuna.ats.internal.jta.transaction.arjunacore.UserTransactionImple;

It possed another issue. Both of them are not Serializable and transient objects can not be bound to JNDI!!.

Well the only solution I found was to create dummy extesion classes and implement Serializable interface






//Created to avoid serialization error while binding to the jndi
private static class CustomTXNManager extends TransactionManagerImple implements Serializable {

private static final long serialVersionUID = 1L;

public CustomTXNManager() {
}
}

// Created to avoid serialization error while binding to the jndi
private static class CustomUserTransaction extends UserTransactionImple implements Serializable {

private static final long serialVersionUID = 1L;

public CustomUserTransaction() {
}
}




TransactionManager tm = new CustomTXNManager();
ic.bind("java:/TransactionManager", tm);

UserTransaction ut = new CustomUserTransaction();
ic.bind("UserTransaction",ut);

These can be included in a base JUnit class with a static block. The properties can be externalized in a properties file to reduce the hard coded parts. Below is the entire static block declaration and a static util method I have used.


    static {

try {
// Create initial context

Properties props = new Properties();
props.load(AbstractTestCase.class.getClassLoader()
.getResourceAsStream("mytest.properties"));

System.setProperty(Context.URL_PKG_PREFIXES, props
.getProperty("java.naming.factory.url.pkgs"));

System.setProperty(Context.INITIAL_CONTEXT_FACTORY, props
.getProperty("java.naming.factory.initial"));

// to avoid discoverServer error
NamingServer server = new NamingServer();
NamingContext.setLocal(server);

// Construct DataSource
MysqlConnectionPoolDataSource ds = new MysqlConnectionPoolDataSource();
// For Oracle database use the following (no common interface, hence
// can't generalize; well reflection would help)
// OracleConnectionPoolDataSource ds = new
// OracleConnectionPoolDataSource();
ds.setURL(props.getProperty("ds.url"));
ds.setUser(props.getProperty("ds.username"));
ds.setPassword(props.getProperty("ds.password"));

InitialContext ic = new InitialContext();

String dsName = props.getProperty("ds.name");

String[] cxts = dsName.split("/");

String inCxt = cxts[0];
createSubcontext(ic, inCxt);
for (int i = 1; i < cxts.length - 1; i++) {
// if the data source name is like java:/comp/mysqldatasource
// this takes care of creating subcontexts in jndi
inCxt = inCxt + "/" + cxts[i];
createSubcontext(ic, inCxt);
}

ic.bind(dsName, ds);

// The following binding is done to support the hibernate properties
// hibernate.transaction.factory_class and transaction.manager_lookup_class

// the following requires JBoss dependent class. May be sth can be done
// to generalize this
TransactionManager tm = new CustomTXNManager();
ic.bind("java:/TransactionManager", tm);

UserTransaction ut = new CustomUserTransaction();
ic.bind("UserTransaction",ut);

} catch (Exception e) {
// what can be done?
e.printStackTrace();
}

}

// copied from org.jboss.naming.Util
private static Context createSubcontext(Context ctx, String cxtName)
throws NamingException {
//System.out.println(" creating subcontext " + cxtName);
Context subctx = ctx;
Name name = ctx.getNameParser("").parse(cxtName);
for (int pos = 0; pos < name.size(); pos++) {
String ctxName = name.get(pos);
try {
subctx = (Context) ctx.lookup(ctxName);
} catch (NameNotFoundException e) {
subctx = ctx.createSubcontext(ctxName);
}
// The current subctx will be the ctx for the next name component
ctx = subctx;
}
return subctx;
}


Property file contents

ds.name=java:/mysqldatasource
ds.url=jdbc:mysql://localhost:3306/mysqldb
ds.username=someuser
ds.password=somepwd
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming

Hope this information helps solving some of the issues you are facing. If you need some clarifications let me know.