/*
 * 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-2008 Sun Microsystems, Inc.
 */
package org.opends.server.types;
import org.opends.messages.Message;



import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.LinkedList;

import org.opends.server.config.ConfigException;

import static org.opends.server.loggers.debug.DebugLogger.*;
import org.opends.server.loggers.debug.DebugTracer;
import static org.opends.messages.CoreMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;



/**
 * This class defines a data structure for holding information about a
 * filesystem directory that contains data for one or more backups
 * associated with a backend.  Only backups for a single backend may
 * be placed in any given directory.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.VOLATILE,
     mayInstantiate=true,
     mayExtend=false,
     mayInvoke=true)
public final class BackupDirectory
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();




  /**
   * The name of the property that will be used to provide the DN of
   * the configuration entry for the backend associated with the
   * backups in this directory.
   */
  public static final String PROPERTY_BACKEND_CONFIG_DN =
       "backend_dn";



  // The DN of the configuration entry for the backend with which this
  // backup directory is associated.
  private DN configEntryDN;

  // The set of backups in the specified directory.  The iteration
  // order will be the order in which the backups were created.
  private LinkedHashMap<String,BackupInfo> backups;

  // The filesystem path to the backup directory.
  private String path;



  /**
   * Creates a new backup directory object with the provided
   * information.
   *
   * @param  path           The path to the directory containing the
   *                        backup file(s).
   * @param  configEntryDN  The DN of the configuration entry for the
   *                        backend with which this backup directory
   *                        is associated.
   */
  public BackupDirectory(String path, DN configEntryDN)
  {
    this.path          = path;
    this.configEntryDN = configEntryDN;

    backups = new LinkedHashMap<String,BackupInfo>();
  }



  /**
   * Creates a new backup directory object with the provided
   * information.
   *
   * @param  path           The path to the directory containing the
   *                        backup file(s).
   * @param  configEntryDN  The DN of the configuration entry for the
   *                        backend with which this backup directory
   *                        is associated.
   * @param  backups        Information about the set of backups
   *                        available within the specified directory.
   */
  public BackupDirectory(String path, DN configEntryDN,
                         LinkedHashMap<String,BackupInfo> backups)
  {
    this.path          = path;
    this.configEntryDN = configEntryDN;

    if (backups == null)
    {
      this.backups = new LinkedHashMap<String,BackupInfo>();
    }
    else
    {
      this.backups = backups;
    }
  }



  /**
   * Retrieves the path to the directory containing the backup
   * file(s).
   *
   * @return  The path to the directory containing the backup file(s).
   */
  public String getPath()
  {
    return path;
  }



  /**
   * Retrieves the DN of the configuration entry for the backend with
   * which this backup directory is associated.
   *
   * @return  The DN of the configuration entry for the backend with
   *          which this backup directory is associated.
   */
  public DN getConfigEntryDN()
  {
    return configEntryDN;
  }



  /**
   * Retrieves the set of backups in this backup directory, as a
   * mapping between the backup ID and the associated backup info.
   * The iteration order for the map will be the order in which the
   * backups were created.
   *
   * @return  The set of backups in this backup directory.
   */
  public LinkedHashMap<String,BackupInfo> getBackups()
  {
    return backups;
  }



  /**
   * Retrieves the backup info structure for the backup with the
   * specified ID.
   *
   * @param  backupID  The backup ID for the structure to retrieve.
   *
   * @return  The requested backup info structure, or
   *          <CODE>null</CODE> if no such structure exists.
   */
  public BackupInfo getBackupInfo(String backupID)
  {
    return backups.get(backupID);
  }



  /**
   * Retrieves the most recent backup for this backup directory,
   * according to the backup date.
   *
   * @return  The most recent backup for this backup directory,
   *          according to the backup date, or <CODE>null</CODE> if
   *          there are no backups in the backup directory.
   */
  public BackupInfo getLatestBackup()
  {
    BackupInfo latestBackup = null;
    for (BackupInfo backup : backups.values())
    {
      if (latestBackup == null)
      {
        latestBackup = backup;
      }
      else
      {
        if (backup.getBackupDate().getTime() >
            latestBackup.getBackupDate().getTime())
        {
          latestBackup = backup;
        }
      }
    }

    return latestBackup;
  }



  /**
   * Adds information about the provided backup to this backup
   * directory.
   *
   * @param  backupInfo  The backup info structure for the backup to
   *                     be added.
   *
   * @throws  ConfigException  If another backup already exists with
   *                           the same backup ID.
   */
  public void addBackup(BackupInfo backupInfo)
         throws ConfigException
  {
    String backupID = backupInfo.getBackupID();
    if (backups.containsKey(backupID))
    {
      Message message =
          ERR_BACKUPDIRECTORY_ADD_DUPLICATE_ID.get(backupID, path);
      throw new ConfigException(message);
    }

    backups.put(backupID, backupInfo);
  }



  /**
   * Removes the backup with the specified backup ID from this backup
   * directory.
   *
   * @param  backupID  The backup ID for the backup to remove from
   *                   this backup directory.
   *
   * @throws  ConfigException  If it is not possible to remove the
   *                           requested backup for some reason (e.g.,
   *                           no such backup exists, or another
   *                           backup is dependent on it).
   */
  public void removeBackup(String backupID)
         throws ConfigException
  {
    if (! backups.containsKey(backupID))
    {
      Message message =
          ERR_BACKUPDIRECTORY_NO_SUCH_BACKUP.get(backupID, path);
      throw new ConfigException(message);
    }

    for (BackupInfo backup : backups.values())
    {
      if (backup.dependsOn(backupID))
      {
        Message message = ERR_BACKUPDIRECTORY_UNRESOLVED_DEPENDENCY.
            get(backupID, path, backup.getBackupID());
        throw new ConfigException(message);
      }
    }

    backups.remove(backupID);
  }



  /**
   * Retrieves a path to the backup descriptor file that should be
   * used for this backup directory.
   *
   * @return  A path to the backup descriptor file that should be used
   *          for this backup directory.
   */
  public String getDescriptorPath()
  {
    return path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE;
  }



  /**
   * Writes the descriptor with the information contained in this
   * structure to disk in the appropriate directory.
   *
   * @throws  IOException  If a problem occurs while writing to disk.
   */
  public void writeBackupDirectoryDescriptor()
         throws IOException
  {
    // First make sure that the target directory exists.  If it
    // doesn't, then try to create it.
    File dir = new File(path);
    if (! dir.exists())
    {
      try
      {
        dir.mkdirs();
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        Message message = ERR_BACKUPDIRECTORY_CANNOT_CREATE_DIRECTORY.
            get(path, getExceptionMessage(e));
        throw new IOException(message.toString());
      }
    }
    else if (! dir.isDirectory())
    {
      Message message = ERR_BACKUPDIRECTORY_NOT_DIRECTORY.get(path);
      throw new IOException(message.toString());
    }


    // We'll write to a temporary file so that we won't destroy the
    // live copy if a problem occurs.
    String newDescriptorFilePath = path + File.separator +
                                   BACKUP_DIRECTORY_DESCRIPTOR_FILE +
                                   ".new";
    File newDescriptorFile = new File(newDescriptorFilePath);
    BufferedWriter writer =
         new BufferedWriter(new FileWriter(newDescriptorFile, false));


    // The first line in the file will only contain the DN of the
    // configuration entry for the associated backend.
    writer.write(PROPERTY_BACKEND_CONFIG_DN + "=" +
                 configEntryDN.toString());
    writer.newLine();
    writer.newLine();


    // Iterate through all of the backups and add them to the file.
    for (BackupInfo backup : backups.values())
    {
      LinkedList<String> backupLines = backup.encode();

      for (String line : backupLines)
      {
        writer.write(line);
        writer.newLine();
      }

      writer.newLine();
    }


    // At this point, the file should be complete so flush and close
    // it.
    writer.flush();
    writer.close();


    // If previous backup descriptor file exists, then rename it.
    String descriptorFilePath = path + File.separator +
                                BACKUP_DIRECTORY_DESCRIPTOR_FILE;
    File descriptorFile = new File(descriptorFilePath);
    if (descriptorFile.exists())
    {
      String savedDescriptorFilePath = descriptorFilePath + ".save";
      File savedDescriptorFile = new File(savedDescriptorFilePath);
      if (savedDescriptorFile.exists())
      {
        try
        {
          savedDescriptorFile.delete();
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          Message message =
              ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR.
                get(savedDescriptorFilePath, getExceptionMessage(e),
                    newDescriptorFilePath, descriptorFilePath);
          throw new IOException(message.toString());
        }
      }

      try
      {
        descriptorFile.renameTo(savedDescriptorFile);
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        Message message =
            ERR_BACKUPDIRECTORY_CANNOT_RENAME_CURRENT_DESCRIPTOR.
              get(descriptorFilePath, savedDescriptorFilePath,
                  getExceptionMessage(e), newDescriptorFilePath,
                  descriptorFilePath);
        throw new IOException(message.toString());
      }
    }


    // Rename the new descriptor file to match the previous one.
    try
    {
      newDescriptorFile.renameTo(descriptorFile);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message =
        ERR_BACKUPDIRECTORY_CANNOT_RENAME_NEW_DESCRIPTOR.
            get(newDescriptorFilePath, descriptorFilePath,
                getExceptionMessage(e));
      throw new IOException(message.toString());
    }
  }



  /**
   * Reads the backup descriptor file in the specified path and uses
   * the information it contains to create a new backup directory
   * structure.
   *
   * @param  path  The path to the directory containing the backup
   *               descriptor file to read.
   *
   * @return  The backup directory structure created from the contents
   *          of the descriptor file.
   *
   * @throws  IOException  If a problem occurs while trying to read
   *                       the contents of the descriptor file.
   *
   * @throws  ConfigException  If the contents of the descriptor file
   *                           cannot be parsed to create a backup
   *                           directory structure.
   */
  public static BackupDirectory
                     readBackupDirectoryDescriptor(String path)
         throws IOException, ConfigException
  {
    // Make sure that the descriptor file exists.
    String descriptorFilePath = path + File.separator +
                                BACKUP_DIRECTORY_DESCRIPTOR_FILE;
    File descriptorFile = new File(descriptorFilePath);
    if (! descriptorFile.exists())
    {
      Message message = ERR_BACKUPDIRECTORY_NO_DESCRIPTOR_FILE.get(
          descriptorFilePath);
      throw new ConfigException(message);
    }


    // Open the file for reading.  The first line should be the DN of
    // the associated configuration entry.
    BufferedReader reader =
         new BufferedReader(new FileReader(descriptorFile));
    String line = reader.readLine();
    if ((line == null) || (line.length() == 0))
    {
      Message message =
        ERR_BACKUPDIRECTORY_CANNOT_READ_CONFIG_ENTRY_DN.
            get(descriptorFilePath);
      throw new ConfigException(message);
    }
    else if (! line.startsWith(PROPERTY_BACKEND_CONFIG_DN))
    {
      Message message = ERR_BACKUPDIRECTORY_FIRST_LINE_NOT_DN.get(
          descriptorFilePath, line);
      throw new ConfigException(message);
    }

    String dnString =
         line.substring(PROPERTY_BACKEND_CONFIG_DN.length() + 1);
    DN configEntryDN;
    try
    {
      configEntryDN = DN.decode(dnString);
    }
    catch (DirectoryException de)
    {
      Message message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get(
          dnString, descriptorFilePath, de.getMessageObject());
      throw new ConfigException(message, de);
    }
    catch (Exception e)
    {
      Message message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get(
          dnString, descriptorFilePath, getExceptionMessage(e));
      throw new ConfigException(message, e);
    }


    // Create the backup directory structure from what we know so far.
    BackupDirectory backupDirectory =
         new BackupDirectory(path, configEntryDN);


    // Iterate through the rest of the file and create the backup info
    // structures.  Blank lines will be considered delimiters.
    LinkedList<String> lines = new LinkedList<String>();
    while (true)
    {
      line = reader.readLine();
      if ((line == null) || (line.length() == 0))
      {
        // It's a blank line or the end of the file.  If we have lines
        // to process then do so.  Otherwise, move on.
        if (lines.isEmpty())
        {
          if (line == null)
          {
            break;
          }
          else
          {
            continue;
          }
        }


        // Parse the lines that we read and add the backup info to the
        // directory structure.
        BackupInfo backupInfo = BackupInfo.decode(backupDirectory,
                                                  lines);
        backupDirectory.addBackup(backupInfo);
        lines.clear();


        // If it was the end of the file, then break out of the loop.
        if (line == null)
        {
          break;
        }
      }
      else
      {
        lines.add(line);
      }
    }


    // Close the reader and return the backup directory structure.
    reader.close();
    return backupDirectory;
  }
}

