/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2012-2015 ForgeRock AS.
 */

package org.opends.server.tools.upgrade;



import static org.opends.messages.ToolMessages.*;
import static com.forgerock.opendj.util.OperatingSystem.isUnix;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.FilePermission;
import org.opends.server.util.StaticUtils;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import org.forgerock.i18n.slf4j.LocalizedLogger;




/**
 * Utility class for use by applications containing methods for managing file
 * system files. This class handles application notifications for interesting
 * events.
 */
class FileManager
{

  /**
   * Describes the approach taken to deleting a file or directory.
   */
  public static enum DeletionPolicy
  {

    /**
     * Delete the file or directory immediately.
     */
    DELETE_IMMEDIATELY,

    /**
     * Mark the file or directory for deletion after the JVM has exited.
     */
    DELETE_ON_EXIT,

    /**
     * First try to delete the file immediately. If the deletion was
     * unsuccessful mark the file for deleteion when the JVM has existed.
     */
    DELETE_ON_EXIT_IF_UNSUCCESSFUL

  }



  /**
   * Upgrade's Log.
   */
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();



  private FileManager()
  {
    // do nothing;
  }



  /**
   * Deletes everything below the specified file.
   *
   * @param file
   *          the path to be deleted.
   * @throws IOException
   *           if something goes wrong.
   */
  public static void deleteRecursively(File file) throws IOException
  {
    deleteRecursively(file, null, DeletionPolicy.DELETE_IMMEDIATELY);
  }



  private static void deleteRecursively(File file, FileFilter filter,
      DeletionPolicy deletePolicy) throws IOException
  {
    operateRecursively(new DeleteOperation(file, deletePolicy), filter);
  }



  /**
   * Copies everything below the specified file.
   *
   * @param objectFile
   *          the file to be copied.
   * @param destDir
   *          the directory to copy the file to
   * @param overwrite
   *          overwrite destination files.
   * @return File representing the destination
   * @throws IOException
   *           if something goes wrong.
   */
  public static File copy(File objectFile, File destDir, boolean overwrite)
      throws IOException
  {
    CopyOperation co = new CopyOperation(objectFile, destDir, overwrite);
    co.apply();
    return co.getDestination();
  }



  private static void operateRecursively(FileOperation op, FileFilter filter)
      throws IOException
  {
    File file = op.getObjectFile();
    if (file.exists())
    {
      if (file.isFile())
      {
        if (filter != null)
        {
          if (filter.accept(file))
          {
            op.apply();
          }
        }
        else
        {
          op.apply();
        }
      }
      else
      {
        File[] children = file.listFiles();
        if (children != null)
        {
          for (File aChildren : children)
          {
            FileOperation newOp = op.copyForChild(aChildren);
            operateRecursively(newOp, filter);
          }
        }
        if (filter != null)
        {
          if (filter.accept(file))
          {
            op.apply();
          }
        }
        else
        {
          op.apply();
        }
      }
    }
    else
    {
      logger.debug(LocalizableMessage.raw("File '" + file + "' does not exist"));
    }
  }



  /**
   * Renames the source file to the target file. If the target file exists it is
   * first deleted. The rename and delete operation return values are checked
   * for success and if unsuccessful, this method throws an exception.
   *
   * @param fileToRename
   *          The file to rename.
   * @param target
   *          The file to which <code>fileToRename</code> will be moved.
   * @throws IOException
   *           If a problem occurs while attempting to rename the file. On the
   *           Windows platform, this typically indicates that the file is in
   *           use by this or another application.
   */
  public static void rename(File fileToRename, File target) throws IOException
  {
    if (fileToRename != null && target != null)
    {
      synchronized (target)
      {
        if (target.exists() && !target.delete())
        {
          throw new IOException(INFO_ERROR_DELETING_FILE.get(
              UpgradeUtils.getPath(target)).toString());
        }
      }
      if (!fileToRename.renameTo(target))
      {
        throw new IOException(INFO_ERROR_RENAMING_FILE.get(
            UpgradeUtils.getPath(fileToRename), UpgradeUtils.getPath(target))
            .toString());
      }
    }
  }



  /**
   * A file operation.
   */
  private static abstract class FileOperation
  {

    private File objectFile;



    /**
     * Creates a new file operation.
     *
     * @param objectFile
     *          to be operated on
     */
    public FileOperation(File objectFile)
    {
      this.objectFile = objectFile;
    }



    /**
     * Gets the file to be operated on.
     *
     * @return File to be operated on
     */
    protected File getObjectFile()
    {
      return objectFile;
    }



    /**
     * Make a copy of this class for the child file.
     *
     * @param child
     *          to act as the new file object
     * @return FileOperation as the same type as this class
     */
    public abstract FileOperation copyForChild(File child);



    /**
     * Execute this operation.
     *
     * @throws IOException
     *           if there is a problem.
     */
    public abstract void apply() throws IOException;

  }



  /**
   * A copy operation.
   */
  private static class CopyOperation extends FileOperation
  {

    private File destination;

    private boolean overwrite;



    /**
     * Create a new copy operation.
     *
     * @param objectFile
     *          to copy
     * @param destDir
     *          to copy to
     * @param overwrite
     *          if true copy should overwrite any existing file
     */
    public CopyOperation(File objectFile, File destDir, boolean overwrite)
    {
      super(objectFile);
      this.destination = new File(destDir, objectFile.getName());
      this.overwrite = overwrite;
    }



    /** {@inheritDoc} */
    public FileOperation copyForChild(File child)
    {
      return new CopyOperation(child, destination, overwrite);
    }



    /**
     * Returns the destination file that is the result of copying
     * <code>objectFile</code> to <code>destDir</code>.
     *
     * @return The destination file.
     */
    public File getDestination()
    {
      return this.destination;
    }



    /** {@inheritDoc} */
    public void apply() throws IOException
    {
      final File objectFile = getObjectFile();
      if (objectFile.isDirectory())
      {
        if (!destination.exists())
        {
          destination.mkdirs();
        }
      }
      else
      {
        // If overwriting and the destination exists then kill it
        if (destination.exists() && overwrite)
        {
          deleteRecursively(destination);
        }

        if (!destination.exists())
        {
          if (insureParentsExist(destination))
          {
            logger.debug(LocalizableMessage.raw("Copying file '%s' to '%s'",
                objectFile.getAbsolutePath(), destination.getAbsolutePath()));
            FileInputStream fis = null;
            FileOutputStream fos = null;
            try
            {
              fis = new FileInputStream(objectFile);
              fos = new FileOutputStream(destination);
              final byte[] buf = new byte[1024];
              int i;
              while ((i = fis.read(buf)) != -1)
              {
                fos.write(buf, 0, i);
              }
              if (destination.exists() && isUnix())
              {
                // TODO: set the file's permissions. This is made easier in
                // Java 1.6 but until then use the TestUtilities methods
                final FilePermission permissions = getFileSystemPermissions(objectFile);
                FilePermission.setPermissions(destination, permissions);
              }
            }
            catch (IOException e)
            {
              throw e;
            }
            catch (Exception e)
            {
              final LocalizableMessage errMsg = INFO_ERROR_COPYING_FILE.get(
                  objectFile.getAbsolutePath(), destination.getAbsolutePath());
              throw new IOException(errMsg.toString(), e);
            }
            finally
            {
              StaticUtils.close(fis, fos);
            }
          }
          else
          {
            final LocalizableMessage errMsg = INFO_ERROR_COPYING_FILE.get(
                objectFile.getAbsolutePath(), destination.getAbsolutePath());
            logger.error(LocalizableMessage.raw(errMsg));
            throw new IOException(errMsg.toString());
          }
        }
        else
        {
          logger.debug(LocalizableMessage.raw(
              "Ignoring file '%s' since '%s' already exists",
              objectFile.getAbsolutePath(), destination.getAbsolutePath()));
        }
      }
    }

  }



  /**
   * Returns the file permission on the selected file.
   *
   * @param file
   *          The file of which we want to extract the permissions.
   * @return A file permission about the concerned file.
   * @throws DirectoryException
   *           If the provided string is not a valid three-digit UNIX mode.
   */
  private static FilePermission getFileSystemPermissions(final File file)
      throws DirectoryException
  {
    final String name = file.getName();
    if (file.getParent().endsWith(
        File.separator + Installation.WINDOWS_BINARIES_PATH_RELATIVE)
        || file.getParent().endsWith(
            File.separator + Installation.UNIX_BINARIES_PATH_RELATIVE))
    {
      if (name.endsWith(".bat"))
      {
        return FilePermission.decodeUNIXMode("644");
      }
      return FilePermission.decodeUNIXMode("755");

    }
    else if (name.endsWith(".sh"))
    {
      return FilePermission.decodeUNIXMode("755");
    }
    else if (name.endsWith(Installation.UNIX_SETUP_FILE_NAME)
        || name.endsWith(Installation.UNIX_UNINSTALL_FILE_NAME)
        || name.endsWith(Installation.UNIX_UPGRADE_FILE_NAME))
    {
      return FilePermission.decodeUNIXMode("755");
    }
    else if (name.endsWith(Installation.MAC_JAVA_APP_STUB_NAME))
    {
      return FilePermission.decodeUNIXMode("755");
    }
    return FilePermission.decodeUNIXMode("644");
  }



  /**
   * Creates the parent directory if it does not already exist.
   *
   * @param f
   *          File for which parentage will be insured
   * @return boolean indicating whether or not the input <code>f</code> has a
   *         parent after this method is invoked.
   */
  private static boolean insureParentsExist(File f)
  {
    File parent = f.getParentFile();
    boolean b = parent.exists();
    if (!b)
    {
      b = parent.mkdirs();
    }
    return b;
  }



  /**
   * A delete operation.
   */
  private static class DeleteOperation extends FileOperation
  {

    private DeletionPolicy deletionPolicy;



    /**
     * Creates a delete operation.
     *
     * @param objectFile
     *          to delete
     * @param deletionPolicy
     *          describing how files will be deleted is to take place after this
     *          program exists. This is useful for cleaning up files that are
     *          currently in use.
     */
    public DeleteOperation(File objectFile, DeletionPolicy deletionPolicy)
    {
      super(objectFile);
      this.deletionPolicy = deletionPolicy;
    }



    /** {@inheritDoc} */
    public FileOperation copyForChild(File child)
    {
      return new DeleteOperation(child, deletionPolicy);
    }



    /** {@inheritDoc} */
    public void apply() throws IOException
    {
      File file = getObjectFile();
      boolean isFile = file.isFile();

      logger.debug(LocalizableMessage.raw("deleting " + (isFile ? " file " : " directory ")
          + file.getAbsolutePath()));

      boolean delete = false;
      /*
       * Sometimes the server keeps some locks on the files. TODO: remove this
       * code once stop-ds returns properly when server is stopped.
       */
      int nTries = 5;
      for (int i = 0; i < nTries && !delete; i++)
      {
        if (DeletionPolicy.DELETE_ON_EXIT.equals(deletionPolicy))
        {
          file.deleteOnExit();
          delete = true;
        }
        else
        {
          delete = file.delete();
          if (!delete
              && DeletionPolicy.DELETE_ON_EXIT_IF_UNSUCCESSFUL
                  .equals(deletionPolicy))
          {
            file.deleteOnExit();
            delete = true;
          }
        }
        if (!delete)
        {
          try
          {
            Thread.sleep(1000);
          }
          catch (Exception ex)
          {
            // do nothing;
          }
        }
      }

      if (!delete)
      {
        LocalizableMessage errMsg;
        if (isFile)
        {
          errMsg = INFO_ERROR_DELETING_FILE.get(file.getAbsolutePath());
        }
        else
        {
          errMsg = INFO_ERROR_DELETING_DIRECTORY.get(file.getAbsolutePath());
        }
        throw new IOException(errMsg.toString());
      }
    }
  }

}
