Friday, June 07, 2013

javase Hibernate: goodbye Ejb3Configuration, hello JNDI Context mock

I've been cleaning up littleware's asset database code this week, and was dismayed to discover upon updating the hibernate dependencies that the latest hibernate releases have deprecated and removed the Ejb3Configuration mechanism for bootstrapping a JPA environment and allocating an EntityManagerFactory in a javase (no javaee container) application. Fortunately - I came up with a hacky way to trick hibernate into using the DataSource instance I want it to use.

Somehow when I started working with JPA I already had code in littleware's guice-based IOC setup to initialize and inject DataSource instances in traditional JDBC code either directly or via JNDI lookup. When I started using JPA in littleware I wanted to wire things up so that JPA used the same DataSource as the rest of the code. When running in a web container like Tomcat or Glassfish the app can let the container manage the DataSource, and both JPA and the guice-runtime access the same DataSource via a directory (JNDI) lookup, but when running a standalone application, I had things coded up to use hibernate, and used Ejb3Configuration to tell hibernate to use littleware's DataSource.

Anyway, with Ejb3Configuration going away I had to specify the database connection parameters in a JPA persistence.xml file in one of two ways. The first option was to specify properties for a JDBC Driver that JPA (hibernate) would wrap with its own connection pool manager, and the first hack I tried implemented a JDBC Driver (it's just a one method interface) that pulled connections from littleware's DataSource. That actually worked fine - I'm always amazed when these things work. I setup persistence.xml like this:

<?xml version="1.0" encoding="UTF-8"?>

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">

  <persistence-unit name="littlewarePU" transaction-type="RESOURCE_LOCAL">
        <description>This JPA persistence unit tracks littleware assets
        </description>

        <!-- jndi datasource lookup        
        <non-jta-data-source>jdbc/littleDB</non-jta-data-source>
        -->
        
        <class>littleware.asset.server.db.jpa.AssetTypeEntity</class>
        <class>littleware.asset.server.db.jpa.TransactionEntity</class>
        <class>littleware.asset.server.db.jpa.AssetAttribute</class>
        <class>littleware.asset.server.db.jpa.AssetDate</class>
        <class>littleware.asset.server.db.jpa.AssetLink</class>
        <class>littleware.asset.server.db.jpa.AssetEntity</class>

    <properties>

      <property name="eclipselink.target-database" value="DERBY"/>
      <property name="eclipselink.ddl-generation" value="create-tables" />
      <property name="hibernate.hbm2ddl.auto" value="create"/>
      <property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect" />
            
      <property name="javax.persistence.jdbc.driver" value="littleware.asset.server.db.jpa.LittleDriver"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:littleware://ignore/this/stuff"/>
      <property name="javax.persistence.jdbc.user" value="APP"/>
      <property name="javax.persistence.jdbc.password" value="APP"/>

    </properties> 
    
  </persistence-unit>
</persistence>

And I wrote a bogus littleware.asset.server.db.jpa.LittleDriver that some initialization code configured via its setDataSource() method:


/**
 * JDBC driver that just delegates to the active DataSource defined in the
 * active littleware runtime.
 * Register this driver with JPA persistence.xml to plug into the
 * littleware managed DataSource.
 */
public class LittleDriver implements Driver {
    private static  DataSource dataSource = null;
    /**
     * HibernateProvider injects littleware data source at startup time as needed
     */
    public static void setDataSource( DataSource value ) {
        dataSource = value;
    }
    
    
    @Override
    public Connection connect(String string, Properties prprts) throws SQLException {
        Whatever.get().check( "LittleDriver requires data source injection",  null != dataSource );
        return dataSource.getConnection();
    }

    @Override
    public boolean acceptsURL(String string) throws SQLException {
        return true;
    }

    private static final DriverPropertyInfo[] empty = new DriverPropertyInfo[0];
    @Override
    public DriverPropertyInfo[] getPropertyInfo(String string, Properties prprts) throws SQLException {
        return empty;
    }

    @Override
    public int getMajorVersion() {
        return 0;
    }

    @Override
    public int getMinorVersion() {
        return 0;
    }

    @Override
    public boolean jdbcCompliant() {
        return false;
    }

    private static final Logger log = Logger.getLogger( LittleDriver.class.getName() );
    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return log;
    }
    
}

Anyway - that worked great, and it's actually good enough for my current needs (mostly just junit tests), but it made me itch thinking about hibernate wrapping a connection pool around a mock driver that pulls connections from another connection pool. Ugh. So I started thinking about setting up an in-memory JNDI directory where initialization code could stuff the DataSource before allocating the JPA (hibernate or whatever) EntityManagerFactory. I found this cool little SimpleJNDI JNDI implementation, but it wasn't registered with Maven central, and it was a little bigger than I would like to copy into my code base, and anyway - I didn't need a whole JNDI implementation - I just needed to trick hibernate into using my DataSource, so I tried just wiring up a mock JNDI Context, and it worked! It took a few tries to figure out which methods hibernate calls to do its directory lookup, but in the end I wound up with a persistence.xml file like this:

<?xml version="1.0" encoding="UTF-8"?>

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">

  <persistence-unit name="littlewarePU" transaction-type="RESOURCE_LOCAL">
        <description>This JPA persistence unit tracks littleware assets
        </description>

        <non-jta-data-source>jdbc/littleDB</non-jta-data-source>
        
        <class>littleware.asset.server.db.jpa.AssetTypeEntity</class>
        <class>littleware.asset.server.db.jpa.TransactionEntity</class>
        <class>littleware.asset.server.db.jpa.AssetAttribute</class>
        <class>littleware.asset.server.db.jpa.AssetDate</class>
        <class>littleware.asset.server.db.jpa.AssetLink</class>
        <class>littleware.asset.server.db.jpa.AssetEntity</class>

    <properties>
    </properties> 
    
  </persistence-unit>
</persistence>

The mock JNDI Context looks like this:

/**
 * Mock JNDI context that is just NOOPs except
 * lookup always returns the DataSource injected via
 * the setDataSource static method.
 * Similar to LittleDriver - just a hack to try to get
 * hibernate to use our DataSource
 */
public class LittleContext implements javax.naming.Context {
    private static  DataSource dataSource = null;
    /**
     * HibernateProvider injects littleware data source at startup time as needed
     */
    public static void setDataSource( DataSource value ) {
        dataSource = value;
    }

    @Override
    public Object lookup(Name name) throws NamingException {
        return dataSource;
    }

    @Override
    public Object lookup(String string) throws NamingException {
        return dataSource;
    }

    @Override
    public void bind(Name name, Object o) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void bind(String string, Object o) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void rebind(Name name, Object o) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void rebind(String string, Object o) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void unbind(Name name) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void unbind(String string) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void rename(Name name, Name name1) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void rename(String string, String string1) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public NamingEnumeration<NameClassPair> list(Name name) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public NamingEnumeration<NameClassPair> list(String string) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public NamingEnumeration<Binding> listBindings(Name name) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public NamingEnumeration<Binding> listBindings(String string) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void destroySubcontext(Name name) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void destroySubcontext(String string) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public Context createSubcontext(Name name) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public Context createSubcontext(String string) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public Object lookupLink(Name name) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public Object lookupLink(String string) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public NameParser getNameParser(Name name) throws NamingException {
        return getNameParser("");
    }

    @Override
    public NameParser getNameParser( String string) throws NamingException {
        return new NameParser(){

            @Override
            public Name parse( final String string) throws NamingException {
                return new Name(){
                    @Override
                    public Object clone() { return this; }
                    
                    @Override
                    public int compareTo(Object o) {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public int size() {
                        return 1;
                    }

                    @Override
                    public boolean isEmpty() {
                        return false;
                    }

                    @Override
                    public Enumeration<String> getAll() {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public String get(int i) {
                        return string;
                    }

                    @Override
                    public Name getPrefix(int i) {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public Name getSuffix(int i) {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public boolean startsWith(Name name) {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public boolean endsWith(Name name) {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public Name addAll(Name name) throws InvalidNameException {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public Name addAll(int i, Name name) throws InvalidNameException {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public Name add(String string) throws InvalidNameException {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public Name add(int i, String string) throws InvalidNameException {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }

                    @Override
                    public Object remove(int i) throws InvalidNameException {
                        throw new UnsupportedOperationException("Not supported yet."); 
                    }
                };

            }
        };
    }

    @Override
    public Name composeName(Name name, Name name1) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public String composeName(String string, String string1) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public Object addToEnvironment(String string, Object o) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public Object removeFromEnvironment(String string) throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public Hashtable<?, ?> getEnvironment() throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    @Override
    public void close() throws NamingException {}

    @Override
    public String getNameInNamespace() throws NamingException {
        throw new UnsupportedOperationException("Not supported yet."); 
    }

    
    //---------------------------
    
    /**
     * Assign this to the "java.naming.factory.initial" system property
     * to make the LittleContext mock the initial context.
     */
    public static class Factory implements javax.naming.spi.InitialContextFactory {

        @Override
        public Context getInitialContext(Hashtable<?, ?> hshtbl) throws NamingException {
            return new LittleContext();
        }
        
    }
}

This is the JPA EntityManager guice Provider used in a standalone application:

@Singleton
public class HibernateProvider implements Provider<EntityManagerFactory> {

    private final DataSource dataSource;
    private final String dataSourceURL;
    private EntityManagerFactory emFactory = null;


    @Inject
    public HibernateProvider(@Named("datasource.littleware") DataSource dsource,
            @Named("datasource.littleware") String sDatasourceUrl) {
        dataSource = dsource;
        dataSourceURL = sDatasourceUrl;
    }


    @Override
    public EntityManagerFactory get() {
        if (null == emFactory) {
            LittleDriver.setDataSource( dataSource );
            LittleContext.setDataSource( dataSource );
            if ( null == System.getProperty(  "java.naming.factory.initial" ) ) {
                System.setProperty( "java.naming.factory.initial", LittleContext.Factory.class.getName() );
            }
            
            emFactory = Persistence.createEntityManagerFactory( "littlewarePU" );
        }
        return emFactory;
    }
}

Anyway - I barely know how to use JPA, so duplicate these hacks at your own risk. The code is online here for now (I'm always moving things around in that repo).

Post a Comment