/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.hibernate.jmx;

import org.jboss.system.ServiceMBeanSupport;
import org.jboss.hibernate.cache.DeployedTreeCacheProvider;
import org.jboss.logging.Logger;
import org.jboss.naming.Util;

import java.net.URL;
import java.io.File;
import java.util.Properties;

import javax.management.Notification;
import javax.management.ObjectName;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.hibernate.cfg.Environment;
import org.hibernate.cfg.Configuration;
import org.hibernate.transaction.JBossTransactionManagerLookup;
import org.hibernate.transaction.JTATransactionFactory;
import org.hibernate.SessionFactory;
import org.hibernate.Interceptor;
import org.hibernate.HibernateException;

/**
 * A Hibernate service.  Configures a {@link org.hibernate.SessionFactory}
 * instance as an MBean and exposes it through JNDI.  Meant to work in
 * conjuction with the {@link org.jboss.hibernate.har.HARDeployer}.
 *
 * @jmx:mbean
 *    name="jboss.har:service=Hibernate"
 *    extends="org.jboss.system.ServiceMBean, javax.management.MBeanRegistration"
 *
 * @version <tt>$Revision: 1.3.2.10 $</tt>
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @author <a href="mailto:gavin@hibernate.org">Gavin King</a>
 * @author <a href="mailto:steve@hibernate.org">Steve Ebersole</a>
 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 */
public class Hibernate
      extends ServiceMBeanSupport
      implements HibernateMBean
{
   private static final Logger log = Logger.getLogger( Hibernate.class );

   /**
    * notification type produced when the session factory gets created
    */
   public static final String SESSION_FACTORY_CREATE = "hibernate.sessionfactory.create";
   
   /**
    * notification type produced when the session factory gets destroyed
    */
   public static final String SESSION_FACTORY_DESTROY = "hibernate.sessionfactory.destroy";


   // Configuration attributes "passed through" to Hibernate
   private String datasourceName;
   private String dialect;
   private String defaultSchema;
    private String defaultCatalog;
   private String maxFetchDepth;
   private String jdbcBatchSize;
   private String batchVersionedDataEnabled;
   private String jdbcFetchSize;
   private String jdbcScrollableResultSetEnabled;
   private String getGeneratedKeysEnabled;
   private String streamsForBinaryEnabled;
   private String reflectionOptimizationEnabled;
   private String hbm2ddlAuto;
   private String querySubstitutions;
   private String showSqlEnabled;
   private String username;
   private String password;
   private String queryCacheEnabled;
   private String cacheProviderClass;
   private ObjectName deployedTreeCacheObjectName;
   private String minimalPutsEnabled;
   private String cacheRegionPrefix;
   private String sessionFactoryInterceptor;

   // Configuration attributes used strictly by the MBean
   private URL harUrl;
   private String sessionFactoryName;

   // Internal state
   private boolean dirty = false;
   private SessionFactory sessionFactory;

   /**
    * Get the <tt>SessionFactory</tt> JNDI name.
    * @jmx.managed-attribute
    */
   public String getSessionFactoryName()
   {
      return sessionFactoryName;
   }

   /**
    * Set the <tt>SessionFactory</tt> JNDI name.
    * @jmx.managed-attribute
    */
   public void setSessionFactoryName(String sessionFactoryName)
   {
      this.sessionFactoryName = sessionFactoryName;
      dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public URL getHarUrl()
   {
      return harUrl;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setHarUrl(URL harUrl)
   {
      this.harUrl = harUrl;
      dirty = true;
   }

   /**
    * Get the JNDI datasource name.
    * @jmx.managed-attribute
    */
   public String getDatasourceName()
   {
      return datasourceName;
   }

   /**
    * Set the JNDI datasource name.
    * @jmx.managed-attribute
    */
   public void setDatasourceName(String datasourceName)
   {
      this.datasourceName = datasourceName;
      dirty = true;
   }

   /**
    * Get the default database schema.
    * @jmx.managed-attribute
    */
   public String getDefaultSchema()
   {
      return defaultSchema;
   }

   /**
    * Set the default database schema.
    * @jmx.managed-attribute
    */
   public void setDefaultSchema(String defaultSchema)
   {
      this.defaultSchema = defaultSchema;
      dirty = true;
   }

   /**
    * Get the default database catalog.
    * @jmx.managed-attribute
    */
   public String getDefaultCatalog()
   {
      return defaultCatalog;
   }

   /**
    * Set the default database catalog.
    * @jmx.managed-attribute
    */
   public void setDefaultCatalog(String defaultCatalog)
   {
      this.defaultCatalog = defaultCatalog;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getHbm2ddlAuto()
   {
      return hbm2ddlAuto;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setHbm2ddlAuto(String hbm2ddlAuto)
   {
      this.hbm2ddlAuto = hbm2ddlAuto;
      dirty = true;
   }

   /**
    * Get the Hibernate SQL dialect.
    * @jmx.managed-attribute
    */
   public String getDialect()
   {
      return dialect;
   }

   /**
    * Set the Hibernate SQL dialect.
    * @jmx.managed-attribute
    */
   public void setDialect(String dialect)
   {
      this.dialect = dialect;
      dirty = true;
   }

   /**
    * Get the maximum outer join fetch depth.
    * @jmx.managed-attribute
    */
   public String getMaxFetchDepth()
   {
      return maxFetchDepth;
   }

   /**
    * Set the maximum outer join fetch depth.
    * @jmx.managed-attribute
    */
   public void setMaxFetchDepth(String maxFetchDepth)
   {
      this.maxFetchDepth = maxFetchDepth;
      dirty = true;
   }

   /**
    * Get the JDBC batch update batch size.
    * @jmx.managed-attribute
    */
   public String getJdbcBatchSize()
   {
      return jdbcBatchSize;
   }

   /**
    * Set the JDBC batch update batch size.
    * @jmx.managed-attribute
    */
   public void setJdbcBatchSize(String jdbcBatchSize)
   {
      this.jdbcBatchSize = jdbcBatchSize;
      dirty = true;
   }

   /**
    * Get the JDBC fetch size.
    * @jmx.managed-attribute
    */
   public String getJdbcFetchSize()
   {
      return jdbcFetchSize;
   }

   /**
    * Set the JDBC fetch size.
    * @jmx.managed-attribute
    */
   public void setJdbcFetchSize(String jdbcFetchSize)
   {
      this.jdbcFetchSize = jdbcFetchSize;
      dirty = true;
   }

   /**
    * Are scrollable result sets enabled?
    * @jmx.managed-attribute
    */
   public String getJdbcScrollableResultSetEnabled()
   {
      return jdbcScrollableResultSetEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setJdbcScrollableResultSetEnabled(String jdbcScrollableResultSetEnabled)
   {
      this.jdbcScrollableResultSetEnabled = jdbcScrollableResultSetEnabled;
      dirty = true;
   }

   /**
    * Is the use of JDBC3 <tt>getGeneratedKeys()</tt> enabled?
    * @jmx.managed-attribute
    */
   public String getGetGeneratedKeysEnabled()
   {
      return getGeneratedKeysEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setGetGeneratedKeysEnabled(String getGeneratedKeysEnabled)
   {
      this.getGeneratedKeysEnabled = getGeneratedKeysEnabled;
      dirty = true;
   }

   /**
    * Get the query substitutions.
    * @jmx.managed-attribute
    */
   public String getQuerySubstitutions()
   {
      return querySubstitutions;
   }

   /**
    * Set the query substitutions.
    * @jmx.managed-attribute
    */
   public void setQuerySubstitutions(String querySubstitutions)
   {
      this.querySubstitutions = querySubstitutions;
      dirty = true;
   }

   /**
    * Is the query cache enabled?
    * @jmx.managed-attribute
    */
   public String getQueryCacheEnabled()
   {
      return queryCacheEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setQueryCacheEnabled(String queryCacheEnabled)
   {
      this.queryCacheEnabled = queryCacheEnabled;
      dirty = true;
   }

   /**
    * Get the cache provider class.
    * @jmx.managed-attribute
    */
   public String getCacheProviderClass()
   {
      return cacheProviderClass;
   }

   /**
    * Set the cache provider class.
    * @jmx.managed-attribute
    */
   public void setCacheProviderClass(String cacheProviderClass)
   {
      this.cacheProviderClass = cacheProviderClass;
      dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getCacheRegionPrefix()
   {
      return cacheRegionPrefix;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setCacheRegionPrefix(String cacheRegionPrefix)
   {
      this.cacheRegionPrefix = cacheRegionPrefix;
      dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getMinimalPutsEnabled()
   {
      return minimalPutsEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setMinimalPutsEnabled(String minimalPutsEnabled)
   {
      this.minimalPutsEnabled = minimalPutsEnabled;
      dirty = true;
   }

   /**
    * Is SQL being logged to the console?
    * @jmx.managed-attribute
    */
   public String getShowSqlEnabled()
   {
      return showSqlEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setShowSqlEnabled(String showSqlEnabled)
   {
      this.showSqlEnabled = showSqlEnabled;
      dirty = true;
   }

   /**
    * Get the database username.
    * @jmx.managed-attribute
    */
   public String getUsername()
   {
      return username;
   }

   /**
    * Set the database username.
    * @jmx.managed-attribute
    */
   public void setUsername(String username)
   {
      this.username = username;
      dirty = true;
   }

   /**
    * Get the database password.
    * @jmx.managed-attribute
    */
   public String getPassword()
   {
      return password;
   }

   /**
    * Set the database password.
    * @jmx.managed-attribute
    */
   public void setPassword(String password)
   {
      this.password = password;
      dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getSessionFactoryInterceptor()
   {
      return sessionFactoryInterceptor;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setSessionFactoryInterceptor(String sessionFactoryInterceptor)
   {
      this.sessionFactoryInterceptor = sessionFactoryInterceptor;
      dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public ObjectName getDeployedTreeCacheObjectName()
   {
      return deployedTreeCacheObjectName;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setDeployedTreeCacheObjectName(ObjectName deployedTreeCacheObjectName)
   {
      this.deployedTreeCacheObjectName = deployedTreeCacheObjectName;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getBatchVersionedDataEnabled()
   {
      return batchVersionedDataEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setBatchVersionedDataEnabled(String batchVersionedDataEnabled)
   {
      this.batchVersionedDataEnabled = batchVersionedDataEnabled;
      this.dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getStreamsForBinaryEnabled()
   {
      return streamsForBinaryEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setStreamsForBinaryEnabled(String streamsForBinaryEnabled)
   {
      this.streamsForBinaryEnabled = streamsForBinaryEnabled;
      this.dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getReflectionOptimizationEnabled()
   {
      return reflectionOptimizationEnabled;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setReflectionOptimizationEnabled(String reflectionOptimizationEnabled)
   {
      this.reflectionOptimizationEnabled = reflectionOptimizationEnabled;
      this.dirty = true;
   }

   /**
    * @jmx.managed-attribute
    */
   public boolean isDirty()
   {
      return dirty;
   }

   /**
    * @jmx.managed-attribute
    */
   public boolean isSessionFactoryRunning()
   {
      return sessionFactory != null;
   }

   /**
    * @jmx.managed-operation
    */
   public void rebuildSessionFactory() throws Exception
   {
      destroySessionFactory();
      buildSessionFactory();
   }

   /**
    * Configure Hibernate and bind the <tt>SessionFactory</tt> to JNDI.
    */
   public void startService() throws Exception
   {
      log.debug( "Hibernate MBean starting; " + this );
      buildSessionFactory();
   }

   /**
    * Close the <tt>SessionFactory</tt>.
    */
   public void stopService() throws Exception
   {
      destroySessionFactory();
   }

   private void buildSessionFactory() throws Exception
   {
      log.debug( "Building SessionFactory; " + this );
      Configuration cfg = new Configuration();
      cfg.addProperties( getProperties() );

      final File file = new File(harUrl.getFile());
      if ( file.isDirectory() )
      {
         cfg.addDirectory( file );
      }
      else
      {
         cfg.addJar( file );
      }

      Interceptor interceptorInstance = generateInterceptorInstance();
      if ( interceptorInstance != null )
      {
         cfg.setInterceptor( interceptorInstance );
      }

      sessionFactory = cfg.buildSessionFactory();

      try {
         bind();
      }
      catch( HibernateException e ) {
         forceCleanup();
         throw e;
      }

      dirty = false;

      sendNotification(
         new Notification(SESSION_FACTORY_CREATE, getServiceName(), getNextNotificationSequenceNumber())
      );

      log.info("SessionFactory successfully built and bound into JNDI [" + sessionFactoryName + "]");
   }

   private void destroySessionFactory() throws Exception
   {
      if (sessionFactory != null)
      {
         // TODO : need to determine the exact situations where we need to clear the 2nd-lvl cache
         //    to allow clean release of the classloaders.  Most likely, if structured entries are
         //    used; anything else?
         unbind();
         sessionFactory.close();
         sessionFactory = null;

         sendNotification(
            new Notification(SESSION_FACTORY_DESTROY, getServiceName(), getNextNotificationSequenceNumber())
         );
      }
   }

   private Properties getProperties()
   {
      Properties props = new Properties();

      setUnlessNull(props, Environment.DATASOURCE, datasourceName);
      setUnlessNull(props, Environment.DIALECT, dialect);
      // TODO : needed until we decide what to do about "no cache provider specified" internally within Hibernate
      if ( cacheProviderClass == null )
      {
         // Hibernate defaults to always instantiating an EHCacheProvider, which
         // introduces a huge potential for an unecessary dependency
         cacheProviderClass = "org.hibernate.cache.HashtableCacheProvider";
      }
      setUnlessNull(props, Environment.CACHE_PROVIDER, cacheProviderClass);
      setUnlessNull(props, Environment.CACHE_REGION_PREFIX, cacheRegionPrefix);
      setUnlessNull(props, Environment.USE_MINIMAL_PUTS, minimalPutsEnabled);
      setUnlessNull(props, Environment.HBM2DDL_AUTO, hbm2ddlAuto);
      setUnlessNull(props, Environment.DEFAULT_SCHEMA, defaultSchema);
      setUnlessNull(props, Environment.STATEMENT_BATCH_SIZE, jdbcBatchSize);

      log.info( "Using JDBC batch size : " + jdbcBatchSize );

      setUnlessNull(props, Environment.STATEMENT_FETCH_SIZE, jdbcFetchSize);
      setUnlessNull(props, Environment.USE_SCROLLABLE_RESULTSET, jdbcScrollableResultSetEnabled);
      setUnlessNull(props, Environment.USE_QUERY_CACHE, queryCacheEnabled);
      setUnlessNull(props, Environment.QUERY_SUBSTITUTIONS, querySubstitutions);
      setUnlessNull(props, Environment.MAX_FETCH_DEPTH, maxFetchDepth);
      setUnlessNull(props, Environment.SHOW_SQL, showSqlEnabled);
      setUnlessNull(props, Environment.USE_GET_GENERATED_KEYS, getGeneratedKeysEnabled);
      setUnlessNull(props, Environment.USER, username);
      setUnlessNull(props, Environment.PASS, password);
      setUnlessNull(props, Environment.BATCH_VERSIONED_DATA, batchVersionedDataEnabled);
      setUnlessNull(props, Environment.USE_STREAMS_FOR_BINARY, streamsForBinaryEnabled);
      setUnlessNull(props, Environment.USE_REFLECTION_OPTIMIZER, reflectionOptimizationEnabled);

      setUnlessNull(props, Environment.TRANSACTION_MANAGER_STRATEGY, JBossTransactionManagerLookup.class.getName());
      setUnlessNull(props, Environment.TRANSACTION_STRATEGY, JTATransactionFactory.class.getName());

      if ( deployedTreeCacheObjectName != null )
      {
         String objNameString = deployedTreeCacheObjectName.toString();
         if ( objNameString != null && !"".equals( objNameString ) )
         {
            props.setProperty( DeployedTreeCacheProvider.OBJECT_NAME_PROP, objNameString );
         }
      }

      props.setProperty( Environment.FLUSH_BEFORE_COMPLETION, "true" );
      props.setProperty( Environment.AUTO_CLOSE_SESSION, "true" );
      props.setProperty( Environment.RELEASE_CONNECTIONS, "true" );

      return props;
   }

   private void setUnlessNull(Properties props, String key, String value)
   {
      if(value != null)
      {
         props.setProperty(key, value);
      }
   }

   private Interceptor generateInterceptorInstance()
   {
      if (sessionFactoryInterceptor == null)
      {
         return null;
      }

      log.info("Generating session factory interceptor instance [" + sessionFactoryInterceptor + "]");
      try
      {
         return (Interceptor) Thread.currentThread()
                 .getContextClassLoader()
                 .loadClass( sessionFactoryInterceptor )
                 .newInstance();
      }
      catch(Throwable t)
      {
         log.warn("Unable to generate session factory interceptor instance", t);
      }

      return null;
   }

   private void bind() throws HibernateException
   {
      InitialContext ctx = null;
      try
      {
         ctx = new InitialContext();
         Util.bind( ctx, sessionFactoryName, sessionFactory );
      }
      catch( NamingException e )
      {
         throw new HibernateException( "Unable to bind SessionFactory into JNDI", e );
      }
      finally
      {
         if ( ctx != null )
         {
            try
            {
               ctx.close();
            }
            catch( Throwable ignore )
            {
               // ignore
            }
         }
      }
   }

   private void unbind() throws HibernateException
   {
      InitialContext ctx = null;
      try
      {
         ctx = new InitialContext();
         Util.unbind( ctx, sessionFactoryName );
      }
      catch( NamingException e )
      {
         throw new HibernateException( "Unable to unbind SessionFactory from JNDI", e );
      }
      finally {
         if ( ctx != null )
         {
            try
            {
               ctx.close();
            }
            catch( Throwable ignore )
            {
               // ignore
            }
         }
      }
   }

   private void forceCleanup()
   {
      try
      {
         sessionFactory.close();
         sessionFactory = null;
      }
      catch( Throwable ignore )
      {
         // ignore
      }
   }

   public String toString()
   {
      return super.toString() + " [ServiceName=" + serviceName + ", JNDI=" + sessionFactoryName + "]";
   }
}