/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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-2009 Sun Microsystems, Inc.
 */
package org.opends.server.backends.jeb;
import org.opends.messages.Message;

import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.util.DynamicConstants;
import org.opends.server.types.CryptoManagerException;

import javax.crypto.Mac;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.opends.server.types.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.*;
import org.opends.server.loggers.debug.DebugTracer;
import static org.opends.messages.JebMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;



/**
 * A backup manager for JE backends.
 */
public class BackupManager
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();

  /**
   * The common prefix for archive files.
   */
  public static final String BACKUP_BASE_FILENAME = "backup-";

  /**
   * The name of the property that holds the name of the latest log file
   * at the time the backup was created.
   */
  public static final String PROPERTY_LAST_LOGFILE_NAME = "last_logfile_name";

  /**
   * The name of the property that holds the size of the latest log file
   * at the time the backup was created.
   */
  public static final String PROPERTY_LAST_LOGFILE_SIZE = "last_logfile_size";


  /**
   * The name of the entry in an incremental backup archive file
   * containing a list of log files that are unchanged since the
   * previous backup.
   */
  public static final String ZIPENTRY_UNCHANGED_LOGFILES = "unchanged.txt";

  /**
   * The name of a dummy entry in the backup archive file that will act
   * as a placeholder in case a backup is done on an empty backend.
   */
  public static final String ZIPENTRY_EMPTY_PLACEHOLDER = "empty.placeholder";


  /**
   * The backend ID.
   */
  private String backendID;


  /**
   * Construct a backup manager for a JE backend.
   * @param backendID The ID of the backend instance for which a backup
   * manager is required.
   */
  public BackupManager(String backendID)
  {
    this.backendID   = backendID;
  }

  /**
   * Create a backup of the JE backend.  The backup is stored in a single zip
   * file in the backup directory.  If the backup is incremental, then the
   * first entry in the zip is a text file containing a list of all the JE
   * log files that are unchanged since the previous backup.  The remaining
   * zip entries are the JE log files themselves, which, for an incremental,
   * only include those files that have changed.
   * @param backendDir The directory of the backend instance for
   * which the backup is required.
   * @param  backupConfig  The configuration to use when performing the backup.
   * @throws DirectoryException If a Directory Server error occurs.
   */
  public void createBackup(File backendDir, BackupConfig backupConfig)
       throws DirectoryException
  {
    // Get the properties to use for the backup.
    String          backupID        = backupConfig.getBackupID();
    BackupDirectory backupDir       = backupConfig.getBackupDirectory();
    boolean         incremental     = backupConfig.isIncremental();
    String          incrBaseID      = backupConfig.getIncrementalBaseID();
    boolean         compress        = backupConfig.compressData();
    boolean         encrypt         = backupConfig.encryptData();
    boolean         hash            = backupConfig.hashData();
    boolean         signHash        = backupConfig.signHash();


    // Create a hash map that will hold the extra backup property information
    // for this backup.
    HashMap<String,String> backupProperties = new HashMap<String,String>();


    // Get the crypto manager and use it to obtain references to the message
    // digest and/or MAC to use for hashing and/or signing.
    CryptoManager cryptoManager   = DirectoryServer.getCryptoManager();
    Mac           mac             = null;
    MessageDigest digest          = null;
    String        digestAlgorithm = null;
    String        macKeyID    = null;

    if (hash)
    {
      if (signHash)
      {
        try
        {
          macKeyID = cryptoManager.getMacEngineKeyEntryID();
          backupProperties.put(BACKUP_PROPERTY_MAC_KEY_ID, macKeyID);

          mac = cryptoManager.getMacEngine(macKeyID);
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          Message message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get(
              macKeyID, stackTraceToSingleLineString(e));
          throw new DirectoryException(
               DirectoryServer.getServerErrorResultCode(), message, e);
        }
      }
      else
      {
        digestAlgorithm = cryptoManager.getPreferredMessageDigestAlgorithm();
        backupProperties.put(BACKUP_PROPERTY_DIGEST_ALGORITHM, digestAlgorithm);

        try
        {
          digest = cryptoManager.getPreferredMessageDigest();
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          Message message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get(
              digestAlgorithm, stackTraceToSingleLineString(e));
          throw new DirectoryException(
               DirectoryServer.getServerErrorResultCode(), message, e);
        }
      }
    }


    // Date the backup.
    Date backupDate = new Date();

    // If this is an incremental, determine the base backup for this backup.
    HashSet<String> dependencies = new HashSet<String>();
    BackupInfo baseBackup = null;
/*
    FilenameFilter backupTagFilter = new FilenameFilter()
    {
      public boolean accept(File dir, String name)
      {
        return name.startsWith(BackupInfo.PROPERTY_BACKUP_ID);
      }
    };
*/
    if (incremental)
    {
      if (incrBaseID == null)
      {
        // The default is to use the latest backup as base.
        if (backupDir.getLatestBackup() != null)
        {
          incrBaseID = backupDir.getLatestBackup().getBackupID();
        }
      }

      // Get the set of possible base backups from the current database.
/*
      String[] files = backendDir.list(backupTagFilter);
      if (files == null || files.length == 0)
      {
        // Incremental not allowed until after a full.
        Message msg = ERR_JEB_INCR_BACKUP_REQUIRES_FULL.get();
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     msg);
      }
      HashSet<String> backups = new HashSet<String>();
      int prefixLen = BackupInfo.PROPERTY_BACKUP_ID.length()+1;
      for (String s : files)
      {
        String actualBaseID = s.substring(prefixLen);
        backups.add(actualBaseID);
      }

      // Check that it makes sense to do this incremental.
      if (incrBaseID == null || !backups.contains(incrBaseID))
      {
        Message msg =
            ERR_JEB_INCR_BACKUP_FROM_WRONG_BASE.get(backups.toString());
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     msg);
      }
*/

      if (incrBaseID == null)
      {
        // No incremental backup ID: log a message informing that a backup
        // could not be found and that a normal backup will be done.
        incremental = false;
        Message message = WARN_BACKUPDB_INCREMENTAL_NOT_FOUND_DOING_NORMAL.get(
            backupDir.getPath());
        logError(message);
      }
      else
      {
        baseBackup = getBackupInfo(backupDir, incrBaseID);
      }
    }

    // Get information about the latest log file from the base backup.
    String latestFileName = null;
    long latestFileSize = 0;
    if (baseBackup != null)
    {
      HashMap<String,String> properties = baseBackup.getBackupProperties();
      latestFileName = properties.get(PROPERTY_LAST_LOGFILE_NAME);
      latestFileSize = Long.parseLong(
           properties.get(PROPERTY_LAST_LOGFILE_SIZE));
    }

    // Create an output stream that will be used to write the archive file.  At
    // its core, it will be a file output stream to put a file on the disk.  If
    // we are to encrypt the data, then that file output stream will be wrapped
    // in a cipher output stream.  The resulting output stream will then be
    // wrapped by a zip output stream (which may or may not actually use
    // compression).
    String archiveFilename = null;
    OutputStream outputStream;
    File archiveFile;
    try
    {
      archiveFilename = BACKUP_BASE_FILENAME + backendID + "-" + backupID;
      archiveFile = new File(backupDir.getPath(), archiveFilename);
      if (archiveFile.exists())
      {
        int i=1;
        while (true)
        {
          archiveFile = new File(backupDir.getPath(),
                                 archiveFilename  + "." + i);
          if (archiveFile.exists())
          {
            i++;
          }
          else
          {
            archiveFilename = archiveFilename + "." + i;
            break;
          }
        }
      }

      outputStream = new FileOutputStream(archiveFile, false);
      backupProperties.put(BACKUP_PROPERTY_ARCHIVE_FILENAME, archiveFilename);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message = ERR_JEB_BACKUP_CANNOT_CREATE_ARCHIVE_FILE.
          get(String.valueOf(archiveFilename), backupDir.getPath(),
              stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message, e);
    }


    // If we should encrypt the data, then wrap the output stream in a cipher
    // output stream.
    if (encrypt)
    {
      try
      {
        outputStream
                = cryptoManager.getCipherOutputStream(outputStream);
      }
      catch (CryptoManagerException e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get(
                stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }


    // Wrap the file output stream in a zip output stream.
    ZipOutputStream zipStream = new ZipOutputStream(outputStream);

    Message message = ERR_JEB_BACKUP_ZIP_COMMENT.get(
            DynamicConstants.PRODUCT_NAME,
            backupID, backendID);
    zipStream.setComment(message.toString());

    if (compress)
    {
      zipStream.setLevel(Deflater.DEFAULT_COMPRESSION);
    }
    else
    {
      zipStream.setLevel(Deflater.NO_COMPRESSION);
    }

    // Record this backup in the database itself.
/*
    String backupTag = BackupInfo.PROPERTY_BACKUP_ID + "_" + backupID;
    File tagFile = new File(backendDir, backupTag);
    try
    {
      tagFile.createNewFile();
    }
    catch (IOException e)
    {
      assert debugException(CLASS_NAME, "createBackup", e);
      Message msg = ERR_JEB_CANNOT_CREATE_BACKUP_TAG_FILE.get(
          backupTag, backendDir.getPath());
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   msg);
    }
*/

    // Get a list of all the log files comprising the database.
    FilenameFilter filenameFilter = new FilenameFilter()
    {
      public boolean accept(File d, String name)
      {
        return name.endsWith(".jdb");
      }
    };

    File[] logFiles;
    try
    {
      logFiles = backendDir.listFiles(filenameFilter);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get(
          backendDir.getAbsolutePath(), stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
          message, e);
    }

    // Check to see if backend is empty. If so, insert placeholder entry into
    // archive
    if(logFiles.length <= 0)
    {
      try
      {
        ZipEntry emptyPlaceholder = new ZipEntry(ZIPENTRY_EMPTY_PLACEHOLDER);
        zipStream.putNextEntry(emptyPlaceholder);
      }
      catch (IOException e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(
            ZIPENTRY_EMPTY_PLACEHOLDER, stackTraceToSingleLineString(e));
        throw new DirectoryException(
            DirectoryServer.getServerErrorResultCode(), message, e);
      }
    }

    // Sort the log files from oldest to youngest since this is the order
    // in which they must be copied.
    // This is easy since the files are created in alphabetical order by JE.
    Arrays.sort(logFiles);

    try
    {
      // Archive the backup tag files.
/*
      File[] tagFiles = backendDir.listFiles(backupTagFilter);
      if (tagFiles != null)
      {
        for (File f : tagFiles)
        {
          try
          {
            archiveFile(zipStream, mac, digest, f);
          }
          catch (IOException e)
          {
            assert debugException(CLASS_NAME, "createBackup", e);
            Message message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(
                backupTag, stackTraceToSingleLineString(e));
            throw new DirectoryException(
                 DirectoryServer.getServerErrorResultCode(), message, e);
          }
        }
      }
*/

      // Process log files that are unchanged from the base backup.
      int indexCurrent = 0;
      if (latestFileName != null)
      {
        ArrayList<String> unchangedList = new ArrayList<String>();
        while (indexCurrent < logFiles.length &&
                !backupConfig.isCancelled())
        {
          File logFile = logFiles[indexCurrent];
          String logFileName = logFile.getName();

          // Stop when we get to the first log file that has been
          // written since the base backup.
          int compareResult = logFileName.compareTo(latestFileName);
          if (compareResult > 0 ||
               (compareResult == 0 && logFile.length() != latestFileSize))
          {
            break;
          }

          message = NOTE_JEB_BACKUP_FILE_UNCHANGED.get(logFileName);
          logError(message);

          unchangedList.add(logFileName);

          indexCurrent++;
        }

        // Write a file containing the list of unchanged log files.
        if (!unchangedList.isEmpty())
        {
          String zipEntryName = ZIPENTRY_UNCHANGED_LOGFILES;
          try
          {
            archiveList(zipStream, mac, digest, zipEntryName, unchangedList);
          }
          catch (IOException e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(
                zipEntryName, stackTraceToSingleLineString(e));
            throw new DirectoryException(
                 DirectoryServer.getServerErrorResultCode(), message, e);
          }

          // Set the dependency.
          dependencies.add(baseBackup.getBackupID());
        }
      }

      // Write the new log files to the zip file.
      do
      {
        boolean deletedFiles = false;

        while (indexCurrent < logFiles.length &&
                !backupConfig.isCancelled())
        {
          File logFile = logFiles[indexCurrent];

          try
          {
            latestFileSize = archiveFile(zipStream, mac, digest,
                                         logFile, backupConfig);
            latestFileName = logFile.getName();
          }
          catch (FileNotFoundException e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

            // A log file has been deleted by the cleaner since we started.
            deletedFiles = true;
          }
          catch (IOException e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(
                logFile.getName(), stackTraceToSingleLineString(e));
            throw new DirectoryException(
                 DirectoryServer.getServerErrorResultCode(), message, e);
          }

          indexCurrent++;
        }

        if (deletedFiles)
        {
          // The cleaner is active and has deleted one or more of the log files
          // since we started.  The in-use data from those log files will have
          // been written to new log files, so we must include those new files.
          final String latest = logFiles[logFiles.length-1].getName();
          final long latestSize = latestFileSize;
          FilenameFilter filter = new FilenameFilter()
          {
            public boolean accept(File d, String name)
            {
              if (!name.endsWith(".jdb")) return false;
              int compareTo = name.compareTo(latest);
              if (compareTo > 0) return true;
              if (compareTo == 0 && d.length() > latestSize) return true;
              return false;
            }
          };

          try
          {
            logFiles = backendDir.listFiles(filter);
            indexCurrent = 0;
          }
          catch (Exception e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

            message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get(
                backendDir.getAbsolutePath(), stackTraceToSingleLineString(e));
            throw new DirectoryException(
                 DirectoryServer.getServerErrorResultCode(), message, e);
          }

          if (logFiles == null)
          {
            break;
          }

          Arrays.sort(logFiles);

          message = NOTE_JEB_BACKUP_CLEANER_ACTIVITY.get(
                  String.valueOf(logFiles.length));
          logError(message);
        }
        else
        {
          // We are done.
          break;
        }
      }
      while (true);

    }
    catch (DirectoryException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      try
      {
        zipStream.close();
      } catch (Exception e2) {}
    }

    // We're done writing the file, so close the zip stream (which should also
    // close the underlying stream).
    try
    {
      zipStream.close();
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      message = ERR_JEB_BACKUP_CANNOT_CLOSE_ZIP_STREAM.
          get(archiveFilename, backupDir.getPath(),
              stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message, e);
    }


    // Get the digest or MAC bytes if appropriate.
    byte[] digestBytes = null;
    byte[] macBytes    = null;
    if (hash)
    {
      if (signHash)
      {
        macBytes = mac.doFinal();
      }
      else
      {
        digestBytes = digest.digest();
      }
    }


    // Create a descriptor for this backup.
    backupProperties.put(PROPERTY_LAST_LOGFILE_NAME, latestFileName);
    backupProperties.put(PROPERTY_LAST_LOGFILE_SIZE,
                         String.valueOf(latestFileSize));
    BackupInfo backupInfo = new BackupInfo(backupDir, backupID,
                                           backupDate, incremental, compress,
                                           encrypt, digestBytes, macBytes,
                                           dependencies, backupProperties);

    try
    {
      backupDir.addBackup(backupInfo);
      backupDir.writeBackupDirectoryDescriptor();
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get(
          backupDir.getDescriptorPath(), stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message, e);
    }

    // Remove the backup if this operation was cancelled since the
    // backup may be incomplete
    if (backupConfig.isCancelled())
    {
      removeBackup(backupDir, backupID);
    }
  }



  /**
   * Restore a JE backend from backup, or verify the backup.
   * @param backendDir The configuration of the backend instance to be
   * restored.
   * @param  restoreConfig The configuration to use when performing the restore.
   * @throws DirectoryException If a Directory Server error occurs.
   */
  public void restoreBackup(File backendDir,
                            RestoreConfig restoreConfig)
       throws DirectoryException
  {
    // Get the properties to use for the restore.
    String          backupID        = restoreConfig.getBackupID();
    BackupDirectory backupDir       = restoreConfig.getBackupDirectory();
    boolean         verifyOnly      = restoreConfig.verifyOnly();

    BackupInfo backupInfo = getBackupInfo(backupDir, backupID);

    // Create a restore directory with a different name to the backend
    // directory.
    File restoreDir = new File(backendDir.getPath() + "-restore-" + backupID);
    if (!verifyOnly)
    {
      File[] files = restoreDir.listFiles();
      if (files != null)
      {
        for (File f : files)
        {
          f.delete();
        }
      }
      restoreDir.mkdir();
    }

    // Get the set of restore files that are in dependencies.
    Set<String> includeFiles;
    try
    {
      includeFiles = getUnchanged(backupDir, backupInfo);
    }
    catch (IOException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get(
          backupInfo.getBackupID(), stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message, e);
    }

    // Restore any dependencies.
    List<BackupInfo> dependents = getDependents(backupDir, backupInfo);
    for (BackupInfo dependent : dependents)
    {
      try
      {
        restoreArchive(restoreDir, restoreConfig, dependent, includeFiles);
      }
      catch (IOException e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get(
            dependent.getBackupID(), stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }

    // Restore the final archive file.
    try
    {
      restoreArchive(restoreDir, restoreConfig, backupInfo, null);
    }
    catch (IOException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get(
          backupInfo.getBackupID(), stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message, e);
    }

    // Delete the current backend directory and rename the restore directory.
    if (!verifyOnly)
    {
      File[] files = backendDir.listFiles();
      if (files != null)
      {
        for (File f : files)
        {
          f.delete();
        }
      }
      backendDir.delete();
      if (!restoreDir.renameTo(backendDir))
      {
        Message msg = ERR_JEB_CANNOT_RENAME_RESTORE_DIRECTORY.get(
            restoreDir.getPath(), backendDir.getPath());
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     msg);
      }
    }
  }

  /**
   * Removes the specified backup if it is possible to do so.
   *
   * @param  backupDir  The backup directory structure with which the
   *                          specified backup is associated.
   * @param  backupID         The backup ID for the backup to be removed.
   *
   * @throws  DirectoryException  If it is not possible to remove the specified
   *                              backup for some reason (e.g., no such backup
   *                              exists or there are other backups that are
   *                              dependent upon it).
   */
  public void removeBackup(BackupDirectory backupDir,
                           String backupID)
         throws DirectoryException
  {
    BackupInfo backupInfo = getBackupInfo(backupDir, backupID);
    HashMap<String,String> backupProperties = backupInfo.getBackupProperties();

    String archiveFilename =
         backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME);
    File archiveFile = new File(backupDir.getPath(), archiveFilename);

    try
    {
      backupDir.removeBackup(backupID);
    }
    catch (ConfigException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   e.getMessageObject());
    }

    try
    {
      backupDir.writeBackupDirectoryDescriptor();
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get(
          backupDir.getDescriptorPath(), stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message, e);
    }

    // Remove the archive file.
    archiveFile.delete();

  }



  /**
   * Restore the contents of an archive file.  If the archive is being
   * restored as a dependency, then only files in the specified set
   * are restored, and the restored files are removed from the set.  Otherwise
   * all files from the archive are restored, and files that are to be found
   * in dependencies are added to the set.
   *
   * @param restoreDir     The directory in which files are to be restored.
   * @param restoreConfig  The restore configuration.
   * @param backupInfo     The backup containing the files to be restored.
   * @param includeFiles   The set of files to be restored.  If null, then
   *                       all files are restored.
   * @throws DirectoryException If a Directory Server error occurs.
   * @throws IOException   If an I/O exception occurs during the restore.
   */
  private void restoreArchive(File restoreDir,
                              RestoreConfig restoreConfig,
                              BackupInfo backupInfo,
                              Set<String> includeFiles)
       throws DirectoryException,IOException
  {
    BackupDirectory backupDir       = restoreConfig.getBackupDirectory();
    boolean verifyOnly              = restoreConfig.verifyOnly();

    String          backupID        = backupInfo.getBackupID();
    boolean         encrypt         = backupInfo.isEncrypted();
    byte[]          hash            = backupInfo.getUnsignedHash();
    byte[]          signHash        = backupInfo.getSignedHash();

    HashMap<String,String> backupProperties = backupInfo.getBackupProperties();

    String archiveFilename =
         backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME);
    File archiveFile = new File(backupDir.getPath(), archiveFilename);

    InputStream inputStream = new FileInputStream(archiveFile);

    // Get the crypto manager and use it to obtain references to the message
    // digest and/or MAC to use for hashing and/or signing.
    CryptoManager cryptoManager   = DirectoryServer.getCryptoManager();
    Mac           mac             = null;
    MessageDigest digest          = null;
    String        digestAlgorithm = null;
    String        macKeyID        = null;

    if (signHash != null)
    {
      macKeyID = backupProperties.get(BACKUP_PROPERTY_MAC_KEY_ID);

      try
      {
        mac = cryptoManager.getMacEngine(macKeyID);
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        Message message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get(
            macKeyID, stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }

    if (hash != null)
    {
      digestAlgorithm = backupProperties.get(BACKUP_PROPERTY_DIGEST_ALGORITHM);

      try
      {
        digest = cryptoManager.getMessageDigest(digestAlgorithm);
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        Message message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get(
            digestAlgorithm, stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }


    // If the data is encrypted, then wrap the input stream in a cipher
    // input stream.
    if (encrypt)
    {
      try
      {
        inputStream = cryptoManager.getCipherInputStream(inputStream);
      }
      catch (CryptoManagerException e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get(
            stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }


    // Wrap the file input stream in a zip input stream.
    ZipInputStream zipStream = new ZipInputStream(inputStream);

    // Iterate through the entries in the zip file.
    ZipEntry zipEntry = zipStream.getNextEntry();
    while (zipEntry != null && !restoreConfig.isCancelled())
    {
      String name = zipEntry.getName();

      if (name.equals(ZIPENTRY_EMPTY_PLACEHOLDER))
      {
        // This entry is treated specially to indicate a backup of an empty
        // backend was attempted.

        zipEntry = zipStream.getNextEntry();
        continue;
      }

      if (name.equals(ZIPENTRY_UNCHANGED_LOGFILES))
      {
        // This entry is treated specially. It is never restored,
        // and its hash is computed on the strings, not the bytes.
        if (mac != null || digest != null)
        {
          // The file name is part of the hash.
          if (mac != null)
          {
            mac.update(getBytes(name));
          }

          if (digest != null)
          {
            digest.update(getBytes(name));
          }

          InputStreamReader reader = new InputStreamReader(zipStream);
          BufferedReader bufferedReader = new BufferedReader(reader);
          String line = bufferedReader.readLine();
          while (line != null)
          {
            if (mac != null)
            {
              mac.update(getBytes(line));
            }

            if (digest != null)
            {
              digest.update(getBytes(line));
            }

            line = bufferedReader.readLine();
          }
        }

        zipEntry = zipStream.getNextEntry();
        continue;
      }

      // See if we need to restore the file.
      File file = new File(restoreDir, name);
      OutputStream outputStream = null;
      if (includeFiles == null || includeFiles.contains(zipEntry.getName()))
      {
        if (!verifyOnly)
        {
          outputStream = new FileOutputStream(file);
        }
      }

      if (outputStream != null || mac != null || digest != null)
      {
        if (verifyOnly)
        {
          Message message = NOTE_JEB_BACKUP_VERIFY_FILE.get(zipEntry.getName());
          logError(message);
        }

        // The file name is part of the hash.
        if (mac != null)
        {
          mac.update(getBytes(name));
        }

        if (digest != null)
        {
          digest.update(getBytes(name));
        }

        // Process the file.
        long totalBytesRead = 0;
        byte[] buffer = new byte[8192];
        int bytesRead = zipStream.read(buffer);
        while (bytesRead > 0 && !restoreConfig.isCancelled())
        {
          totalBytesRead += bytesRead;

          if (mac != null)
          {
            mac.update(buffer, 0, bytesRead);
          }

          if (digest != null)
          {
            digest.update(buffer, 0, bytesRead);
          }

          if (outputStream != null)
          {
            outputStream.write(buffer, 0, bytesRead);
          }

          bytesRead = zipStream.read(buffer);
        }

        if (outputStream != null)
        {
          outputStream.close();

          Message message = NOTE_JEB_BACKUP_RESTORED_FILE.get(
              zipEntry.getName(), totalBytesRead);
          logError(message);
        }
      }

      zipEntry = zipStream.getNextEntry();
    }

    zipStream.close();

    // Check the hash.
    if (digest != null)
    {
      if (!Arrays.equals(digest.digest(), hash))
      {
        Message message = ERR_JEB_BACKUP_UNSIGNED_HASH_ERROR.get(backupID);
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message);
      }
    }

    if (mac != null)
    {
      byte[] computedSignHash = mac.doFinal();

      if (!Arrays.equals(computedSignHash, signHash))
      {
        Message message = ERR_JEB_BACKUP_SIGNED_HASH_ERROR.get(backupID);
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message);
      }
    }
  }



  /**
   * Writes a file to an entry in the archive file.
   * @param zipStream The zip output stream to which the file is to be
   *                  written.
   * @param mac A message authentication code to be updated, if not null.
   * @param digest A message digest to be updated, if not null.
   * @param file The file to be written.
   * @return The number of bytes written from the file.
   * @throws FileNotFoundException If the file to be archived does not exist.
   * @throws IOException If an I/O error occurs while archiving the file.
   */
  private long archiveFile(ZipOutputStream zipStream,
                           Mac mac, MessageDigest digest, File file,
                           BackupConfig backupConfig)
       throws IOException, FileNotFoundException
  {
    ZipEntry zipEntry = new ZipEntry(file.getName());

    // Open the file for reading.
    InputStream inputStream = new FileInputStream(file);

    // Start the zip entry.
    zipStream.putNextEntry(zipEntry);

    // Put the name in the hash.
    if (mac != null)
    {
      mac.update(getBytes(file.getName()));
    }

    if (digest != null)
    {
      digest.update(getBytes(file.getName()));
    }

    // Write the file.
    long totalBytesRead = 0;
    byte[] buffer = new byte[8192];
    int bytesRead = inputStream.read(buffer);
    while (bytesRead > 0 && !backupConfig.isCancelled())
    {
      if (mac != null)
      {
        mac.update(buffer, 0, bytesRead);
      }

      if (digest != null)
      {
        digest.update(buffer, 0, bytesRead);
      }

      zipStream.write(buffer, 0, bytesRead);
      totalBytesRead += bytesRead;
      bytesRead = inputStream.read(buffer);
    }
    inputStream.close();

    // Finish the zip entry.
    zipStream.closeEntry();

    Message message = NOTE_JEB_BACKUP_ARCHIVED_FILE.get(zipEntry.getName());
    logError(message);

    return totalBytesRead;
  }

  /**
   * Write a list of strings to an entry in the archive file.
   * @param zipStream The zip output stream to which the entry is to be
   *                  written.
   * @param mac An optional MAC to be updated.
   * @param digest An optional message digest to be updated.
   * @param fileName The name of the zip entry to be written.
   * @param list A list of strings to be written.  The strings must not
   *             contain newlines.
   * @throws IOException If an I/O error occurs while writing the archive entry.
   */
  private void archiveList(ZipOutputStream zipStream,
                           Mac mac, MessageDigest digest, String fileName,
                           List<String> list)
       throws IOException
  {
    ZipEntry zipEntry = new ZipEntry(fileName);

    // Start the zip entry.
    zipStream.putNextEntry(zipEntry);

    // Put the name in the hash.
    if (mac != null)
    {
      mac.update(getBytes(fileName));
    }

    if (digest != null)
    {
      digest.update(getBytes(fileName));
    }

    Writer writer = new OutputStreamWriter(zipStream);
    for (String s : list)
    {
      if (mac != null)
      {
        mac.update(getBytes(s));
      }

      if (digest != null)
      {
        digest.update(getBytes(s));
      }

      writer.write(s);
      writer.write(EOL);
    }
    writer.flush();

    // Finish the zip entry.
    zipStream.closeEntry();
  }

  /**
   * Obtains the set of files in a backup that are unchanged from its
   * dependent backup or backups.  This list is stored as the first entry
   * in the archive file.
   * @param backupDir The backup directory.
   * @param backupInfo The backup info.
   * @return The set of files that were unchanged.
   * @throws DirectoryException If an error occurs while trying to get the
   * appropriate cipher algorithm for an encrypted backup.
   * @throws IOException If an I/O error occurs while reading the backup
   * archive file.
   */
  private Set<String> getUnchanged(BackupDirectory backupDir,
                                   BackupInfo backupInfo)
       throws DirectoryException, IOException
  {
    HashSet<String> hashSet = new HashSet<String>();

    boolean         encrypt         = backupInfo.isEncrypted();

    HashMap<String,String> backupProperties = backupInfo.getBackupProperties();

    String archiveFilename =
         backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME);
    File archiveFile = new File(backupDir.getPath(), archiveFilename);

    InputStream inputStream = new FileInputStream(archiveFile);

    // Get the crypto manager and use it to obtain references to the message
    // digest and/or MAC to use for hashing and/or signing.
    CryptoManager cryptoManager   = DirectoryServer.getCryptoManager();

    // If the data is encrypted, then wrap the input stream in a cipher
    // input stream.
    if (encrypt)
    {
      try
      {
        inputStream = cryptoManager.getCipherInputStream(inputStream);
      }
      catch (CryptoManagerException e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get(
                stackTraceToSingleLineString(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }


    // Wrap the file input stream in a zip input stream.
    ZipInputStream zipStream = new ZipInputStream(inputStream);

    // Iterate through the entries in the zip file.
    ZipEntry zipEntry = zipStream.getNextEntry();
    while (zipEntry != null)
    {
      // We are looking for the entry containing the list of unchanged files.
      if (zipEntry.getName().equals(ZIPENTRY_UNCHANGED_LOGFILES))
      {
        InputStreamReader reader = new InputStreamReader(zipStream);
        BufferedReader bufferedReader = new BufferedReader(reader);
        String line = bufferedReader.readLine();
        while (line != null)
        {
          hashSet.add(line);
          line = bufferedReader.readLine();
        }
        break;
      }

      zipEntry = zipStream.getNextEntry();
    }

    zipStream.close();
    return hashSet;
  }

  /**
   * Obtains a list of the dependencies of a given backup in order from
   * the oldest (the full backup), to the most recent.
   * @param backupDir The backup directory.
   * @param backupInfo The backup for which dependencies are required.
   * @return A list of dependent backups.
   * @throws DirectoryException If a Directory Server error occurs.
   */
  private ArrayList<BackupInfo> getDependents(BackupDirectory backupDir,
                                              BackupInfo backupInfo)
       throws DirectoryException
  {
    ArrayList<BackupInfo> dependents = new ArrayList<BackupInfo>();
    while (backupInfo != null && !backupInfo.getDependencies().isEmpty())
    {
      String backupID = backupInfo.getDependencies().iterator().next();
      backupInfo = getBackupInfo(backupDir, backupID);
      if (backupInfo != null)
      {
        dependents.add(backupInfo);
      }
    }
    Collections.reverse(dependents);
    return dependents;
  }

  /**
   * Get the information for a given backup ID from the backup directory.
   * @param backupDir The backup directory.
   * @param backupID The backup ID.
   * @return The backup information, never null.
   * @throws DirectoryException If the backup information cannot be found.
   */
  private BackupInfo getBackupInfo(BackupDirectory backupDir,
                                   String backupID) throws DirectoryException
  {
    BackupInfo backupInfo = backupDir.getBackupInfo(backupID);
    if (backupInfo == null)
    {
      Message message =
          ERR_JEB_BACKUP_MISSING_BACKUPID.get(backupDir.getPath(), backupID);
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message);
    }
    return backupInfo;
  }
}
