/*
 * 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 2014-2015 ForgeRock AS
 */
package org.opends.server.types;

import org.forgerock.i18n.LocalizableMessage;




import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.util.Base64;
import org.forgerock.i18n.slf4j.LocalizedLogger;

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
 * backup that is available in a backup directory.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.VOLATILE,
     mayInstantiate=false,
     mayExtend=false,
     mayInvoke=true)
public final class BackupInfo
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();




  /**
   * The name of the property that holds the date that the backup was
   * created.
   */
  public static final String PROPERTY_BACKUP_DATE = "backup_date";



  /**
   * The name of the property that holds the backup ID in encoded
   * representations.
   */
  public static final String PROPERTY_BACKUP_ID = "backup_id";



  /**
   * The name of the property that holds the incremental flag in
   * encoded representations.
   */
  public static final String PROPERTY_IS_INCREMENTAL = "incremental";



  /**
   * The name of the property that holds the compressed flag in
   * encoded representations.
   */
  public static final String PROPERTY_IS_COMPRESSED = "compressed";



  /**
   * The name of the property that holds the encrypted flag in encoded
   * representations.
   */
  public static final String PROPERTY_IS_ENCRYPTED = "encrypted";



  /**
   * The name of the property that holds the unsigned hash in encoded
   * representations.
   */
  public static final String PROPERTY_UNSIGNED_HASH = "hash";



  /**
   * The name of the property that holds the signed hash in encoded
   * representations.
   */
  public static final String PROPERTY_SIGNED_HASH = "signed_hash";



  /**
   * The name of the property that holds the set of dependencies in
   * encoded representations (one dependency per instance).
   */
  public static final String PROPERTY_DEPENDENCY = "dependency";



  /**
   * The prefix to use with custom backup properties.  The name of the
   * property will be appended to this prefix.
   */
  public static final String PROPERTY_CUSTOM_PREFIX = "property.";



  /**
   * The backup directory with which this backup info structure is
   * associated.
   */
  private BackupDirectory backupDirectory;

  /** Indicates whether this backup is compressed. */
  private boolean isCompressed;

  /** Indicates whether this backup is encrypted. */
  private boolean isEncrypted;

  /** Indicates whether this is an incremental backup. */
  private boolean isIncremental;

  /** The signed hash for this backup, if appropriate. */
  private byte[] signedHash;

  /** The unsigned hash for this backup, if appropriate. */
  private byte[] unsignedHash;

  /** The time that this backup was created. */
  private Date backupDate;

  /** The set of backup ID(s) on which this backup is dependent. */
  private HashSet<String> dependencies;

  /**
   * The set of additional properties associated with this backup.
   * This is intended for use by the backend for storing any kind of
   * state information that it might need to associated with the
   * backup.  The mapping will be between a name and a value, where
   * the name must not contain an equal sign and neither the name nor
   * the value may contain line breaks;
   */
  private HashMap<String,String> backupProperties;

  /** The unique ID for this backup. */
  private String backupID;



  /**
   * Creates a new backup info structure with the provided
   * information.
   *
   * @param  backupDirectory   A reference to the backup directory in
   *                           which this backup is stored.
   * @param  backupID          The unique ID for this backup.
   * @param  backupDate        The time that this backup was created.
   * @param  isIncremental     Indicates whether this is an
   *                           incremental or a full backup.
   * @param  isCompressed      Indicates whether the backup is
   *                           compressed.
   * @param  isEncrypted       Indicates whether the backup is
   *                           encrypted.
   * @param  unsignedHash      The unsigned hash for this backup, if
   *                           appropriate.
   * @param  signedHash        The signed hash for this backup, if
   *                           appropriate.
   * @param  dependencies      The backup IDs of the previous backups
   *                           on which this backup is dependent.
   * @param  backupProperties  The set of additional backend-specific
   *                           properties that should be stored with
   *                           this backup information.  It should be
   *                           a mapping between property names and
   *                           values, where the names do not contain
   *                           any equal signs and neither the names
   *                           nor the values contain line breaks.
   */
  public BackupInfo(BackupDirectory backupDirectory, String backupID,
                    Date backupDate, boolean isIncremental,
                    boolean isCompressed, boolean isEncrypted,
                    byte[] unsignedHash, byte[] signedHash,
                    HashSet<String> dependencies,
                    HashMap<String,String> backupProperties)
  {
    this.backupDirectory = backupDirectory;
    this.backupID        = backupID;
    this.backupDate      = backupDate;
    this.isIncremental   = isIncremental;
    this.isCompressed    = isCompressed;
    this.isEncrypted     = isEncrypted;
    this.unsignedHash    = unsignedHash;
    this.signedHash      = signedHash;

    if (dependencies == null)
    {
      this.dependencies = new HashSet<>();
    }
    else
    {
      this.dependencies = dependencies;
    }

    if (backupProperties == null)
    {
      this.backupProperties = new HashMap<>();
    }
    else
    {
      this.backupProperties = backupProperties;
    }
  }



  /**
   * Retrieves the reference to the backup directory in which this
   * backup is stored.
   *
   * @return  A reference to the backup directory in which this backup
   *          is stored.
   */
  public BackupDirectory getBackupDirectory()
  {
    return backupDirectory;
  }



  /**
   * Retrieves the unique ID for this backup.
   *
   * @return  The unique ID for this backup.
   */
  public String getBackupID()
  {
    return backupID;
  }



  /**
   * Retrieves the date that this backup was created.
   *
   * @return  The date that this backup was created.
   */
  public Date getBackupDate()
  {
    return backupDate;
  }



  /**
   * Indicates whether this is an incremental or a full backup.
   *
   * @return  <CODE>true</CODE> if this is an incremental backup, or
   *          <CODE>false</CODE> if it is a full backup.
   */
  public boolean isIncremental()
  {
    return isIncremental;
  }



  /**
   * Indicates whether this backup is compressed.
   *
   * @return  <CODE>true</CODE> if this backup is compressed, or
   *          <CODE>false</CODE> if it is not.
   */
  public boolean isCompressed()
  {
    return isCompressed;
  }



  /**
   * Indicates whether this backup is encrypted.
   *
   * @return  <CODE>true</CODE> if this backup is encrypted, or
   *          <CODE>false</CODE> if it is not.
   */
  public boolean isEncrypted()
  {
    return isEncrypted;
  }



  /**
   * Retrieves the data for the unsigned hash for this backup, if
   * appropriate.
   *
   * @return  The data for the unsigned hash for this backup, or
   *          <CODE>null</CODE> if there is none.
   */
  public byte[] getUnsignedHash()
  {
    return unsignedHash;
  }



  /**
   * Retrieves the data for the signed hash for this backup, if
   * appropriate.
   *
   * @return  The data for the signed hash for this backup, or
   *          <CODE>null</CODE> if there is none.
   */
  public byte[] getSignedHash()
  {
    return signedHash;
  }



  /**
   * Retrieves the set of the backup IDs for the backups on which this
   * backup is dependent.  This is primarily intended for use with
   * incremental backups (which should be dependent on at least a full
   * backup and possibly one or more other incremental backups).  The
   * contents of this hash should not be directly updated by the
   * caller.
   *
   * @return  The set of the backup IDs for the backups on which this
   *          backup is dependent.
   */
  public HashSet<String> getDependencies()
  {
    return dependencies;
  }



  /**
   * Indicates whether this backup has a dependency on the backup with
   * the provided ID.
   *
   * @param  backupID  The backup ID for which to make the
   *                   determination.
   *
   * @return  <CODE>true</CODE> if this backup has a dependency on the
   *          backup with the provided ID, or <CODE>false</CODE> if
   *          not.
   */
  public boolean dependsOn(String backupID)
  {
    return dependencies.contains(backupID);
  }



  /**
   * Retrieves a set of additional properties that should be
   * associated with this backup.  This may be used by the backend to
   * store arbitrary information that may be needed later to restore
   * the backup or perform an incremental backup based on this backup.
   * The mapping will be between property names and values, where the
   * names are not allowed to contain equal signs, and neither the
   * names nor the values may have line breaks.  The contents of the
   * mapping should not be altered by the caller.
   *
   * @return  A set of additional properties that should be associated
   *          with this backup.
   */
  public HashMap<String,String> getBackupProperties()
  {
    return backupProperties;
  }



  /**
   * Retrieves the value of the backup property with the specified
   * name.
   *
   * @param  name  The name of the backup property to retrieve.
   *
   * @return  The value of the backup property with the specified
   *          name, or <CODE>null</CODE> if there is no such property.
   */
  public String getBackupProperty(String name)
  {
    return backupProperties.get(name);
  }



  /**
   * Encodes this backup info structure to a multi-line string
   * representation.  This representation may be parsed by the
   * <CODE>decode</CODE> method to reconstruct the structure.
   *
   * @return  A multi-line string representation of this backup info
   *          structure.
   */
  public LinkedList<String> encode()
  {
    LinkedList<String> list = new LinkedList<>();
    SimpleDateFormat   dateFormat =
         new SimpleDateFormat(DATE_FORMAT_GMT_TIME);

    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

    list.add(PROPERTY_BACKUP_ID + "=" + backupID);
    list.add(PROPERTY_BACKUP_DATE + "=" + dateFormat.format(backupDate));
    list.add(PROPERTY_IS_INCREMENTAL + "=" + String.valueOf(isIncremental));
    list.add(PROPERTY_IS_COMPRESSED + "=" + String.valueOf(isCompressed));
    list.add(PROPERTY_IS_ENCRYPTED + "=" + String.valueOf(isEncrypted));

    if (unsignedHash != null)
    {
      list.add(PROPERTY_UNSIGNED_HASH + "=" + Base64.encode(unsignedHash));
    }

    if (signedHash != null)
    {
      list.add(PROPERTY_SIGNED_HASH + "=" + Base64.encode(signedHash));
    }

    if (! dependencies.isEmpty())
    {
      for (String dependency : dependencies)
      {
        list.add(PROPERTY_DEPENDENCY + "=" + dependency);
      }
    }

    if (! backupProperties.isEmpty())
    {
      for (String name : backupProperties.keySet())
      {
        String value = backupProperties.get(name);
        if (value == null)
        {
          value = "";
        }

        list.add(PROPERTY_CUSTOM_PREFIX + name + "=" + value);
      }
    }

    return list;
  }



  /**
   * Decodes the provided list of strings as the representation of a
   * backup info structure.
   *
   * @param  backupDirectory  The reference to the backup directory
   *                          with which the backup info is
   *                          associated.
   * @param  encodedInfo      The list of strings that comprise the
   *                          string representation of the backup info
   *                          structure.
   *
   * @return  The decoded backup info structure.
   *
   * @throws  ConfigException  If a problem occurs while attempting to
   *                           decode the backup info data.
   */
  public static BackupInfo decode(BackupDirectory backupDirectory,
                                  List<String> encodedInfo)
         throws ConfigException
  {
    String                 backupID         = null;
    Date                   backupDate       = null;
    boolean                isIncremental    = false;
    boolean                isCompressed     = false;
    boolean                isEncrypted      = false;
    byte[]                 unsignedHash     = null;
    byte[]                 signedHash       = null;
    HashSet<String>        dependencies     = new HashSet<>();
    HashMap<String,String> backupProperties = new HashMap<>();

    String backupPath = backupDirectory.getPath();
    try
    {
      for (String line : encodedInfo)
      {
        int equalPos = line.indexOf('=');
        if (equalPos < 0)
        {
          LocalizableMessage message =
              ERR_BACKUPINFO_NO_DELIMITER.get(line, backupPath);
          throw new ConfigException(message);
        }
        else if (equalPos == 0)
        {
          LocalizableMessage message =
              ERR_BACKUPINFO_NO_NAME.get(line, backupPath);
          throw new ConfigException(message);
        }

        String name  = line.substring(0, equalPos);
        String value = line.substring(equalPos+1);

        if (name.equals(PROPERTY_BACKUP_ID))
        {
          if (backupID == null)
          {
            backupID = value;
          }
          else
          {
            LocalizableMessage message = ERR_BACKUPINFO_MULTIPLE_BACKUP_IDS.get(
                backupPath, backupID, value);
            throw new ConfigException(message);
          }
        }
        else if (name.equals(PROPERTY_BACKUP_DATE))
        {
          SimpleDateFormat dateFormat =
               new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
          dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
          backupDate = dateFormat.parse(value);
        }
        else if (name.equals(PROPERTY_IS_INCREMENTAL))
        {
          isIncremental = Boolean.valueOf(value);
        }
        else if (name.equals(PROPERTY_IS_COMPRESSED))
        {
          isCompressed = Boolean.valueOf(value);
        }
        else if (name.equals(PROPERTY_IS_ENCRYPTED))
        {
          isEncrypted = Boolean.valueOf(value);
        }
        else if (name.equals(PROPERTY_UNSIGNED_HASH))
        {
          unsignedHash = Base64.decode(value);
        }
        else if (name.equals(PROPERTY_SIGNED_HASH))
        {
          signedHash = Base64.decode(value);
        }
        else if (name.equals(PROPERTY_DEPENDENCY))
        {
          dependencies.add(value);
        }
        else if (name.startsWith(PROPERTY_CUSTOM_PREFIX))
        {
          String propertyName =
               name.substring(PROPERTY_CUSTOM_PREFIX.length());
          backupProperties.put(propertyName, value);
        }
        else
        {
          LocalizableMessage message = ERR_BACKUPINFO_UNKNOWN_PROPERTY.get(
              backupPath, name, value);
          throw new ConfigException(message);
        }
      }
    }
    catch (ConfigException ce)
    {
      throw ce;
    }
    catch (Exception e)
    {
      logger.traceException(e);

      LocalizableMessage message = ERR_BACKUPINFO_CANNOT_DECODE.get(
          backupPath, getExceptionMessage(e));
      throw new ConfigException(message, e);
    }


    // There must have been at least a backup ID and backup date
    // specified.
    if (backupID == null)
    {
      LocalizableMessage message = ERR_BACKUPINFO_NO_BACKUP_ID.get(backupPath);
      throw new ConfigException(message);
    }

    if (backupDate == null)
    {
      LocalizableMessage message =
          ERR_BACKUPINFO_NO_BACKUP_DATE.get(backupID, backupPath);
      throw new ConfigException(message);
    }


    return new BackupInfo(backupDirectory, backupID, backupDate,
                          isIncremental, isCompressed, isEncrypted,
                          unsignedHash, signedHash, dependencies,
                          backupProperties);
  }



  /**
   * Retrieves a multi-line string representation of this backup info
   * structure.
   *
   * @return  A multi-line string representation of this backup info
   *          structure.
   */
  public String toString()
  {
    StringBuilder buffer = new StringBuilder();
    toString(buffer);
    return buffer.toString();
  }



  /**
   * Appends a multi-line string representation of this backup info
   * structure to the provided buffer.
   *
   * @param  buffer  The buffer to which the information should be
   *                 written.
   */
  public void toString(StringBuilder buffer)
  {
    LinkedList<String> lines = encode();
    for (String line : lines)
    {
      buffer.append(line);
      buffer.append(EOL);
    }
  }
}

