/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ejb.plugins.cmp.jdbc;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.Iterator;
import java.rmi.RemoteException;

import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.RemoveException;
import javax.transaction.Status;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;

import org.jboss.deployment.DeploymentException;
import org.jboss.ejb.Container;
import org.jboss.ejb.EjbModule;
import org.jboss.ejb.EntityContainer;
import org.jboss.ejb.EntityEnterpriseContext;
import org.jboss.ejb.GenericEntityObjectFactory;
import org.jboss.ejb.plugins.cmp.ejbql.Catalog;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMPFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMRFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCAbstractEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCApplicationMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCEntityMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCXmlFileLoader;
import org.jboss.logging.Logger;
import org.jboss.metadata.ApplicationMetaData;
import org.jboss.tm.TransactionLocal;

/**
 * JDBCStoreManager manages storage of persistence data into a table.
 * Other then loading the initial jbosscmp-jdbc.xml file this class
 * does very little. The interesting tasks are performed by the command
 * classes.
 *
 * Life-cycle:
 *      Tied to the life-cycle of the entity container.
 *
 * Multiplicity:
 *      One per cmp entity bean. This could be less if another implementaion of
 * EntityPersistenceStore is created and thoes beans use the implementation
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
 * @see org.jboss.ejb.EntityPersistenceStore
 * @version $Revision: 81030 $
 */
public final class JDBCStoreManager implements JDBCEntityPersistenceStore
{
   /** The key used to store the tx data map. */
   private static final Object TX_DATA_KEY = "TX_DATA_KEY";
   /** The key to store the Catalog */
   private static final String CATALOG = "CATALOG";

   private static final String CREATED_MANAGERS = "CREATED_JDBCStoreManagers";
   private static final String CMP_JDBC = "CMP-JDBC";

   private EjbModule ejbModule;
   private EntityContainer container;
   private Logger log;

   private JDBCEntityMetaData metaData;
   private JDBCEntityBridge entityBridge;

   private JDBCTypeFactory typeFactory;
   private JDBCQueryManager queryManager;

   private JDBCCommandFactory commandFactory;

   private ReadAheadCache readAheadCache;

   // Manager life cycle commands
   private JDBCInitCommand initCommand;
   private JDBCStartCommand startCommand;
   private JDBCStopCommand stopCommand;
   private JDBCDestroyCommand destroyCommand;

   // Entity life cycle commands
   private JDBCCreateBeanClassInstanceCommand createBeanClassInstanceCommand;
   private JDBCInitEntityCommand initEntityCommand;
   private JDBCFindEntityCommand findEntityCommand;
   private JDBCFindEntitiesCommand findEntitiesCommand;
   private JDBCCreateCommand createEntityCommand;
   private JDBCPostCreateEntityCommand postCreateEntityCommand;
   private JDBCRemoveEntityCommand removeEntityCommand;
   private JDBCLoadEntityCommand loadEntityCommand;
   private JDBCIsModifiedCommand isModifiedCommand;
   private JDBCStoreEntityCommand storeEntityCommand;
   private JDBCActivateEntityCommand activateEntityCommand;
   private JDBCPassivateEntityCommand passivateEntityCommand;

   // commands
   private JDBCLoadRelationCommand loadRelationCommand;
   private JDBCDeleteRelationsCommand deleteRelationsCommand;
   private JDBCInsertRelationsCommand insertRelationsCommand;

   /** A Transaction manager so that we can link preloaded data to a transaction */
   private TransactionManager tm;
   private TransactionLocal txDataMap;

   /** Set of EJBLocalObject instances to be cascade-deleted excluding those that should be batch-cascade-deleted. */
   private TransactionLocal cascadeDeleteSet = new TransactionLocal()
   {
      protected Object initialValue()
      {
         return new CascadeDeleteRegistry();
      }
   };

   /**
    * Gets the container for this entity.
    * @return the container for this entity; null if container has not been set
    */
   public EntityContainer getContainer()
   {
      return container;
   }

   /**
    * Sets the container for this entity.
    * @param container the container for this entity
    * @throws ClassCastException if the container is not an instance of
    * EntityContainer
    */
   public void setContainer(Container container)
   {
      this.container = (EntityContainer)container;
      if(container != null)
      {
         ejbModule = container.getEjbModule();
         log = Logger.getLogger(
            this.getClass().getName() +
            "." +
            container.getBeanMetaData().getEjbName());
      }
      else
      {
         ejbModule = null;
      }
   }

   public JDBCAbstractEntityBridge getEntityBridge()
   {
      return entityBridge;
   }

   public JDBCTypeFactory getJDBCTypeFactory()
   {
      return typeFactory;
   }

   public JDBCEntityMetaData getMetaData()
   {
      return metaData;
   }

   public JDBCQueryManager getQueryManager()
   {
      return queryManager;
   }

   public JDBCCommandFactory getCommandFactory()
   {
      return commandFactory;
   }

   public ReadAheadCache getReadAheadCache()
   {
      return readAheadCache;
   }

   //
   // Genertic data containers
   //
   public Map getApplicationDataMap()
   {
      return ejbModule.getModuleDataMap();
   }

   public Object getApplicationData(Object key)
   {
      return ejbModule.getModuleData(key);
   }

   public void putApplicationData(Object key, Object value)
   {
      ejbModule.putModuleData(key, value);
   }

   private Map getApplicationTxDataMap()
   {
      try
      {
         Transaction tx = tm.getTransaction();
         if(tx == null)
         {
            return null;
         }

         // get the txDataMap from the txMap
         Map txMap = (Map)txDataMap.get(tx);

         // do we have an existing map
         if(txMap == null)
         {
            int status = tx.getStatus();
            if(status == Status.STATUS_ACTIVE || status == Status.STATUS_PREPARING)
            {
               // create and add the new map
               txMap = new HashMap();
               txDataMap.set(tx, txMap);
            }
         }
         return txMap;
      }
      catch(EJBException e)
      {
         throw e;
      }
      catch(Exception e)
      {
         throw new EJBException("Error getting application tx data map.", e);
      }
   }

   /**
    * Schedules instances for cascade-delete
    */
   public void scheduleCascadeDelete(List pks)
   {
      CascadeDeleteRegistry registry = (CascadeDeleteRegistry)cascadeDeleteSet.get();
      registry.scheduleAll(pks);
   }

   /**
    * Unschedules instance cascade delete.
    * @param pk  instance primary key.
    * @return  true if the instance was scheduled for cascade deleted.
    */
   public boolean unscheduledCascadeDelete(Object pk)
   {
      CascadeDeleteRegistry registry = (CascadeDeleteRegistry)cascadeDeleteSet.get();
      return registry.unschedule(pk);
   }

   public Object getApplicationTxData(Object key)
   {
      Map map = getApplicationTxDataMap();
      if(map != null)
      {
         return map.get(key);
      }
      return null;
   }

   public void putApplicationTxData(Object key, Object value)
   {
      Map map = getApplicationTxDataMap();
      if(map != null)
      {
         map.put(key, value);
      }
   }

   private Map getEntityTxDataMap()
   {
      Map entityTxDataMap = (Map)getApplicationTxData(this);
      if(entityTxDataMap == null)
      {
         entityTxDataMap = new HashMap();
         putApplicationTxData(this, entityTxDataMap);
      }
      return entityTxDataMap;
   }

   public Object getEntityTxData(Object key)
   {
      return getEntityTxDataMap().get(key);
   }

   public void putEntityTxData(Object key, Object value)
   {
      getEntityTxDataMap().put(key, value);
   }

   public void removeEntityTxData(Object key)
   {
      getEntityTxDataMap().remove(key);
   }

   public Catalog getCatalog()
   {
      return (Catalog)getApplicationData(CATALOG);
   }

   private void initApplicationDataMap()
   {
      Map moduleData = ejbModule.getModuleDataMap();
      synchronized(moduleData)
      {
         txDataMap = (TransactionLocal)moduleData.get(TX_DATA_KEY);
         if(txDataMap == null)
         {
            txDataMap = new TransactionLocal();
            moduleData.put(TX_DATA_KEY, txDataMap);
         }
      }
   }

   /**
    * Does almost nothing because other services such
    * as JDBC data sources may not have been started.
    */
   public void create() throws Exception
   {
      // Store a reference to this manager in an application level hashtable.
      // This way in the start method other managers will be able to know
      // the other managers.
      HashMap managersMap = (HashMap)getApplicationData(CREATED_MANAGERS);
      if(managersMap == null)
      {
         managersMap = new HashMap();
         putApplicationData(CREATED_MANAGERS, managersMap);
      }
      managersMap.put(container.getBeanMetaData().getEjbName(), this);
   }

   /**
    * Bring the store to a fully initialized state
    */
   public void start() throws Exception
   {
      //
      //
      // Start Phase 1: create bridge and commands but
      // don't access other entities
      initStoreManager();


      // If all managers have been started (this is the last manager),
      // complete the other two phases of startup.
      Catalog catalog = getCatalog();
      HashMap managersMap = (HashMap)getApplicationData(CREATED_MANAGERS);
      if(catalog.getEntityCount() == managersMap.size()
         && catalog.getEJBNames().equals(managersMap.keySet()))
      {
         // Make a copy of the managers (for safty)
         ArrayList managers = new ArrayList(managersMap.values());

         //
         //
         // Start Phase 2: resolve relationships
         for(int i = 0; i < managers.size(); ++i)
         {
            JDBCStoreManager manager = (JDBCStoreManager)managers.get(i);
            manager.resolveRelationships();
         }

         //
         //
         // Start Phase 3: create tables and compile queries
         for(int i = 0; i < managers.size(); ++i)
         {
            JDBCStoreManager manager = (JDBCStoreManager)managers.get(i);
            manager.startStoreManager();
         }

         // add foreign key constraints
         for(int i = 0; i < managers.size(); ++i)
         {
            JDBCStoreManager manager = (JDBCStoreManager)managers.get(i);
            manager.startCommand.addForeignKeyConstraints();
         }
      }
   }

   /**
    * Preforms as much initialization as possible without referencing
    * another entity.
    */
   private void initStoreManager() throws Exception
   {
      if(log.isDebugEnabled())
         log.debug("Initializing CMP plugin for " + container.getBeanMetaData().getEjbName());

      // get the transaction manager
      tm = container.getTransactionManager();

      // initializes the generic data containers
      initApplicationDataMap();

      // load the metadata for this entity
      metaData = loadJDBCEntityMetaData();

      // setup the type factory, which is used to map java types to sql types.
      typeFactory = new JDBCTypeFactory(
         metaData.getTypeMapping(),
         metaData.getJDBCApplication().getValueClasses(),
         metaData.getJDBCApplication().getUserTypeMappings()
      );

      // create the bridge between java land and this engine (sql land)
      entityBridge = new JDBCEntityBridge(metaData, this);
      entityBridge.init();

      // add the entity bridge to the catalog
      Catalog catalog = getCatalog();
      if(catalog == null)
      {
         catalog = new Catalog();
         putApplicationData(CATALOG, catalog);
      }
      catalog.addEntity(entityBridge);

      // create the read ahead cache
      readAheadCache = new ReadAheadCache(this);
      readAheadCache.create();

      // Set up Commands
      commandFactory = new JDBCCommandFactory(this);

      // Execute the init command
      initCommand = commandFactory.createInitCommand();
      initCommand.execute();
   }

   private void resolveRelationships() throws Exception
   {
      entityBridge.resolveRelationships();
   }

   /**
    * Brings the store manager into a completely running state.
    * This method will create the database table and compile the queries.
    */
   private void startStoreManager() throws Exception
   {
      entityBridge.start();

      // Store manager life cycle commands
      startCommand = commandFactory.createStartCommand();
      stopCommand = commandFactory.createStopCommand();
      destroyCommand = commandFactory.createDestroyCommand();

      // Entity commands
      initEntityCommand = commandFactory.createInitEntityCommand();
      createBeanClassInstanceCommand = commandFactory.createCreateBeanClassInstanceCommand();
      findEntityCommand = commandFactory.createFindEntityCommand();
      findEntitiesCommand = commandFactory.createFindEntitiesCommand();
      createEntityCommand = commandFactory.createCreateEntityCommand();
      postCreateEntityCommand = commandFactory.createPostCreateEntityCommand();
      removeEntityCommand = commandFactory.createRemoveEntityCommand();
      loadEntityCommand = commandFactory.createLoadEntityCommand();
      isModifiedCommand = commandFactory.createIsModifiedCommand();
      storeEntityCommand = commandFactory.createStoreEntityCommand();
      activateEntityCommand = commandFactory.createActivateEntityCommand();
      passivateEntityCommand = commandFactory.createPassivateEntityCommand();

      // Relation commands
      loadRelationCommand = commandFactory.createLoadRelationCommand();
      deleteRelationsCommand = commandFactory.createDeleteRelationsCommand();
      insertRelationsCommand = commandFactory.createInsertRelationsCommand();

      // Create the query manager
      queryManager = new JDBCQueryManager(this);

      // Execute the start command, creates the tables
      startCommand.execute();

      // Start the query manager. At this point is creates all of the
      // query commands. The must occure in the start phase, as
      // queries can opperate on other entities in the application, and
      // all entities are gaurenteed to be createed until the start phase.
      queryManager.start();

      readAheadCache.start();
   }

   public void stop()
   {
      // On deploy errors, sometimes CMPStoreManager was never initialized!
      if(stopCommand != null)
      {
         Map managersMap = (HashMap)getApplicationData(CREATED_MANAGERS);
         while(!managersMap.isEmpty())
         {
            int stoppedInIteration = 0;
            for(Iterator i = managersMap.values().iterator(); i.hasNext();)
            {
               JDBCStoreManager manager = (JDBCStoreManager)i.next();
               if(manager.stopCommand == null || manager.stopCommand.execute())
               {
                  i.remove();
                  ++stoppedInIteration;
               }
            }

            if(stoppedInIteration == 0)
            {
               break;
            }
         }
      }
      readAheadCache.stop();
   }

   public void destroy()
   {
      // On deploy errors, sometimes CMPStoreManager was never initialized!
      if(destroyCommand != null)
      {
         destroyCommand.execute();
      }

      if(readAheadCache != null)
      {
         readAheadCache.destroy();
      }

      readAheadCache = null;
      if(queryManager != null)
      {
         queryManager.clear();
      }
      queryManager = null;
      //Remove proxy from proxy map so UnifiedClassloader may be released
      if(createBeanClassInstanceCommand != null)
      {
         createBeanClassInstanceCommand.destroy();
      } // end of if ()
   }

   //
   // EJB Life Cycle Commands
   //
   /**
    * Returns a new instance of a class which implemnts the bean class.
    *
    * @return the new instance
    */
   public Object createBeanClassInstance() throws Exception
   {
      if(createBeanClassInstanceCommand == null)
         throw new IllegalStateException("createBeanClassInstanceCommand == null");
      return createBeanClassInstanceCommand.execute();
   }

   public void initEntity(EntityEnterpriseContext ctx)
   {
      initEntityCommand.execute(ctx);
   }

   public Object createEntity(Method createMethod, Object[] args, EntityEnterpriseContext ctx)
      throws CreateException
   {
      Object pk = createEntityCommand.execute(createMethod, args, ctx);
      if(pk == null)
         throw new CreateException("Primary key for created instance is null.");
      return pk;
   }

   public Object postCreateEntity(Method createMethod, Object[] args, EntityEnterpriseContext ctx)
   {
      return postCreateEntityCommand.execute(createMethod, args, ctx);
   }

   public Object findEntity(Method finderMethod,
                            Object[] args,
                            EntityEnterpriseContext ctx,
                            GenericEntityObjectFactory factory)
      throws FinderException
   {
      return findEntityCommand.execute(finderMethod, args, ctx, factory);
   }

   public Collection findEntities(Method finderMethod,
                                  Object[] args,
                                  EntityEnterpriseContext ctx,
                                  GenericEntityObjectFactory factory)
      throws FinderException
   {
      return findEntitiesCommand.execute(finderMethod, args, ctx, factory);
   }

   public void activateEntity(EntityEnterpriseContext ctx)
   {
      activateEntityCommand.execute(ctx);
   }

   /**
    * Loads entity.
    * If entity not found NoSuchEntityException is thrown.
    * @param ctx - entity context.
    */
   public void loadEntity(EntityEnterpriseContext ctx)
   {
      loadEntity(ctx, true);
   }

   public boolean loadEntity(EntityEnterpriseContext ctx, boolean failIfNotFound)
   {
      // is any on the data already in the entity valid
      if(!ctx.isValid())
      {
         if(log.isTraceEnabled())
         {
            log.trace("RESET PERSISTENCE CONTEXT: id=" + ctx.getId());
         }
         entityBridge.resetPersistenceContext(ctx);
      }

      // mark the entity as created; if it was loading it was created
      JDBCEntityBridge.setCreated(ctx);

      return loadEntityCommand.execute(ctx, failIfNotFound);
   }

   public void loadField(JDBCCMPFieldBridge field, EntityEnterpriseContext ctx)
   {
      loadEntityCommand.execute(field, ctx);
   }

   public boolean isStoreRequired(EntityEnterpriseContext ctx)
   {
      return isModifiedCommand.execute(ctx);
   }

   public boolean isModified(EntityEnterpriseContext ctx)
   {
      return entityBridge.isModified(ctx);
   }

   public void storeEntity(EntityEnterpriseContext ctx)
   {
      storeEntityCommand.execute(ctx);
      synchronizeRelationData();
   }

   private void synchronizeRelationData()
   {
      final JDBCCMRFieldBridge[] cmrFields = (JDBCCMRFieldBridge[]) entityBridge.getCMRFields();
      for(int i = 0; i < cmrFields.length; ++i)
      {
         final JDBCCMRFieldBridge.RelationDataManager relationManager = cmrFields[i].getRelationDataManager();
         if(relationManager.isDirty())
         {
            final RelationData relationData = relationManager.getRelationData();

            deleteRelations(relationData);
            insertRelations(relationData);

            relationData.addedRelations.clear();
            relationData.removedRelations.clear();
            relationData.notRelatedPairs.clear();
         }
      }
   }

   public void passivateEntity(EntityEnterpriseContext ctx)
   {
      passivateEntityCommand.execute(ctx);
   }

   public void removeEntity(EntityEnterpriseContext ctx) throws RemoveException, RemoteException
   {
      removeEntityCommand.execute(ctx);
   }

   //
   // Relationship Commands
   //
   public Collection loadRelation(JDBCCMRFieldBridge cmrField, Object pk)
   {
      return loadRelationCommand.execute(cmrField, pk);
   }

   private void deleteRelations(RelationData relationData)
   {
      deleteRelationsCommand.execute(relationData);
   }

   private void insertRelations(RelationData relationData)
   {
      insertRelationsCommand.execute(relationData);
   }

   private JDBCEntityMetaData loadJDBCEntityMetaData()
      throws DeploymentException
   {
      ApplicationMetaData amd = container.getBeanMetaData().getApplicationMetaData();

      // Get JDBC MetaData
      JDBCApplicationMetaData jamd = (JDBCApplicationMetaData)amd.getPluginData(CMP_JDBC);

      if(jamd == null)
      {
         // we are the first cmp entity to need jbosscmp-jdbc.
         // Load jbosscmp-jdbc.xml for the whole application
         JDBCXmlFileLoader jfl = new JDBCXmlFileLoader(container, log);

         jamd = jfl.load();
         amd.addPluginData(CMP_JDBC, jamd);
      }

      // Get JDBC Bean MetaData
      String ejbName = container.getBeanMetaData().getEjbName();
      JDBCEntityMetaData metadata = jamd.getBeanByEjbName(ejbName);
      if(metadata == null)
      {
         throw new DeploymentException("No metadata found for bean " + ejbName);
      }
      return metadata;
   }

   // Inner

   private final class CascadeDeleteRegistry
   {
      private Set scheduled;

      public void scheduleAll(List pks)
      {
         if(scheduled == null)
         {
            scheduled = new HashSet();
         }
         scheduled.addAll(pks);
      }

      public boolean unschedule(Object pk)
      {
         return scheduled.remove(pk);
      }
   }
}
