/*
 * 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.JDBCUtils;
import org.exoplatform.services.jcr.core.security.JCRRuntimePermissions;
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 java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * The goal of this class is removing data from database.
 *
 * @author <a href="karpenko.sergiy@gmail.com">Karpenko Sergiy</a> 
 * @version $Id: DBCleanerTool.java 3769 2011-01-04 15:36:06Z areshetnyak $
 */
public class DBCleanerTool
{
   /**
    * Logger.
    */
   protected final static Log LOG = ExoLogger.getLogger("exo.jcr.component.core.DBCleaner");

   protected final Connection connection;

   protected final boolean autoCommit;

   protected final List<String> rollbackingScripts = new ArrayList<String>();

   protected final List<String> committingScripts = new ArrayList<String>();

   protected final List<String> cleaningScripts = new ArrayList<String>();

   /**
    * DBCleanerTool constructor.
    * 
    * @param connection 
    *          connection to database which will be used in cleaning, take in account DBCleanerTool does not
    *          close connection
    * @param autoCommit 
    *          auto commit mode which will be set during script execution         
    */
   DBCleanerTool(Connection connection, boolean autoCommit, Collection<String> cleaningScripts,
      Collection<String> committingScripts, Collection<String> rollbackingScripts)
   {
      this.connection = connection;
      this.autoCommit = autoCommit;

      this.cleaningScripts.addAll(cleaningScripts);
      this.committingScripts.addAll(committingScripts);
      this.rollbackingScripts.addAll(rollbackingScripts);
   }

   /**
    * Clean JCR tables, can't contain some indexes or constraints,
    * should be added on {@link #commit}. It done for possibility to restore
    * data without any violations.
    * 
    * <br>
    * This method does not invoke commit or rollback on {@link Connection} but
    * needed autocommit mode can be set.
    * 
    * @throws DBCleanException
    */
   public void clean() throws DBCleanException
   {
      try
      {
         execute(cleaningScripts);
      }
      catch (SQLException e)
      {
         throw new DBCleanException(JDBCUtils.getFullMessage(e), e);
      }
   }

   /**
    * Executes SQL scripts for finishing clean operations if needed. 
    * It can be adding indexes, constraints, removing temporary objects etc 
    * (related to specific database) or does nothing.
    * 
    * <br>
    * This method does not invoke commit or rollback on {@link Connection} but
    * needed autocommit mode can be set.
    * 
    * @throws DBCleanException
    */
   public void commit() throws DBCleanException
   {
      try
      {
         execute(committingScripts);
      }
      catch (SQLException e)
      {
         throw new DBCleanException(JDBCUtils.getFullMessage(e), e);
      }
   }

   /**
    * Tries to restore previous data by renaming tables etc
    * (related to specific database) or does nothing.
    * 
    * <br>
    * This method does not invoke commit or rollback on {@link Connection} but
    * needed autocommit mode can be set.
    * 
    * @throws DBCleanException
    */
   public void rollback() throws DBCleanException
   {
      try
      {
         execute(rollbackingScripts);
      }
      catch (SQLException e)
      {
         throw new DBCleanException(JDBCUtils.getFullMessage(e), e);
      }
   }

   /**
    * Return connection which it is used in cleaner.
    */
   public Connection getConnection()
   {
      return connection;
   }

   /**
    * Execute script on database. Set auto commit mode if needed.  
    * 
    * @param scripts
    *          the scripts for execution 
    * @throws SQLException
    */
   protected void execute(List<String> scripts) throws SQLException
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      // set needed auto commit mode
      boolean autoCommit = connection.getAutoCommit();
      if (autoCommit != this.autoCommit)
      {
         connection.setAutoCommit(this.autoCommit);
      }

      Statement st = connection.createStatement();
      try
      {
         for (String scr : scripts)
         {
            String sql = JDBCUtils.cleanWhitespaces(scr.trim());
            if (!sql.isEmpty())
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.debug("Execute script: \n[" + sql + "]");
               }

               executeQuery(st, sql);
            }
         }
      }
      finally
      {
         try
         {
            st.close();
         }
         catch (SQLException e)
         {
            LOG.error("Can't close the Statement." + e.getMessage());
         }

         // restore previous auto commit mode
         if (autoCommit != this.autoCommit)
         {
            connection.setAutoCommit(autoCommit);
         }
      }
   }

   protected void executeQuery(final Statement statement, final String sql) throws SQLException
   {
      try
      {
         long start = 0;
         if (LOG.isDebugEnabled())
         {
            start = System.currentTimeMillis();
            LOG.debug("Execute script: \n[" + sql + "]");
         }
         SecurityHelper.doPrivilegedSQLExceptionAction(new PrivilegedExceptionAction<Object>()
         {
            public Object run() throws Exception
            {
               statement.executeUpdate(sql);
               return null;
            }
         });
         if (LOG.isDebugEnabled())
         {
            LOG.debug("Script "+sql+" executed in " + ((System.currentTimeMillis() - start) / 1000d) + " sec");
         }
      }
      catch (SQLException e)
      {
         LOG.error("Query execution \"" + sql + "\" failed: " + JDBCUtils.getFullMessage(e), e);
         throw e;
      }
   }
}
