Enterprise Java

JNDI and JPA without J2EE Container

We wanted to test some JPA code with as simple setup as possible. The plan was to use only Java and Maven, without an application server or other J2EE container.

Our JPA configuration needs two things to run successfully:

  • database to store data,
  • JNDI to access the database.

This post has two parts. First part shows how to use standalone JNDI and an embedded in-memory database in test. Remaining chapters explain how the solution works.

All used code is available on Github. If you are interested in the solution but do not want to read the explanations, download the project from Github and read only the first chapter.

JPA Test

This chapter shows how to use our code to enable standalone JNDI and embedded in-memory database in tests. How and why the solution works is explained in the rest of this post.

The solution has three ‘API’ classes:

  • JNDIUtil – JNDI initialization, clean up and some convenience methods,
  • InMemoryDBUtil – database and data source creation/removal,
  • AbstractTestCase – database clean up before first test and JNDI clean up before each test.

We use Liquibase to maintain the database structure. If you do not wish to use Liquibase, you will have to customize the class InMemoryDBUtil. Tweak the method createDatabaseStructure to do what you need.

Liquibase keeps list of all needed database changes in a file named changelog. Unless configured otherwise, each change runs only once. Even if the changelog file is applied multiple times to the same database.

Usage

Any test case extended from the AbstractTestCase will:

  • drop the database before first test,
  • install standalone JNDI or delete all data stored in it before each test,
  • run Liquibase changelog against the database before each test.

JPA test case must extend AbstractTestCase and override the getInitialChangeLog method. The method should return changelog file location.

public class DemoJPATest extends AbstractTestCase {

  private static final String CHANGELOG_LOCATION = "src/test/java/org/meri/jpa/simplest/db.changelog.xml";
  private static EntityManagerFactory factory;

  public DemoJPATest() {
  }

  @Override
  protected String getInitialChangeLog() {
    return CHANGELOG_LOCATION;
  }

  @Test
  @SuppressWarnings("unchecked")
  public void testJPA() {
    EntityManager em = factory.createEntityManager();

    Query query = em.createQuery("SELECT x FROM Person x");
    List<Person> allUsers = query.getResultList();
    em.close();

    assertFalse(allUsers.isEmpty());
  }

  @BeforeClass
  public static void createFactory() {
    factory = Persistence.createEntityManagerFactory("Simplest");
  }

  @AfterClass
  public static void closeFactory() {
    factory.close();
  }

}

Note: it would be cleaner to drop the database before each test. However, drop and recreate db structure is costly operation. It would slow down the test case too much. Doing it only before the class seems to be reasonable compromise.

While the database is dropped only once, the changelog is run before each test. It may seems like a waste, but this solution has some advantages. First, the getInitialChangeLog method does not have to be static and may be overridden in each test. Second, changes configured to ‘runAlways’ will run before each test and thus may contain some cheap clean up or other initialization.

JNDI

This chapter explains what JNDI is, how it is used and how to configure it. If you are not interested in theory, skip to the next chapter. Standalone JNDI is created there.

Basic Usage

JNDI allows clients to store and look up data and objects via a name. The data store is accessed through an implementation of the interface Context.

The following code shows how to store data in JNDI:

Context ctx = new InitialContext();
ctx.bind("jndiName", "value");
ctx.close();

Second piece of code shows how to look for things in JNDI:

Context ctx = new InitialContext();
Object result = ctx.lookup("jndiName");
ctx.close();

Try to run the above without a J2EE container and you will get an error:

javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file:  java.naming.factory.initial
 at javax.naming.spi.NamingManager.getInitialContext(Unknown Source)
 at javax.naming.InitialContext.getDefaultInitCtx(Unknown Source)
 at javax.naming.InitialContext.getURLOrDefaultInitCtx(Unknown Source)
 at javax.naming.InitialContext.bind(Unknown Source)
 at org.meri.jpa.JNDITestCase.test(JNDITestCase.java:16)
 at ...

The code does not work because InitialContext class is not a real data store. The InitialContext class is only able to find another instance of the Context interface and delegates all work to it. It is not able to store data nor to find them.

Context Factories

The real context, the one that does all the work and is able to store/find data, has to be created by a context factory. This section shows how to create a context factory and how to configure InitialContext to use it.

Each context factory must implement InitialContextFactory interface and must have a no-argument constructor:

package org.meri.jpa.jndi;

public class MyContextFactory implements InitialContextFactory {

 @Override
 public Context getInitialContext(Hashtable environment) throws NamingException {
  return new MyContext();
 }
 
}

Our factory returns a simple context called MyContext. Its lookup method always returns a string “stored value”:

class MyContext implements Context {

 @Override
 public Object lookup(Name name) throws NamingException {
  return "stored value";
 }

 @Override
 public Object lookup(String name) throws NamingException {
  return "stored value";
 }

 .. the rest ...
}

JNDI configuration is passed between classes in a hash table. The key always contains property name and the value contains the property value. As the initial context constructor InitialContext() has no parameter, an empty hash table is assumed. The class has also an alternative constructor which takes configuration properties hash table as a parameter.

Use the property "java.naming.factory.initial" to specify context factory class name. The property is defined in Context.INITIAL_CONTEXT_FACTORY constant.

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "className");

Context ctx = new InitialContext(environnement);

Next test configures MyContextFactory and checks whether created initial context returns “stored value” no matter what:

@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void testDummyContext() throws NamingException {
 Hashtable environnement = new Hashtable();
 environnement.put(Context.INITIAL_CONTEXT_FACTORY, "org.meri.jpa.jndi.MyContextFactory");

 Context ctx = new InitialContext(environnement);
 Object value = ctx.lookup("jndiName");
 ctx.close();

 assertEquals("stored value", value);
}

Of course, this works only if you can supply hash table with custom properties to the initial context constructor. That is often impossible. Most libraries use no-argument constructor shown in the beginning. They assume that initial context class has default context factory available and that no-argument constructor will use that one.

Naming Manager

Initial context uses NamingManager to create a real context. Naming manager has a static method getInitialContext(Hashtable env) which returns an instance of a context. The parameter env contains a configuration properties used to build the context.

By default, naming manager reads Context.INITIAL_CONTEXT_FACTORY from the env hash table and creates an instance of specified initial context factory. The factory method then creates a new context instance. If that property is not set, naming manager throws an exception.

It is possible to customize naming managers behavior. The class NamingManager has method setInitialContextFactoryBuilder. If the initial context factory builder is set, naming manager will use it to create context factories.

You can use this method only once. Installed context factory builder can not be changed.

try {
  MyContextFactoryBuilder builder = new MyContextFactoryBuilder();
  NamingManager.setInitialContextFactoryBuilder(builder);
} catch (NamingException e) {
  // handle exception
}

Initial context factory builder must implement InitialContextFactoryBuilder interface. The interface is simple. It has only one method InitialContextFactory createInitialContextFactory(Hashtable env).

Summary

In short, initial context delegates a real context initialization to naming manager which delegates it to context factory. Context factory is created by an instance of initial context factory builder.


Standalone JNDI

We will create and install standalone JNDI implementation. The entry point to our standalone JNDI implementation is the class JNDIUtil.

Three things are needed to enable JNDI without an application server:

  • an implementation of Context and InitialContextFactory interfaces,
  • an implementation of InitialContextFactoryBuilder interface,
  • initial context factory builder installation and ability to clean all stored data.

Context and Factory

We took SimpleJNDI implementation from osjava project and modified it to suit our needs better. The project uses a new BSD license.

Add SimpleJNDI maven dependency into pom.xml:

 
 
  
   simple-jndi
  
 
  
   simple-jndi
  
 
  
   0.11.4.1
  

 

SimpleJNDI comes with a MemoryContext context which lives exclusively in the memory. It requires almost no configuration and its state is never saved of loaded. It does almost what we need, except two things:

  • its close() method deletes all stored data,
  • each instance uses its own storage by default.

Most libraries assume that the close method optimizes resources. They tend to call it each time they load or store data. If the close method deletes all data right after they have been stored, the context is useless. We have to extend the MemoryContext class and override the close method:

@SuppressWarnings({"rawtypes"}) 
public class CloseSafeMemoryContext extends MemoryContext {

 public CloseSafeMemoryContext(Hashtable env) {
  super(env);
 }

 @Override
 public void close() throws NamingException {
  // Original context lost all data on close();
  // That made it unusable for my tests. 
 }

}

By convention, the builder/factory system creates new context instance for each use. If they do not share data, JNDI can not be used to transfer data between different libraries.

Fortunately, this problem has also an easy solution. If the environnement hash table contains property "org.osjava.sj.jndi.shared" with value "true", created memory context will use common static storage. Therefore, our initial context factory creates CloseSafeMemoryContext instances and configures them to use common storage:

public class CloseSafeMemoryContextFactory implements InitialContextFactory {

 private static final String SHARE_DATA_PROPERTY = "org.osjava.sj.jndi.shared";

 public Context getInitialContext(Hashtable environment) throws NamingException {

  // clone the environnement
  Hashtable sharingEnv = (Hashtable) environment.clone();

  // all instances will share stored data
  if (!sharingEnv.containsKey(SHARE_DATA_PROPERTY)) {
   sharingEnv.put(SHARE_DATA_PROPERTY, "true");
  }
  
  return new CloseSafeMemoryContext(sharingEnv);;
 }

}

Initial Context Factory Builder

Our builder acts almost the same way as the original naming manager implementation. If the property Context.INITIAL_CONTEXT_FACTORY is present in the incoming environment, specified factory is created.

However, if this property is missing, the builder creates an instance of CloseSafeMemoryContextFactory. The original naming manager would throw an exception.

Our implementation of the InitialContextFactoryBuilder interface:

public InitialContextFactory createInitialContextFactory(Hashtable env) throws NamingException {
 String requestedFactory = null;
 if (env!=null) {
  requestedFactory = (String) env.get(Context.INITIAL_CONTEXT_FACTORY);
 }

 if (requestedFactory != null) {
  return simulateBuilderlessNamingManager(requestedFactory);
 }
 
 return new CloseSafeMemoryContextFactory();
}

The method simulateBuilderlessNamingManager uses class loader to load requested context factory:

private InitialContextFactory simulateBuilderlessNamingManager(String requestedFactory) throws NoInitialContextException {
  try {
    ClassLoader cl = getContextClassLoader();
    Class requestedClass = Class.forName(className, true, cl);
    return (InitialContextFactory) requestedClass.newInstance();
  } catch (Exception e) {
    NoInitialContextException ne = new NoInitialContextException(...);
    ne.setRootCause(e);
    throw ne;
  }
}

private ClassLoader getContextClassLoader() {
 return (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
  public Object run() {
   return Thread.currentThread().getContextClassLoader();
  }
 });
}

Builder Installation and Context Cleaning

Finally, we have to install context factory builder. As we wanted to use standalone JNDI in tests, we needed also a method to clean up all stored data between tests. Both is done inside the method initializeJNDI which will run before each test:

public class JNDIUtil {

 public void initializeJNDI() {
  if (jndiInitialized()) {
   cleanAllInMemoryData();
  } else {
   installDefaultContextFactoryBuilder();
  }
 }

}

JNDI is initialized if the default context factory builder has been set already:

private boolean jndiInitialized() {
  return NamingManager.hasInitialContextFactoryBuilder();
 }

Installation of the default context factory builder:

private void installDefaultContextFactoryBuilder() {
  try {
    NamingManager.setInitialContextFactoryBuilder(new ImMemoryDefaultContextFactoryBuilder());
  } catch (NamingException e) {
    //We can not solve the problem. We will let it go up without
    //having to declare the exception every time.
    throw new ConfigurationException(e);
  }
}

Use the original implementation of the method close in MemoryContext class to clean up stored data:

private void cleanAllInMemoryData() {
  CleanerContext cleaner = new CleanerContext();
  try {
      cleaner.close();
  } catch (NamingException e) {
    throw new RuntimeException("Memory context cleaning failed:", e);
  }
}

class CleanerContext extends MemoryContext {
 
  private static Hashtable environnement = new Hashtable();
  static {
    environnement.put("org.osjava.sj.jndi.shared", "true");
  }

  public CleanerContext() {
    super(environnement);
  }

}


In-Memory Database

Apache Derby is an open source relational database implemented in Java. It is available under Apache License, Version 2.0. Derby is able to run in embedded mode. Embedded database data are stored either on the filesystem or in the memory.

Maven dependency for Derby:

  
 
   
    org.apache.derby
   
 
   
    derby
   
 
   
    10.8.2.2
   

  

Create DataSource

Use an instance of the EmbeddedDatasource class to connect to the database. The data source will use an in-memory instance whenever the database name starts with “memory:”.

Following code creates data source pointing to an instance of in-memory database. If the database does not exist yet, it will be created:

private EmbeddedDataSource createDataSource() {
 EmbeddedDataSource dataSource = new EmbeddedDataSource();
 dataSource.setDataSourceName(dataSourceJndiName);
 dataSource.setDatabaseName("memory:" + databaseName);
 dataSource.setCreateDatabase("create");

 return dataSource;
}

Drop Database

The easiest way to clean up the database is to drop and recreate it. Create an instance of embedded data source, set the connection attribute “drop” to “true” and call its getConnection method. It will drop the database and throw an exception.

private static final String DATABASE_NOT_FOUND = "XJ004";

 private void dropDatabase() {
  EmbeddedDataSource dataSource = createDataSource();
  dataSource.setCreateDatabase(null);
  dataSource.setConnectionAttributes("drop=true");

  try {
   //drop the database; not the nicest solution, but works
   dataSource.getConnection();
  } catch (SQLNonTransientConnectionException e) {
   //this is OK, database was dropped
  } catch (SQLException e) {
   if (DATABASE_NOT_FOUND.equals(e.getSQLState())) {
    //attempt to drop non-existend database
    //we will ignore this error
    return ; 
   }
   throw new ConfigurationException("Could not drop database.", e);
  }
 }


Database Structure

We used Liquibase to create the database structure and test data. The database structure is kept in a so called changelog file. It is an xml file, but you can include DDL or SQL code if you do not feel like learning yet another xml language.

Liquibase and its advantages are out of scope of this article. The most relevant advantage for this demo is its ability to run the same changelog against the same database multiple times. Each run applies only new changes to the database. If the file did not changed, nothing happens.

You can add the changelog to the jar or war and run it on each application start up. That will ensure that the database is always updated to the latest version. No configuration or installation scripts are necessary.

Add Liquibase dependency to the pom.xml:

  
 
   
    org.liquibase
   
 
   
    liquibase-core
   
 
   
    2.0.3
   

  

Following changelog creates one table named Person and puts one entry ‘slash – Simon Worth’ into it:

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.9"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.9
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.9.xsd">

 <changeSet id="1" author="meri">
  <comment>Create table structure for users and shared items.</comment>

  <createTable tableName="person">
   <column name="user_id" type="integer">
    <constraints primaryKey="true" nullable="false" />
   </column>
   <column name="username" type="varchar(1500)">
    <constraints unique="true" nullable="false" />
   </column>
   <column name="firstname" type="varchar(1500)"/>
   <column name="lastname" type="varchar(1500)"/>
   <column name="homepage" type="varchar(1500)"/>
   <column name="about" type="varchar(1500)"/>
  </createTable>
 </changeSet>

 <changeSet id="2" author="meri" context="test">
  <comment>Add some test data.</comment>
  <insert tableName="person">
   <column name="user_id" valueNumeric="1" />
   <column name="userName" value="slash" />
   <column name="firstName" value="Simon" />
   <column name="lastName" value="Worth" />
   <column name="homePage" value="http://www.slash.blogs.net" />
   <column name="about" value="I like nature and writing my blog. The blog contains my opinions about everything." />
  </insert>
 </changeSet>

</databaseChangeLog>

Liquibase use is pretty straightforward. Use data source to create new Liquibase instance, run its update method and handle all declared exceptions:

private void initializeDatabase(String changelogPath, DataSource dataSource) {
  try {
   //create new liquibase instance
   Connection sqlConnection = dataSource.getConnection();
   DatabaseConnection db = new DerbyConnection(sqlConnection);
   Liquibase liquibase = new Liquibase(changelogPath, new FileSystemResourceAccessor(), db);

   //update the database
   liquibase.update("test");

  } catch (SQLException e) {
   // We can not solve the problem. We will let it go up without
   // having to declare the exception every time.
   throw new ConfigurationException(DB_INITIALIZATION_ERROR, e);
  } catch (LiquibaseException e) {
   // We can not solve the problem. We will let it go up without
   // having to declare the exception every time.
   throw new ConfigurationException(DB_INITIALIZATION_ERROR, e);
  }
 }


End

Both standalone JNDI and embedded in-memory database are up and running each time we run our tests. While the JNDI set up is probably universal, the database construction will probably need project specific modifications.

Feel free to download sample project from Github and use/modify whatever you find useful.

Reference: Running JNDI and JPA Without J2EE Container from our JCG partner Maria Jurcovicova at the This is Stuff blog.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Dimitri Kolbasov
Dimitri Kolbasov
11 years ago

Finally a clear explanation of all that. Sun did it best to confuse everybody. You guys make it clear. Thanks.

Back to top button