/*
 * Copyright (C) 2003-2010 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.services.jcr.impl.clean.rdbms;

import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.services.database.utils.DialectConstants;
import org.exoplatform.services.database.utils.DialectDetecter;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.RepositoryEntry;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.core.security.JCRRuntimePermissions;
import org.exoplatform.services.jcr.impl.clean.rdbms.scripts.DBCleaningScripts;
import org.exoplatform.services.jcr.impl.clean.rdbms.scripts.DBCleaningScriptsFactory;
import org.exoplatform.services.jcr.impl.storage.jdbc.DBConstants;
import org.exoplatform.services.jcr.impl.storage.jdbc.JDBCWorkspaceDataContainer;
import org.exoplatform.services.jcr.impl.util.jdbc.DBInitializerHelper;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.security.PrivilegedExceptionAction;
import java.sql.Connection;
import java.sql.SQLException;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

/**
 * Created by The eXo Platform SAS.
 *
 * Date: 24 01 2011
 * 
 * @author <a href="mailto:anatoliy.bazko@exoplatform.com.ua">Anatoliy Bazko</a>
 * @version $Id: DBCleanService.java 34360 2010-11-11 11:11:11Z tolusha $
 */
public class DBCleanService
{
   /**
    * Logger.
    */
   protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.DBCleanService");

   /**
    * Cleans workspace data from database.
    * 
    * @param wsEntry
    *          workspace configuration
    * @throws DBCleanException         
    */
   public static void cleanWorkspaceData(WorkspaceEntry wsEntry) throws DBCleanException
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      Connection jdbcConn = getConnection(wsEntry);
      String dialect = resolveDialect(jdbcConn, wsEntry);
      boolean autoCommit = dialect.startsWith(DialectConstants.DB_DIALECT_SYBASE);

      try
      {
         jdbcConn.setAutoCommit(autoCommit);

         DBCleanerTool dbCleaner = getWorkspaceDBCleaner(jdbcConn, wsEntry);
         doClean(dbCleaner);
      }
      catch (SQLException e)
      {
         throw new DBCleanException(e);
      }
      finally
      {
         try
         {
            jdbcConn.close();
         }
         catch (SQLException e)
         {
            LOG.error("Can not close connection", e);
         }
      }
   }

   /**
    * Cleans repository data from database.
    * 
    * @param rEntry
    *          the repository configuration
    * @throws DBCleanException          
    */
   public static void cleanRepositoryData(RepositoryEntry rEntry) throws DBCleanException
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      WorkspaceEntry wsEntry = rEntry.getWorkspaceEntries().get(0);

      boolean multiDB = getMultiDbParameter(wsEntry);
      if (multiDB)
      {
         for (WorkspaceEntry entry : rEntry.getWorkspaceEntries())
         {
            cleanWorkspaceData(entry);
         }
      }
      else
      {
         Connection jdbcConn = getConnection(wsEntry);
         String dialect = resolveDialect(jdbcConn, wsEntry);
         boolean autoCommit = dialect.startsWith(DialectConstants.DB_DIALECT_SYBASE);

         try
         {
            jdbcConn.setAutoCommit(autoCommit);

            DBCleanerTool dbCleaner = getRepositoryDBCleaner(jdbcConn, rEntry);
            doClean(dbCleaner);
         }
         catch (SQLException e)
         {
            throw new DBCleanException(e);
         }
         finally
         {
            try
            {
               jdbcConn.close();
            }
            catch (SQLException e)
            {
               LOG.error("Can not close connection", e);
            }
         }
      }
   }

   /**
    * Returns database cleaner for repository. 
    * 
    * @param jdbcConn
    *          database connection which need to use
    * @param rEntry
    *          repository configuration
    * @return DBCleanerTool
    * @throws DBCleanException
    */
   public static DBCleanerTool getRepositoryDBCleaner(Connection jdbcConn, RepositoryEntry rEntry)
      throws DBCleanException
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      WorkspaceEntry wsEntry = rEntry.getWorkspaceEntries().get(0);

      boolean multiDb = getMultiDbParameter(wsEntry);
      if (multiDb)
      {
         throw new DBCleanException(
            "It is not possible to create cleaner with common connection for multi database repository configuration");
      }

      String dialect = resolveDialect(jdbcConn, wsEntry);
      boolean autoCommit = dialect.startsWith(DialectConstants.DB_DIALECT_SYBASE);

      DBCleaningScripts scripts = DBCleaningScriptsFactory.prepareScripts(dialect, rEntry);

      return new DBCleanerTool(jdbcConn, autoCommit, scripts.getCleaningScripts(), scripts.getCommittingScripts(),
         scripts.getRollbackingScripts());
   }

   /**
    * Returns database cleaner for workspace.  
    * 
    * @param jdbcConn
    *          database connection which need to use
    * @param wsEntry
    *          workspace configuration
    * @return DBCleanerTool
    * @throws DBCleanException
    */
   public static DBCleanerTool getWorkspaceDBCleaner(Connection jdbcConn, WorkspaceEntry wsEntry) throws DBCleanException
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      String dialect = resolveDialect(jdbcConn, wsEntry);
      boolean autoCommit = dialect.startsWith(DialectConstants.DB_DIALECT_SYBASE);
      
      DBCleaningScripts scripts = DBCleaningScriptsFactory.prepareScripts(dialect, wsEntry);

      return new DBCleanerTool(jdbcConn, autoCommit, scripts.getCleaningScripts(), scripts.getCommittingScripts(),
         scripts.getRollbackingScripts());
   }

   /**
    * Cleaning.
    * 
    * @throws SQLException 
    */
   private static void doClean(DBCleanerTool dbCleaner) throws DBCleanException, SQLException
   {
      Connection jdbcConn = dbCleaner.getConnection();
      try
      {
         dbCleaner.clean();
         dbCleaner.commit();

         jdbcConn.commit();
      }
      catch (SQLException e)
      {
         jdbcConn.rollback();

         dbCleaner.rollback();

         jdbcConn.commit();
      }
   }

   /**
    * Opens connection to database underlying a workspace.
    */
   private static Connection getConnection(WorkspaceEntry wsEntry) throws DBCleanException
   {
      String dsName = getSourceNameParameter(wsEntry);

      DataSource ds;
      try
      {
         ds = (DataSource)new InitialContext().lookup(dsName);
      }
      catch (NamingException e)
      {
         throw new DBCleanException(e);
      }

      if (ds == null)
      {
         throw new DBCleanException("Data source " + dsName + " not found");
      }

      final DataSource dsF = ds;

      Connection jdbcConn;
      try
      {
         jdbcConn = SecurityHelper.doPrivilegedSQLExceptionAction(new PrivilegedExceptionAction<Connection>()
         {
            public Connection run() throws Exception
            {
               return dsF.getConnection();
            }
         });
      }
      catch (SQLException e)
      {
         throw new DBCleanException(e);
      }

      return jdbcConn;
   }

   /**
    * Return {@link JDBCWorkspaceDataContainer#SOURCE_NAME} parameter from workspace configuration.
    */
   private static String getSourceNameParameter(WorkspaceEntry wsEntry) throws DBCleanException
   {
      try
      {
         return wsEntry.getContainer().getParameterValue(JDBCWorkspaceDataContainer.SOURCE_NAME);
      }
      catch (RepositoryConfigurationException e)
      {
         throw new DBCleanException(e);
      }
   }

   /**
    * Return {@link JDBCWorkspaceDataContainer#MULTIDB} parameter from workspace configuration.
    */
   private static boolean getMultiDbParameter(WorkspaceEntry wsEntry) throws DBCleanException
   {
      try
      {
         return DBInitializerHelper.getDatabaseType(wsEntry).isMultiDatabase();
      }
      catch (RepositoryConfigurationException e)
      {
         throw new DBCleanException(e);
      }
   }

   /**
    * Resolves dialect which it is used in workspace configuration. First of all, 
    * method will try to get parameter {@link JDBCWorkspaceDataContainer#DB_DIALECT} from 
    * a configuration. And only then method will try to detect dialect using {@link DialectDetecter} in case 
    * if dialect is set as {@link DialectConstants#DB_DIALECT_AUTO}.  
    * 
    * @param wsEntry
    *          workspace configuration
    * @return dialect
    * @throws DBCleanException
    */
   private static String resolveDialect(Connection jdbcConn, WorkspaceEntry wsEntry) throws DBCleanException
   {
      String dialect = DBInitializerHelper.getDatabaseDialect(wsEntry);

      if (dialect.startsWith(DBConstants.DB_DIALECT_AUTO))
      {
         try
         {
            dialect = DialectDetecter.detect(jdbcConn.getMetaData());
         }
         catch (SQLException e)
         {
            throw new DBCleanException(e);
         }
      }

      return dialect;
   }
}
