/** * Copyright 2011 meltmedia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.xchain.framework.hibernate; import java.util.Properties; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import org.hibernate.HibernateException; import org.hibernate.cfg.Environment; import org.hibernate.connection.ConnectionProvider; import org.hibernate.util.NamingHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.SQLException; /** * A data source connection provider that will rebind the data source if it fails to create a connection. When using the 'hibernate.connection.datasource' property, XChains will use this * connection provider, unless the property 'hibernate.connection.provider_class' has been specified in the configuration. * * @author Christian Trimble * @author Josh Kennedy */ public class RebindingDataSourceConnectionProvider implements ConnectionProvider { /** The log for this data source provider. */ private static Logger log = LoggerFactory.getLogger(RebindingDataSourceConnectionProvider.class); /** The jndi name of the data source. */ private String jndiName = null; /** The user name for creating connections. */ private String userName = null; /** The password for creating connections. */ private String password = null; /** The initial context for looking up data sources. */ private InitialContext initialContext = null; /** The current data source for creating connections. */ private DataSource dataSource = null; /** * Configures this connection provider with the specified properties. This method will only fail with a hibernate exception if the 'hibernate.connection.datasource' property is not specified, * or if the JNDI initial context cannot be created. Otherwise, this method will log any problems with the configuration, or looking up the data source and return normally. * * @throws HibernateException if the property 'hibernate.connection.datasource' property is not specified, or if the JNDI initial context cannot be created. */ public void configure(Properties properties) throws HibernateException { jndiName = properties.getProperty( Environment.DATASOURCE ); userName = properties.getProperty( Environment.USER ); password = properties.getProperty( Environment.PASS ); if( jndiName == null ) { if( log.isErrorEnabled() ) { log.error("The JNDI name of the JDBC data source was not specified. Please set the hibernate property '"+Environment.DATASOURCE+"'."); } throw new HibernateException("The JNDI name of the JDBC data source was not specified. Please set the hibernate property '"+Environment.DATASOURCE+"'."); } if( userName != null && password == null && log.isWarnEnabled() ) { log.warn("A JDBC user name and password will not be used because the hibernate property '"+Environment.PASS+"' is not set."); } if( userName == null && password != null && log.isWarnEnabled() ) { log.warn("A JDBC user name and password will not be used because the hibernate property '"+Environment.USER+"' is not set."); } try { initialContext = NamingHelper.getInitialContext(properties); } catch( NamingException ne ) { throw new HibernateException("Could not create initial context for looking up datasource.", ne); } try { initializeDataSource(); } catch( SQLException sqle ) { // this excetion has been logged. let startup continue, as the application will start responding once the configuration issues are resolved. } } /** * Initialized, or reinitializes, the data source for this connection provider. This method logs any errors that prevented the initialization of the data source and will only update * the datasource reference if a new datasource could be obtained. * * @throws SQLException if the data source could not be loaded from JNDI for any reason. */ private void initializeDataSource() throws SQLException { Object jndiObject = null; // Look up the object bound to the jndi name. try { jndiObject = initialContext.lookup(jndiName); } catch( NamingException ne ) { if( log.isErrorEnabled() ) { log.error("A JNDI exception was thrown while looking up the JDBC data source at '"+jndiName+"'.", ne); } throw new SQLException("A JNDI exception was thrown while looking up the JDBC data source at '"+jndiName+"'."); } // If we did not find an object, bail out. if( jndiObject == null ) { if( log.isErrorEnabled() ) { log.error("There is not a javax.sql.DataSource bound to the JNDI name '"+jndiName+"'. This JNDI name is not bound to an object."); } throw new SQLException("There is not a javax.sql.DataSource bound to the JNDI name '"+jndiName+"'. This JNDI name is not bound to an object."); } // if the object is not a data source, bail out. if( !(jndiObject instanceof DataSource) ) { if( log.isErrorEnabled() ) { log.error("There is not a javax.sql.DataSource bound to the JNDI name '"+jndiName+"'. This JNDI name is currently bound to an object of type '"+jndiObject.getClass().getName()+"'."); } throw new SQLException("There is not a javax.sql.DataSource bound to the JNDI name '"+jndiName+"'. This JNDI name is currently bound to an object of type '"+jndiObject.getClass().getName()+"'."); } // We are ready to set this data source. dataSource = (DataSource)jndiObject; } /** * Returns a connection from the currently configured JDBC data source. * * @throws SQLException if the data source is null, or if the data source throws an exception while creating the connection. * @return a connection from the currently configured JDBC data source. */ private Connection getConnectionFromDataSource() throws SQLException { if( dataSource == null ) { throw new SQLException("A JDBC data source could not be found at JNDI name '"+jndiName+"'."); } if( userName != null && password != null ) { return dataSource.getConnection(userName, password); } else { return dataSource.getConnection(); } } /** * Gets a connection from the datasource bound to jndi. If getting the connection fails, this method will attempt to rebind the datasource once, then fail if rebinding fails or the newly bound * DataSource fails. This method is synchronized, so that not more than one thread attempts to rebind the datasource at a time. This should not cause a major performance hit, since the datasource * pool will most likely be synchronized as well. * * @throws SQLException if a fresh copy of the datasource from JNDI caused an exception to be thrown. * @return a connection to the data source, creating a new data source if required. */ public synchronized Connection getConnection() throws SQLException { try { return getConnectionFromDataSource(); } catch( Exception exception ) { if( log.isWarnEnabled() ) { log.warn("Attempting to rebind to the jndi datasource due to an exception.", exception); } initializeDataSource(); return getConnectionFromDataSource(); } } /** * Closes the specified connection. * * @param connection the connection to close. * @throws SQLException if an exception is thrown while closing the connection. */ public void closeConnection( Connection connection ) throws SQLException { connection.close(); } /** * Closes this connection provider. */ public void close() { initialContext = null; dataSource = null; jndiName = null; userName = null; password = null; } public boolean supportsAggressiveRelease() { return true; } }