/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.exoplatform.services.jcr.impl.backup.rdbms;

import org.exoplatform.services.database.utils.DialectConstants;
import org.exoplatform.services.database.utils.DialectDetecter;
import org.exoplatform.services.database.utils.JDBCUtils;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.core.security.JCRRuntimePermissions;
import org.exoplatform.services.jcr.dataflow.serialization.ObjectReader;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.backup.BackupException;
import org.exoplatform.services.jcr.impl.backup.DataRestore;
import org.exoplatform.services.jcr.impl.clean.rdbms.DBCleanException;
import org.exoplatform.services.jcr.impl.clean.rdbms.DBCleanerTool;
import org.exoplatform.services.jcr.impl.dataflow.serialization.ZipObjectReader;
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.io.FileCleaner;
import org.exoplatform.services.jcr.impl.util.jdbc.DBInitializerHelper;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipInputStream;

import javax.naming.NamingException;

/**
 * Created by The eXo Platform SAS.
 *
 * Date: 22 01 2011
 * 
 * @author <a href="mailto:anatoliy.bazko@exoplatform.com.ua">Anatoliy Bazko</a>
 * @version $Id: DBRestore.java 34360 2010-11-11 11:11:11Z tolusha $
 */
public class DBRestore implements DataRestore
{
   /**
    * The maximum possible batch size.
    */
   private final int MAXIMUM_BATCH_SIZE = 1000;

   /**
    * List of temporary files.
    */
   private final List<File> spoolFileList = new ArrayList<File>();

   /**
    *  The file cleaner.
    */
   private final FileCleaner fileCleaner;

   /**
    * Temporary directory.
    */
   private final File tempDir = new File(System.getProperty("java.io.tmpdir"));

   /**
    * Maximum buffer size.
    */
   private final int maxBufferSize;

   /**
    * Logger.
    */
   protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.DBRestor");

   /**
    * Connection to database.
    */
   protected final Connection jdbcConn;

   /**
    * Directory for dumps.
    */
   private final File storageDir;

   /**
    * Restore table rules.
    */
   protected final Map<String, TableTransformationRule> tables;

   /**
    * Database cleaner.
    */
   private final DBCleanerTool dbCleaner;

   /**
    * Database dialect.
    */
   protected final String dialect;

   protected final boolean useSequence;

   protected final String itemTableName;

   /**
    * Contains object names which executed queries.   
    */
   protected List<String> successfulExecuted;

   protected boolean dbCleanerInAutoCommit;

   /**
    * Constructor DBRestore.
    * 
    * @throws NamingException 
    * @throws SQLException 
    * @throws RepositoryConfigurationException 
    */
   public DBRestore(File storageDir, Connection jdbcConn, Map<String, TableTransformationRule> tables,
      WorkspaceEntry wsConfig, FileCleaner fileCleaner, DBCleanerTool dbCleaner) throws NamingException,
      SQLException, RepositoryConfigurationException
   {
      this.jdbcConn = jdbcConn;
      this.fileCleaner = fileCleaner;
      this.maxBufferSize =
         wsConfig.getContainer().getParameterInteger(JDBCWorkspaceDataContainer.MAXBUFFERSIZE_PROP,
            JDBCWorkspaceDataContainer.DEF_MAXBUFFERSIZE);

      this.storageDir = storageDir;
      this.tables = tables;
      this.dbCleaner = dbCleaner;
      this.dialect = DialectDetecter.detect(jdbcConn.getMetaData());
      this.dbCleanerInAutoCommit = dialect.startsWith(DialectConstants.DB_DIALECT_SYBASE);
      this.itemTableName = DBInitializerHelper.getItemTableName(wsConfig);
      this.useSequence= DBInitializerHelper.useSequenceForOrderNumber(wsConfig,this.dialect);
   }

   /**
    * {@inheritDoc}
    */
   public void clean() throws BackupException
   {
      LOG.info("Start to clean JCR tables");
      try
      {
         dbCleaner.clean();
      }
      catch (DBCleanException e)
      {
         throw new BackupException(e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void restore() throws BackupException
   {
      try
      {
         for (Entry<String, TableTransformationRule> entry : tables.entrySet())
         {
            String tableName = entry.getKey();
            TableTransformationRule restoreRule = entry.getValue();

            restoreTable(storageDir, jdbcConn, tableName, restoreRule);
         }
      }
      catch (IOException e)
      {
         throw new BackupException(e);
      }
      catch (SQLException e)
      {
         throw new BackupException("SQL Exception: " + JDBCUtils.getFullMessage(e), e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void commit() throws BackupException
   {
      try
      {
         dbCleaner.commit();

         jdbcConn.commit();
      }
      catch (SQLException e)
      {
         throw new BackupException(e);
      }
      catch (DBCleanException e)
      {
         throw new BackupException(e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void rollback() throws BackupException
   {
      try
      {
         jdbcConn.rollback();

         dbCleaner.rollback();

         jdbcConn.commit();
      }
      catch (SQLException e)
      {
         throw new BackupException(e);
      }
      catch (DBCleanException e)
      {
         throw new BackupException(e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void close() throws BackupException
   {
      try
      {
         // in case for shared connection
         if (!jdbcConn.isClosed())
         {
            jdbcConn.close();
         }
      }
      catch (SQLException e)
      {
         throw new BackupException(e);
      }
   }

   /**
    * Restore table.
    */
   private void restoreTable(File storageDir, Connection jdbcConn, String tableName, TableTransformationRule restoreRule)
      throws IOException, SQLException
   {
      // Need privileges

      ZipObjectReader contentReader = null;
      ZipObjectReader contentLenReader = null;

      PreparedStatement insertNode = null;
      ResultSet tableMetaData = null;
      Statement stmt = null;

      // switch table name to lower case
      if (dialect.startsWith(DBConstants.DB_DIALECT_PGSQL))
      {
         tableName = tableName.toLowerCase();
      }

      try
      {
         File contentFile = new File(storageDir, restoreRule.getSrcTableName() + DBBackup.CONTENT_FILE_SUFFIX);

         // check old style backup format, when for every table was dedicated zip file 
         if (contentFile.exists())
         {
            contentReader = new ZipObjectReader(new ZipInputStream(new FileInputStream(contentFile)));
            contentReader.getNextEntry();

            File contentLenFile =
               new File(storageDir, restoreRule.getSrcTableName() + DBBackup.CONTENT_LEN_FILE_SUFFIX);

            contentLenReader = new ZipObjectReader(new ZipInputStream(new FileInputStream(contentLenFile)));
            contentLenReader.getNextEntry();
         }
         else
         {
            contentFile = new File(storageDir, DBBackup.CONTENT_ZIP_FILE);
            contentReader = new ZipObjectReader(new ZipInputStream(new FileInputStream(contentFile)));

            while (!contentReader.getNextEntry().getName().equals(restoreRule.getSrcTableName()));

            File contentLenFile = new File(storageDir, DBBackup.CONTENT_LEN_ZIP_FILE);
            contentLenReader = new ZipObjectReader(new ZipInputStream(new FileInputStream(contentLenFile)));

            while (!contentLenReader.getNextEntry().getName().equals(restoreRule.getSrcTableName()));
         }

         // get information about source table
         int sourceColumnCount = contentReader.readInt();

         List<Integer> columnType = new ArrayList<Integer>();
         List<String> columnName = new ArrayList<String>();

         for (int i = 0; i < sourceColumnCount; i++)
         {
            columnType.add(contentReader.readInt());
            columnName.add(contentReader.readString());
         }

         int targetColumnCount = sourceColumnCount;
         if (restoreRule.getNewColumnIndex() != null)
         {
            targetColumnCount++;

            columnType.add(restoreRule.getNewColumnIndex(), restoreRule.getNewColumnType());

            String newColumnName =
               dialect.startsWith(DBConstants.DB_DIALECT_PGSQL) ? restoreRule.getNewColumnName().toLowerCase()
                  : restoreRule.getNewColumnName();
            columnName.add(restoreRule.getNewColumnIndex(), newColumnName);
         }

         // construct statement
         StringBuilder names = new StringBuilder();
         StringBuilder parameters = new StringBuilder();
         for (int i = 0; i < targetColumnCount; i++)
         {
            if (restoreRule.getSkipColumnIndex() != null && restoreRule.getSkipColumnIndex() == i)
            {
               continue;
            }
            names.append(columnName.get(i)).append(i == targetColumnCount - 1 ? "" : ",");
            parameters.append("?").append(i == targetColumnCount - 1 ? "" : ",");
         }

         int batchSize = 0;
         insertNode =
            jdbcConn.prepareStatement("INSERT INTO " + tableName + " (" + names + ") VALUES(" + parameters + ")");

         // set data
         outer : while (true)
         {
            for (int i = 0, targetIndex = 0; i < columnType.size(); i++, targetIndex++)
            {
               InputStream stream;
               long len;

               if (restoreRule.getNewColumnIndex() != null && restoreRule.getNewColumnIndex() == i)
               {
                  stream =
                     new ByteArrayInputStream(restoreRule.getDstContainerName().getBytes(Constants.DEFAULT_ENCODING));
                  len = ((ByteArrayInputStream)stream).available();
               }
               else
               {
                  try
                  {
                     len = contentLenReader.readLong();
                  }
                  catch (EOFException e)
                  {
                     if (i == 0)
                     {
                        // content length file is empty check content file
                        try
                        {
                           contentReader.readByte();
                        }
                        catch (EOFException e1)
                        {
                           break outer;
                        }
                     }

                     throw new IOException("Content length file is empty but content still present", e);
                  }
                  stream = len == -1 ? null : spoolInputStream(contentReader, len);
               }

               if (restoreRule.getSkipColumnIndex() != null && restoreRule.getSkipColumnIndex() == i)
               {
                  targetIndex--;
                  continue;
               }

               // set 
               if (stream != null)
               {
                  if (restoreRule.getConvertColumnIndex() != null && restoreRule.getConvertColumnIndex().contains(i))
                  {
                     // convert column value
                     ByteArrayInputStream ba = (ByteArrayInputStream)stream;
                     byte[] readBuffer = new byte[ba.available()];
                     ba.read(readBuffer);

                     String currentValue = new String(readBuffer, Constants.DEFAULT_ENCODING);
                     if (currentValue.equals(Constants.ROOT_PARENT_UUID))
                     {
                        stream = new ByteArrayInputStream(Constants.ROOT_PARENT_UUID.getBytes());
                     }
                     else
                     {
                        if (restoreRule.getDstMultiDb())
                        {
                           if (!restoreRule.getSrcMultiDb())
                           {
                              stream =
                                 new ByteArrayInputStream(new String(readBuffer, Constants.DEFAULT_ENCODING).substring(
                                    restoreRule.getSrcContainerName().length()).getBytes());
                           }
                        }
                        else
                        {
                           if (restoreRule.getSrcMultiDb())
                           {
                              StringBuilder builder = new StringBuilder();
                              builder.append(restoreRule.getDstContainerName());
                              builder.append(currentValue);

                              stream = new ByteArrayInputStream(builder.toString().getBytes());
                           }
                           else
                           {
                              StringBuilder builder = new StringBuilder();
                              builder.append(restoreRule.getDstContainerName());
                              builder.append(new String(readBuffer, Constants.DEFAULT_ENCODING).substring(restoreRule
                                 .getSrcContainerName().length()));

                              stream = new ByteArrayInputStream(builder.toString().getBytes());
                           }
                        }
                     }

                     len = ((ByteArrayInputStream)stream).available();
                  }

                  if (columnType.get(i) == Types.INTEGER || columnType.get(i) == Types.BIGINT
                     || columnType.get(i) == Types.SMALLINT || columnType.get(i) == Types.TINYINT)
                  {
                     ByteArrayInputStream ba = (ByteArrayInputStream)stream;
                     byte[] readBuffer = new byte[ba.available()];
                     ba.read(readBuffer);

                     String value = new String(readBuffer, Constants.DEFAULT_ENCODING);
                     insertNode.setLong(targetIndex + 1, Integer.parseInt(value));
                  }
                  else if (columnType.get(i) == Types.BIT)
                  {
                     ByteArrayInputStream ba = (ByteArrayInputStream)stream;
                     byte[] readBuffer = new byte[ba.available()];
                     ba.read(readBuffer);

                     String value = new String(readBuffer);
                     if (dialect.startsWith(DBConstants.DB_DIALECT_PGSQL))
                     {
                        insertNode.setBoolean(targetIndex + 1, value.equalsIgnoreCase("t"));
                     }
                     else
                     {
                        insertNode.setBoolean(targetIndex + 1, value.equals("1"));
                     }
                  }
                  else if (columnType.get(i) == Types.BOOLEAN)
                  {
                     ByteArrayInputStream ba = (ByteArrayInputStream)stream;
                     byte[] readBuffer = new byte[ba.available()];
                     ba.read(readBuffer);

                     String value = new String(readBuffer);
                     insertNode.setBoolean(targetIndex + 1, value.equalsIgnoreCase("true"));
                  }
                  else if (columnType.get(i) == Types.VARBINARY || columnType.get(i) == Types.LONGVARBINARY
                     || columnType.get(i) == Types.BLOB || columnType.get(i) == Types.BINARY
                     || columnType.get(i) == Types.OTHER)
                  {
                     insertNode.setBinaryStream(targetIndex + 1, stream, (int)len);
                  }
                  else
                  {
                     byte[] readBuffer = new byte[(int)len];
                     stream.read(readBuffer);

                     insertNode.setString(targetIndex + 1, new String(readBuffer, Constants.DEFAULT_ENCODING));
                  }
               }
               else
               {
                  insertNode.setNull(targetIndex + 1, columnType.get(i));
               }
            }

            // add statement to batch
            insertNode.addBatch();

            if (++batchSize == MAXIMUM_BATCH_SIZE)
            {
               insertNode.executeBatch();

               commitBatch();

               batchSize = 0;
            }
         }

         if (batchSize != 0)
         {
            insertNode.executeBatch();

            commitBatch();
         }
         if (useSequence)
         {
            batchSize = 0;
            String update = "DROP SEQUENCE " + tableName + "_seq";
            stmt = jdbcConn.createStatement();
            if ((dialect.startsWith(DBConstants.DB_DIALECT_MYSQL) || dialect.startsWith(DBConstants.DB_DIALECT_MSSQL)
               || dialect.startsWith(DBConstants.DB_DIALECT_SYBASE)) && tableName.equalsIgnoreCase(this.itemTableName))
            {
               boolean exist = checkEntry(jdbcConn, tableName + "_SEQ");
               if (exist)
               {
                  insertNode =
                     jdbcConn.prepareStatement("UPDATE " + tableName + "_SEQ  SET  nextVal=?  where name='LAST_N_ORDER_NUM'");
               }
               else
               {
                  insertNode =
                     jdbcConn.prepareStatement("INSERT INTO " + tableName + "_SEQ  (name, nextVal) VALUES ('LAST_N_ORDER_NUM', ?)");
               }
               insertNode.setInt(1, getStartValue(jdbcConn, tableName));
               insertNode.executeUpdate();
               batchSize++;

            }
            else if ((dialect.startsWith(DBConstants.DB_DIALECT_PGSQL) || dialect.startsWith(DBConstants.DB_DIALECT_DB2) || dialect.startsWith(DBConstants.DB_DIALECT_HSQLDB)
            ) && (tableName.equalsIgnoreCase(this.itemTableName)))

            {
               stmt.execute(update);
               update = "CREATE SEQUENCE " + tableName + "_seq  INCREMENT BY 1 MINVALUE -1 NO MAXVALUE  NO CYCLE START WITH " + (getStartValue(jdbcConn, tableName) + 1);
               stmt.execute(update);
               batchSize++;
            }
            else if (dialect.startsWith(DBConstants.DB_DIALECT_H2) && (tableName.equalsIgnoreCase(this.itemTableName)))
            {
               stmt.execute(update);
               update = "CREATE SEQUENCE " + tableName + "_seq  INCREMENT BY 1 START WITH " + (getStartValue(jdbcConn, tableName) + 1);
               stmt.execute(update);
               batchSize++;
            }
            else if (dialect.startsWith(DBConstants.DB_DIALECT_ORACLE) && tableName.equalsIgnoreCase(this.itemTableName))
            {
               stmt.execute(update);
               update = "CREATE SEQUENCE " + tableName + "_seq  INCREMENT BY 1 MINVALUE -1 NOMAXVALUE NOCACHE NOCYCLE START WITH " + (getStartValue(jdbcConn, tableName) + 1);
               stmt.execute(update);
               batchSize++;

            }
            if (batchSize != 0)
            {

               commitBatch();
            }
         }
      }
      finally
      {
         if (contentReader != null)
         {
            contentReader.close();
         }

         if (contentLenReader != null)
         {
            contentLenReader.close();
         }

         if (insertNode != null)
         {
            insertNode.close();
         }

         if (stmt != null)
         {
            stmt.close();
         }

         // delete all temporary files
         for (File file : spoolFileList)
         {
            if (!file.delete())
            {
               fileCleaner.addFile(file);
            }
         }

         if (tableMetaData != null)
         {
            tableMetaData.close();
         }
      }
   }

   /**
    * Committing changes from batch.
    */
   protected void commitBatch() throws SQLException
   {
      // commit every batch for sybase
      if (dialect.startsWith(DBConstants.DB_DIALECT_SYBASE))
      {
         jdbcConn.commit();
      }
   }

   /**
    * Spool input stream.
    */
   private InputStream spoolInputStream(ObjectReader in, long contentLen) throws IOException
   {
      byte[] buffer = new byte[0];
      byte[] tmpBuff;
      long readLen = 0;
      File sf = null;
      OutputStream sfout = null;

      try
      {
         while (true)
         {
            int needToRead = contentLen - readLen > 2048 ? 2048 : (int)(contentLen - readLen);
            tmpBuff = new byte[needToRead];

            if (needToRead == 0)
            {
               break;
            }

            in.readFully(tmpBuff);

            if (sfout != null)
            {
               sfout.write(tmpBuff);
            }
            else if (readLen + needToRead > maxBufferSize && fileCleaner != null)
            {
               sf = File.createTempFile("jcrvd", null, tempDir);
               sfout = new FileOutputStream(sf);

               sfout.write(buffer);
               sfout.write(tmpBuff);
               buffer = null;
            }
            else
            {
               // reallocate new buffer and spool old buffer contents
               byte[] newBuffer = new byte[(int)(readLen + needToRead)];
               System.arraycopy(buffer, 0, newBuffer, 0, (int)readLen);
               System.arraycopy(tmpBuff, 0, newBuffer, (int)readLen, needToRead);
               buffer = newBuffer;
            }

            readLen += needToRead;
         }

         if (buffer != null)
         {
            return new ByteArrayInputStream(buffer);
         }
         else
         {
            return new FileInputStream(sf);
         }
      }
      finally
      {
         if (sfout != null)
         {
            sfout.close();
         }

         if (sf != null)
         {
            spoolFileList.add(sf);
         }
      }
   }

   /**
    * Init Start value for sequence.
    */
   private int getStartValue(Connection con, String table)
   {

      Statement stmt = null;
      ResultSet trs = null;
      try
      {
         String query = "select max(N_ORDER_NUM) from " + table;
         stmt = con.createStatement();
         trs = stmt.executeQuery(query);
         if (trs.next() && trs.getInt(1) >= 0)
         {
            return trs.getInt(1);
         }
         else
         {
            return -1;
         }

      }
      catch (SQLException e)
      {
         if (LOG.isDebugEnabled())
         {
            LOG.debug("SQLException occurred while calculate the sequence start value", e);
         }
         return -1;
      }
      finally
      {
         JDBCUtils.freeResources(trs, stmt, null);
      }
   }

   /**
    * Check if LAST_N_ORDER_NUM row exists.
    */
   private boolean checkEntry(Connection con, String table)
   {

      Statement stmt = null;
      ResultSet trs = null;
      try
      {
         String query = "select count(*) from " + table +"  where name ='LAST_N_ORDER_NUM'";
         stmt = con.createStatement();
         trs = stmt.executeQuery(query);
         if (trs.next() && trs.getInt(1) > 0)
         {
            return true;
         }
      }
      catch (SQLException e)
      {
         if (LOG.isDebugEnabled())
         {
            LOG.debug("SQLException occurred while check the table " + table +"  entry ", e);
         }
      }
      finally
      {
         JDBCUtils.freeResources(trs, stmt, null);
      }
      return false;
   }
}


