/*
 * 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.core;



import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

import org.opends.messages.Message;
import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn;
import org.opends.server.admin.std.server.PasswordPolicyCfg;
import org.opends.server.admin.std.server.PasswordValidatorCfg;
import org.opends.server.api.AccountStatusNotificationHandler;
import org.opends.server.api.PasswordGenerator;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.api.PasswordValidator;
import org.opends.server.config.ConfigException;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.asn1.ASN1OctetString;
import org.opends.server.schema.GeneralizedTimeSyntax;
import org.opends.server.types.AttributeType;
import org.opends.server.types.ByteString;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DN;
import org.opends.server.types.InitializationException;

import static org.opends.messages.CoreMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.schema.SchemaConstants.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;



/**
 * This class defines a data structure that holds information about a Directory
 * Server password policy.
 */
public class PasswordPolicy
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();

  // The DN of the entry containing the configuration for this password
  // policy.
  private final DN configEntryDN;

  // The attribute type that will hold user passwords for this password policy.
  private final AttributeType passwordAttribute;

  // Indicates whether the attribute type uses the authPassword syntax.
  private final boolean authPasswordSyntax;

  // Indicates whether a user with an expired password will still be allowed to
  // change it via the password modify extended operation.
  private boolean allowExpiredPasswordChanges =
       DEFAULT_PWPOLICY_ALLOW_EXPIRED_CHANGES;

  // Indicates whether the password attribute will be allowed to have multiple
  // distinct values.
  private boolean allowMultiplePasswordValues =
       DEFAULT_PWPOLICY_ALLOW_MULTIPLE_PW_VALUES;

  // Indicates whether to allow pre-encoded passwords.
  private boolean allowPreEncodedPasswords =
       DEFAULT_PWPOLICY_ALLOW_PRE_ENCODED_PASSWORDS;

  // Indicates whether users will be allowed to change their passwords.
  private boolean allowUserPasswordChanges =
       DEFAULT_PWPOLICY_ALLOW_USER_CHANGE;

  // Indicates whether to allow a password to expire without ever providing the
  // user with a notification.
  private boolean expirePasswordsWithoutWarning =
       DEFAULT_PWPOLICY_EXPIRE_WITHOUT_WARNING;

  // Indicates whether users must change their passwords the first time they
  // authenticate after their account is created.
  private boolean forceChangeOnAdd =
       DEFAULT_PWPOLICY_FORCE_CHANGE_ON_ADD;

  // Indicates whether a user must change their password after it has been reset
  // by an administrator.
  private boolean forceChangeOnReset =
       DEFAULT_PWPOLICY_FORCE_CHANGE_ON_RESET;

  // Indicates whether a user must provide their current password in order to
  // use a new password.
  private boolean requireCurrentPassword =
       DEFAULT_PWPOLICY_REQUIRE_CURRENT_PASSWORD;

  // Indicates whether users will be required to authenticate using a secure
  // mechanism.
  private boolean requireSecureAuthentication =
       DEFAULT_PWPOLICY_REQUIRE_SECURE_AUTHENTICATION;

  // Indicates whether users will be required to change their passwords using a
  // secure mechanism.
  private boolean requireSecurePasswordChanges =
       DEFAULT_PWPOLICY_REQUIRE_SECURE_PASSWORD_CHANGES;

  // Indicates whether password validation should be performed for
  // administrative password changes.
  private boolean skipValidationForAdministrators =
       DEFAULT_PWPOLICY_SKIP_ADMIN_VALIDATION;

  // The set of account status notification handlers for this password policy.
  private ConcurrentHashMap<DN, AccountStatusNotificationHandler<?>>
    notificationHandlers;

  // The set of password validators that will be used with this
  // password policy.
  private ConcurrentHashMap<DN, PasswordValidator<?>> passwordValidators;

  // The set of default password storage schemes for this password
  // policy.
  private CopyOnWriteArrayList<PasswordStorageScheme<?>> defaultStorageSchemes =
    new CopyOnWriteArrayList<PasswordStorageScheme<?>>();
  {
    PasswordStorageScheme<?> defaultScheme =
      DirectoryServer.getPasswordStorageScheme(DEFAULT_PASSWORD_STORAGE_SCHEME);
    if (defaultScheme != null) defaultStorageSchemes.add(defaultScheme);
  }

  // The names of the deprecated password storage schemes for this password
  // policy.
  private CopyOnWriteArraySet<String> deprecatedStorageSchemes =
       new CopyOnWriteArraySet<String>();

  // The DN of the password validator for this password policy.
  private DN passwordGeneratorDN = null;

  // The password generator for use with this password policy.
  private PasswordGenerator<?> passwordGenerator = null;

  // The number of grace logins that a user may have.
  private int graceLoginCount = DEFAULT_PWPOLICY_GRACE_LOGIN_COUNT;

  // The number of passwords to keep in the history.
  private int historyCount = DEFAULT_PWPOLICY_HISTORY_COUNT;

  // The maximum length of time in seconds to keep passwords in the history.
  private int historyDuration = DEFAULT_PWPOLICY_HISTORY_DURATION;

  // The maximum length of time in seconds that an account may remain idle
  // before it is locked out.
  private int idleLockoutInterval = DEFAULT_PWPOLICY_IDLE_LOCKOUT_INTERVAL;

  // The length of time a user should stay locked out, in seconds.
  private int lockoutDuration = DEFAULT_PWPOLICY_LOCKOUT_DURATION;

  // The number of authentication failures before an account is locked out.
  private int lockoutFailureCount = DEFAULT_PWPOLICY_LOCKOUT_FAILURE_COUNT;

  // The length of time that authentication failures should be counted against
  // a user.
  private int lockoutFailureExpirationInterval =
       DEFAULT_PWPOLICY_LOCKOUT_FAILURE_EXPIRATION_INTERVAL;

  // The maximum password age (i.e., expiration interval), in seconds.
  private int maximumPasswordAge = DEFAULT_PWPOLICY_MAXIMUM_PASSWORD_AGE;

  // The maximum password age for administratively reset passwords, in seconds.
  private int maximumPasswordResetAge =
       DEFAULT_PWPOLICY_MAXIMUM_PASSWORD_RESET_AGE;

  // The minimum password age, in seconds.
  private int minimumPasswordAge = DEFAULT_PWPOLICY_MINIMUM_PASSWORD_AGE;

  // The password expiration warning interval, in seconds.
  private int warningInterval = DEFAULT_PWPOLICY_WARNING_INTERVAL;

  // The the time by which all users will be required to change their passwords.
  private long requireChangeByTime = -1L;

  // The attribute type that will hold the last login time.
  private AttributeType lastLoginTimeAttribute = null;

  // The format string to use when generating the last login time.
  private String lastLoginTimeFormat = null;

  // The set of previous last login time format strings.
  private CopyOnWriteArrayList<String> previousLastLoginTimeFormats =
       new CopyOnWriteArrayList<String>();

  // The state update failure policy.
  private PasswordPolicyCfgDefn.StateUpdateFailurePolicy
       stateUpdateFailurePolicy =
            PasswordPolicyCfgDefn.StateUpdateFailurePolicy.REACTIVE;



  /**
   * Creates a new password policy based on the configuration contained in the
   * provided configuration entry.  Any parameters not included in the provided
   * configuration entry will be assigned server-wide default values.
   *
   * @param  configuration  The configuration with the information to use to
   *                      initialize this password policy.
   *
   * @throws  ConfigException  If the provided entry does not contain a valid
   *                           password policy configuration.
   *
   * @throws  InitializationException  If an error occurs while initializing the
   *                                   password policy that is not related to
   *                                   the server configuration.
   */
  public PasswordPolicy(PasswordPolicyCfg configuration)
         throws ConfigException, InitializationException
  {
    // Create a list of units and values that we can use to represent time
    // periods.
    LinkedHashMap<String,Double> timeUnits = new LinkedHashMap<String,Double>();
    timeUnits.put(TIME_UNIT_SECONDS_ABBR, 1D);
    timeUnits.put(TIME_UNIT_SECONDS_FULL, 1D);
    timeUnits.put(TIME_UNIT_MINUTES_ABBR, 60D);
    timeUnits.put(TIME_UNIT_MINUTES_FULL, 60D);
    timeUnits.put(TIME_UNIT_HOURS_ABBR, (double) (60 * 60));
    timeUnits.put(TIME_UNIT_HOURS_FULL, (double) (60 * 60));
    timeUnits.put(TIME_UNIT_DAYS_ABBR, (double) (60 * 60 * 24));
    timeUnits.put(TIME_UNIT_DAYS_FULL, (double) (60 * 60 * 24));
    timeUnits.put(TIME_UNIT_WEEKS_ABBR, (double) (60 * 60 * 24 * 7));
    timeUnits.put(TIME_UNIT_WEEKS_FULL, (double) (60 * 60 * 24 * 7));

    this.configEntryDN = configuration.dn();

    // Get the password attribute.  If specified, it must have either the
    // user password or auth password syntax.
    passwordAttribute = configuration.getPasswordAttribute();
    String syntaxOID = passwordAttribute.getSyntaxOID();
    if (syntaxOID.equals(SYNTAX_AUTH_PASSWORD_OID))
    {
      authPasswordSyntax = true;
    }
    else if (syntaxOID.equals(SYNTAX_USER_PASSWORD_OID))
    {
      authPasswordSyntax = false;
    }
    else
    {
      String syntax = passwordAttribute.getSyntax().getSyntaxName();
      if ((syntax == null) || (syntax.length() == 0))
      {
        syntax = syntaxOID;
      }

      Message message = ERR_PWPOLICY_INVALID_PASSWORD_ATTRIBUTE_SYNTAX.
          get(String.valueOf(configEntryDN), passwordAttribute.getNameOrOID(),
              String.valueOf(syntax));
      throw new ConfigException(message);
    }


    // Get the default storage schemes.  They must all reference valid storage
    // schemes that support the syntax for the specified password attribute.
    SortedSet<DN> storageSchemeDNs =
      configuration.getDefaultPasswordStorageSchemeDNs();
    try
    {
      LinkedList<PasswordStorageScheme<?>> schemes =
        new LinkedList<PasswordStorageScheme<?>>();
      for (DN configEntryDN : storageSchemeDNs)
      {
        PasswordStorageScheme<?> scheme =
          DirectoryServer.getPasswordStorageScheme(configEntryDN);

        if (this.authPasswordSyntax &&
            (! scheme.supportsAuthPasswordSyntax()))
        {
          Message message = ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH.get(
              String.valueOf(configEntryDN),
              this.passwordAttribute.getNameOrOID());
          throw new ConfigException(message);
        }

        schemes.add(scheme);
      }

      this.defaultStorageSchemes =
        new CopyOnWriteArrayList<PasswordStorageScheme<?>>(schemes);
    }
    catch (ConfigException ce)
    {
      throw ce;
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message = ERR_PWPOLICY_CANNOT_DETERMINE_DEFAULT_STORAGE_SCHEMES.
          get(String.valueOf(configEntryDN), getExceptionMessage(e));
      throw new InitializationException(message, e);
    }


    // Get the names of the deprecated storage schemes.
    SortedSet<DN> deprecatedStorageSchemeDNs =
      configuration.getDeprecatedPasswordStorageSchemeDNs();
    try
    {
      LinkedHashSet<String> newDeprecatedStorageSchemes =
        new LinkedHashSet<String>();
      for (DN schemeDN : deprecatedStorageSchemeDNs)
      {
        PasswordStorageScheme<?> scheme =
          DirectoryServer.getPasswordStorageScheme(schemeDN);
        if (this.authPasswordSyntax)
        {
          if (scheme.supportsAuthPasswordSyntax())
          {
            newDeprecatedStorageSchemes.add(
                scheme.getAuthPasswordSchemeName());
          }
          else
          {
            Message message = ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH.get(
                String.valueOf(configEntryDN),
                String.valueOf(schemeDN));
            throw new ConfigException(message);
          }
        }
        else
        {
          newDeprecatedStorageSchemes.add(
              toLowerCase(scheme.getStorageSchemeName()));
        }
      }

      this.deprecatedStorageSchemes =
        new CopyOnWriteArraySet<String>(newDeprecatedStorageSchemes);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message =
          ERR_PWPOLICY_CANNOT_DETERMINE_DEPRECATED_STORAGE_SCHEMES.
            get(String.valueOf(configEntryDN), getExceptionMessage(e));
      throw new InitializationException(message, e);
    }


    // Get the password validators.
    SortedSet<DN> passwordValidators = configuration.getPasswordValidatorDNs();
    ConcurrentHashMap<DN, PasswordValidator<?>> validators =
      new ConcurrentHashMap<DN, PasswordValidator<?>>();
    for (DN validatorDN : passwordValidators)
    {
      validators.put(validatorDN,
          DirectoryServer.getPasswordValidator(validatorDN));
    }
    this.passwordValidators = validators;


    // Get the status notification handlers.
    SortedSet<DN> statusNotificationHandlers =
      configuration.getAccountStatusNotificationHandlerDNs();
    ConcurrentHashMap<DN,AccountStatusNotificationHandler<?>> handlers =
      new ConcurrentHashMap<DN,AccountStatusNotificationHandler<?>>();
    for (DN handlerDN : statusNotificationHandlers)
    {
      AccountStatusNotificationHandler<?> handler =
        DirectoryServer.getAccountStatusNotificationHandler(handlerDN);
      handlers.put(handlerDN, handler);
    }
    this.notificationHandlers = handlers;


    // Determine whether to allow user password changes.
    this.allowUserPasswordChanges = configuration.isAllowUserPasswordChanges();

    // Determine whether to require the current password for user changes.
    this.requireCurrentPassword =
      configuration.isPasswordChangeRequiresCurrentPassword();

    // Determine whether to force password changes on add.
    this.forceChangeOnAdd = configuration.isForceChangeOnAdd();

    // Determine whether to force password changes on reset.
    this.forceChangeOnReset = configuration.isForceChangeOnReset();

    // Determine whether to validate reset passwords.
    this.skipValidationForAdministrators =
      configuration.isSkipValidationForAdministrators();

    // Get the password generator.
    DN passGenDN = configuration.getPasswordGeneratorDN() ;
    if (passGenDN != null)
    {
      this.passwordGeneratorDN = passGenDN;
      this.passwordGenerator = DirectoryServer.getPasswordGenerator(passGenDN);
    }


    // Determine whether to require secure authentication.
    this.requireSecureAuthentication =
      configuration.isRequireSecureAuthentication();

    // Determine whether to require secure password changes.
    this.requireSecurePasswordChanges =
      configuration.isRequireSecurePasswordChanges() ;

    // Determine whether to allow multiple password values.
    this.allowMultiplePasswordValues =
      configuration.isAllowMultiplePasswordValues();

    // Determine whether to allow pre-encoded passwords.
    this.allowPreEncodedPasswords = configuration.isAllowPreEncodedPasswords();

    // Get the minimum password age.
    this.minimumPasswordAge = (int) configuration.getMinPasswordAge();

    // Get the maximum password age.
    this.maximumPasswordAge = (int) configuration.getMaxPasswordAge();

    // Get the maximum password reset age.
    this.maximumPasswordResetAge = (int) configuration
        .getMaxPasswordResetAge();

    // Get the warning interval.
    this.warningInterval = (int) configuration
        .getPasswordExpirationWarningInterval();

    // Determine whether to expire passwords without warning.
    this.expirePasswordsWithoutWarning = configuration
        .isExpirePasswordsWithoutWarning();

    // If the expire without warning option is disabled, then there must be a
    // warning interval.
    if ((! this.expirePasswordsWithoutWarning()) &&
        (this.getWarningInterval() <= 0))
    {
      Message message =
        ERR_PWPOLICY_MUST_HAVE_WARNING_IF_NOT_EXPIRE_WITHOUT_WARNING.
            get(String.valueOf(configEntryDN));
      throw new ConfigException(message);
    }

    // Determine whether to allow user changes for expired passwords.
    this.allowExpiredPasswordChanges = configuration
        .isAllowExpiredPasswordChanges();

    // Get the grace login count.
    this.graceLoginCount = configuration.getGraceLoginCount();

    // Get the lockout failure count.
    this.lockoutFailureCount = configuration.getLockoutFailureCount();

    // Get the lockout duration.
    this.lockoutDuration = (int) configuration.getLockoutDuration();

    // Get the lockout failure expiration interval.
    this.lockoutFailureExpirationInterval = (int) configuration
        .getLockoutFailureExpirationInterval();

    // Get the required change time.
    String requireChangeBy = configuration.getRequireChangeByTime();
    try
    {
      if (requireChangeBy != null)
      {
        ByteString valueString = new ASN1OctetString(requireChangeBy);

        GeneralizedTimeSyntax syntax =
             (GeneralizedTimeSyntax)
             DirectoryServer.getAttributeSyntax(SYNTAX_GENERALIZED_TIME_OID,
                                                false);

        if (syntax == null)
        {
          this.requireChangeByTime =
               GeneralizedTimeSyntax.decodeGeneralizedTimeValue(valueString);
        }
        else
        {
          valueString =
               syntax.getEqualityMatchingRule().normalizeValue(valueString);
          this.requireChangeByTime =
               GeneralizedTimeSyntax.decodeGeneralizedTimeValue(valueString);
        }
      }
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message = ERR_PWPOLICY_CANNOT_DETERMINE_REQUIRE_CHANGE_BY_TIME.
          get(String.valueOf(configEntryDN), getExceptionMessage(e));
      throw new InitializationException(message, e);
    }


    // Get the last login time attribute.  If specified, it must be defined in
    // the server schema.  It does not need to have a generalized time syntax
    // because the value that it will store will not necessarily conform to this
    // format.
    lastLoginTimeAttribute = configuration.getLastLoginTimeAttribute();


    // Get the last login time format.  If specified, it must be a valid format
    // string.
    String formatString = configuration.getLastLoginTimeFormat();
    try
    {
      if (formatString != null)
      {
        try
        {
          new SimpleDateFormat(formatString);
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          Message message = ERR_PWPOLICY_INVALID_LAST_LOGIN_TIME_FORMAT.get(
              String.valueOf(configEntryDN), String.valueOf(formatString));
          throw new ConfigException(message);
        }

        this.lastLoginTimeFormat = formatString;
      }
    }
    catch (ConfigException ce)
    {
      throw ce;
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message = ERR_PWPOLICY_CANNOT_DETERMINE_LAST_LOGIN_TIME_FORMAT.
          get(String.valueOf(configEntryDN), getExceptionMessage(e));
      throw new InitializationException(message, e);
    }


    // Get the previous last login time formats.  If specified, they must all
    // be valid format strings.
    SortedSet<String> formatStrings =
      configuration.getPreviousLastLoginTimeFormat() ;
    try
    {
      if (formatStrings != null)
      {
        for (String s : formatStrings)
        {
          try
          {
            new SimpleDateFormat(s);
          }
          catch (Exception e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }

            Message message =
              ERR_PWPOLICY_INVALID_PREVIOUS_LAST_LOGIN_TIME_FORMAT.
                  get(String.valueOf(configEntryDN), String.valueOf(s));
            throw new ConfigException(message);
          }
        }

        this.previousLastLoginTimeFormats =
             new CopyOnWriteArrayList<String>(formatStrings);
      }
    }
    catch (ConfigException ce)
    {
      throw ce;
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      Message message =
          ERR_PWPOLICY_CANNOT_DETERMINE_PREVIOUS_LAST_LOGIN_TIME_FORMAT.
            get(String.valueOf(configEntryDN), getExceptionMessage(e));
      throw new InitializationException(message, e);
    }


    // Get the idle lockout duration.
    this.idleLockoutInterval = (int) configuration.getIdleLockoutInterval();


    // Get the state update failure policy.
    this.stateUpdateFailurePolicy = configuration.getStateUpdateFailurePolicy();


    // Get the password history count and duration.
    this.historyCount    = configuration.getPasswordHistoryCount();
    this.historyDuration = (int) configuration.getPasswordHistoryDuration();


    /*
     *  Holistic validation.
     */

    // Ensure that the password attribute was included in the configuration
    // entry, since it is required.
    if (passwordAttribute == null)
    {
      Message message =
          ERR_PWPOLICY_NO_PASSWORD_ATTRIBUTE.get(String.valueOf(configEntryDN));
      throw new ConfigException(message);
    }

    // Ensure that at least one default password storage scheme was included in
    // the configuration entry, since it is required.
    if (defaultStorageSchemes.isEmpty())
    {
      Message message = ERR_PWPOLICY_NO_DEFAULT_STORAGE_SCHEMES.get(
          String.valueOf(configEntryDN));
      throw new ConfigException(message);
    }

    // If both a maximum password age and a warning interval are provided, then
    // ensure that the warning interval is less than the maximum age.  Further,
    // if a minimum age is specified, then the sum of the minimum age and the
    // warning interval should be less than the maximum age.
    if (maximumPasswordAge > 0)
    {
      int warnInterval = Math.max(0, warningInterval);
      if (minimumPasswordAge > 0)
      {
        if ((warnInterval + minimumPasswordAge) >= maximumPasswordAge)
        {
          Message message =
              ERR_PWPOLICY_MIN_AGE_PLUS_WARNING_GREATER_THAN_MAX_AGE.
                get(String.valueOf(configEntryDN));
          throw new ConfigException(message);
        }
      }
      else if (warnInterval >= maximumPasswordAge)
      {
        Message message = ERR_PWPOLICY_WARNING_INTERVAL_LARGER_THAN_MAX_AGE.get(
            String.valueOf(configEntryDN));
        throw new ConfigException(message);
      }
    }
  }



  /**
   * Retrieves the DN of the configuration entry to which this password policy
   * corresponds.
   *
   * @return  The DN of the configuration entry.
   */
  public DN getConfigEntryDN()
  {
    return configEntryDN;
  }



  /**
   * Retrieves the attribute type used to store the password.
   *
   * @return  The attribute type used to store the password.
   */
  public AttributeType getPasswordAttribute()
  {
    return passwordAttribute;
  }



  /**
   * Indicates whether the associated password attribute uses the auth password
   * syntax.
   *
   * @return  <CODE>true</CODE> if the associated password attribute uses the
   *          auth password syntax, or <CODE>false</CODE> if not.
   */
  public boolean usesAuthPasswordSyntax()
  {
    return authPasswordSyntax;
  }



  /**
   * Retrieves the default set of password storage schemes that will be used for
   * this password policy.  The returned set should not be modified by the
   * caller.
   *
   * @return  The default set of password storage schemes that will be used for
   *          this password policy.
   */
  public CopyOnWriteArrayList<PasswordStorageScheme<?>>
  getDefaultStorageSchemes()
  {
    return defaultStorageSchemes;
  }



  /**
   * Indicates whether the specified storage scheme is a default scheme for this
   * password policy.
   *
   * @param  name  The name of the password storage scheme for which to make the
   *               determination.
   *
   * @return  <CODE>true</CODE> if the storage scheme is a default scheme for
   *          this password policy, or <CODE>false</CODE> if not.
   */
  public boolean isDefaultStorageScheme(String name)
  {
    CopyOnWriteArrayList<PasswordStorageScheme<?>> defaultSchemes =
         getDefaultStorageSchemes();
    if (defaultSchemes == null)
    {
      return false;
    }

    for (PasswordStorageScheme<?> s : defaultSchemes)
    {
      if (authPasswordSyntax)
      {
        if (s.getAuthPasswordSchemeName().equalsIgnoreCase(name))
        {
          return true;
        }
      }
      else
      {
        if (s.getStorageSchemeName().equalsIgnoreCase(name))
        {
          return true;
        }
      }
    }


    return false;
  }



  /**
   * Retrieves the names of the password storage schemes that have been
   * deprecated.  If an authenticating user has one or more of these deprecated
   * storage schemes in use in their entry, then they will be removed and
   * replaced with the passwords encoded in the default storage scheme(s).  The
   * returned list should not be altered by the caller.
   *
   * @return  The names of the password storage schemes that have been
   *          deprecated.
   */
  public CopyOnWriteArraySet<String> getDeprecatedStorageSchemes()
  {
    return deprecatedStorageSchemes;
  }



  /**
   * Indicates whether the specified storage scheme is deprecated.
   *
   * @param  name  The name of the password storage scheme for which to make the
   *               determination.
   *
   * @return  <CODE>true</CODE> if the storage scheme is deprecated, or
   *          <CODE>false</CODE> if not.
   */
  public boolean isDeprecatedStorageScheme(String name)
  {
    CopyOnWriteArraySet<String> deprecatedSchemes =
         getDeprecatedStorageSchemes();
    if (deprecatedSchemes == null)
    {
      return false;
    }

    for (String s : deprecatedSchemes)
    {
      if (s.equalsIgnoreCase(name))
      {
        return true;
      }
    }

    return false;
  }



  /**
   * Retrieves the set of password validators for this password policy.  The
   * returned list should not be altered by the caller.
   *
   * @return  The set of password validators for this password policy.
   */
  public ConcurrentHashMap<DN,
              PasswordValidator<? extends PasswordValidatorCfg>>
              getPasswordValidators()
  {
    return passwordValidators;
  }



  /**
   * Retrieves the set of account status notification handlers that should be
   * used with this password policy.  The returned list should not be altered by
   * the caller.
   *
   * @return  The set of account status notification handlers that should be
   *          used with this password policy.
   */
  public ConcurrentHashMap<DN,AccountStatusNotificationHandler<?>>
              getAccountStatusNotificationHandlers()
  {
    return notificationHandlers;
  }



  /**
   * Indicates whether end users will be allowed to change their own passwords
   * (subject to access control restrictions).
   *
   * @return  <CODE>true</CODE> if users will be allowed to change their own
   *          passwords, or <CODE>false</CODE> if not.
   */
  public boolean allowUserPasswordChanges()
  {
    return allowUserPasswordChanges;
  }



  /**
   * Indicates whether the end user must provide their current password (via the
   * password modify extended operation) in order to set a new password.
   *
   * @return  <CODE>true</CODE> if the end user must provide their current
   *          password in order to set a new password, or <CODE>false</CODE> if
   *          they will not.
   */
  public boolean requireCurrentPassword()
  {
    return requireCurrentPassword;
  }



  /**
   * Indicates whether users will be required to change their passwords as soon
   * as they authenticate after their accounts have been created.
   *
   * @return  <CODE>true</CODE> if users will be required to change their
   *          passwords at the initial authentication, or <CODE>false</CODE> if
   *          not.
   */
  public boolean forceChangeOnAdd()
  {
    return forceChangeOnAdd;
  }



  /**
   * Indicates whether a user will be required to change their password after it
   * has been reset by an administrator.
   *
   * @return  <CODE>true</CODE> if a user will be required to change their
   *          password after it has been reset by an administrator, or
   *          <CODE>false</CODE> if they can continue using that password.
   */
  public boolean forceChangeOnReset()
  {
    return forceChangeOnReset;
  }



  /**
   * Indicates whether operations by administrators that specify a new password
   * for a user (e.g., add, modify, or password modify) will be allowed to
   * bypass the password validation process that will be required for user
   * password changes.
   *
   * @return  <CODE>true</CODE> if administrators will be allowed to bypass the
   *          validation checks, or <CODE>false</CODE> if not.
   */
  public boolean skipValidationForAdministrators()
  {
    return skipValidationForAdministrators;
  }



  /**
   * Retrieves the DN of the password validator configuration entry.
   *
   * @return  The DN of the password validator configuration entry.
   */
  public DN getPasswordGeneratorDN()
  {
    return passwordGeneratorDN;
  }



  /**
   * Retrieves the password generator that will be used with this password
   * policy.
   *
   * @return  The password generator that will be used with this password
   *          policy, or <CODE>null</CODE> if there is none.
   */
  public PasswordGenerator<?> getPasswordGenerator()
  {
    return passwordGenerator;
  }



  /**
   * Retrieves the maximum number of previous passwords to maintain in the
   * password history.
   *
   * @return  The maximum number of previous passwords to maintain in the
   *          password history.
   */
  public int getPasswordHistoryCount()
  {
    return historyCount;
  }



  /**
   * Retrieves the maximum length of time in seconds that previous passwords
   * should remain in the password history.
   *
   * @return  The maximum length of time in seconds that previous passwords
   *          should remain in the password history.
   */
  public int getPasswordHistoryDuration()
  {
    return historyDuration;
  }



  /**
   * Indicates whether users with this password policy will be required to
   * authenticate in a secure manner that does not expose their password.
   *
   *  @return  <CODE>true</CODE> if users with this password policy will be
   *           required to authenticate in a secure manner that does not expose
   *           their password, or <CODE>false</CODE> if they may authenticate in
   *           an insecure manner.
   */
  public boolean requireSecureAuthentication()
  {
    return requireSecureAuthentication;
  }



  /**
   * Indicates whether users with this password policy will be required to
   * change their passwords in a secure manner that does not expose the new
   * password.
   *
   * @return  <CODE>true</CODE> if users with this password policy will be
   *          required to change their passwords in a secure manner that does
   *          not expose the new password, or <CODE>false</CODE> if they may
   *          change their password in an insecure manner.
   */
  public boolean requireSecurePasswordChanges()
  {
    return requireSecurePasswordChanges;
  }



  /**
   * Indicates whether user entries will be allowed to have multiple distinct
   * values in the password attribute.
   *
   * @return  <CODE>true</CODE> if clients will be allowed to have multiple
   *          distinct password values, or <CODE>false</CODE> if not.
   */
  public boolean allowMultiplePasswordValues()
  {
    return allowMultiplePasswordValues;
  }



  /**
   * Indicates whether clients will be allowed to set pre-encoded passwords that
   * are already hashed and therefore cannot be validated for correctness.
   *
   * @return  <CODE>true</CODE> if clients will be allowed to set pre-encoded
   *          passwords that cannot be validated, or <CODE>false</CODE> if not.
   */
  public boolean allowPreEncodedPasswords()
  {
    return allowPreEncodedPasswords;
  }



  /**
   * Retrieves the minimum password age, which is the minimum length of time in
   * seconds that must elapse between user password changes.
   *
   * @return  The minimum password age, which is the minimum length of time in
   *          seconds that must elapse between user password changes, or zero if
   *          there is no minimum age.
   */
  public int getMinimumPasswordAge()
  {
    if (minimumPasswordAge <= 0)
    {
      return 0;
    }

    return minimumPasswordAge;
  }



  /**
   * Retrieves the maximum length of time in seconds that will be allowed to
   * pass between password changes before the password is expired.
   *
   * @return  The maximum length of time in seconds that will be allowed to pass
   *          between password changes before the password is expired, or zero
   *          if password expiration should not be used.
   */
  public int getMaximumPasswordAge()
  {
    if (maximumPasswordAge < 0)
    {
      return 0;
    }

    return maximumPasswordAge;
  }



  /**
   * Retrieves the maximum length of time in seconds that will be allowed to
   * pass after an administrative password reset before that password is
   * expired.
   *
   * @return  The maximum length of time in seconds that will be allowed to pass
   *          after an administrative password reset before that password is
   *          expired, or zero if there is no limit.
   */
  public int getMaximumPasswordResetAge()
  {
    if (maximumPasswordResetAge < 0)
    {
      return 0;
    }

    return maximumPasswordResetAge;
  }



  /**
   * Retrieves the maximum length of time in seconds before the password will
   * expire that the user should start receiving warning notifications.
   *
   * @return  The maximum length of time in seconds before the password will
   *          expire that the user should start receiving warning notifications,
   *          or zero if no warning should be given.
   */
  public int getWarningInterval()
  {
    if (warningInterval < 0)
    {
      return 0;
    }

    return warningInterval;
  }



  /**
   * Indicates whether user passwords will be allowed to expire without the
   * user receiving at least one notification during the warning period.
   *
   * @return  <CODE>true</CODE> if user passwords will be allowed to expire
   *          without the user receiving at least one notification during the
   *          warning period, or <CODE>false</CODE> if the user will always see
   *          at least one warning before the password expires.
   */
  public boolean expirePasswordsWithoutWarning()
  {
    return expirePasswordsWithoutWarning;
  }



  /**
   * Indicates whether a user will be allowed to change their password after it
   * expires and they have no remaining grace logins (and will not be allowed to
   * perform any other operation until the password is changed).
   *
   * @return  <CODE>true</CODE> if a user will be allowed to change their
   *          password after it expires and they have no remaining grace longs,
   *          or <CODE>false</CODE> if the account will be completely locked and
   *          the password must be reset by an administrator.
   */
  public boolean allowExpiredPasswordChanges()
  {
    return allowExpiredPasswordChanges;
  }



  /**
   * Retrieves the maximum number of grace logins that a user will be allowed
   * after their password has expired before they are completely locked out.
   *
   * @return  The maximum number of grace logins that a user will be allowed
   *          after their password has expired before they are completely
   *          locked out, or zero if no grace logins will be allowed or the
   *          grace login duration will be in effect instead of a fixed number
   *          of logins.
   */
  public int getGraceLoginCount()
  {
    if (graceLoginCount < 0)
    {
      return 0;
    }

    return graceLoginCount;
  }



  /**
   * Retrieves the maximum number of authentication failures that will be
   * allowed before an account is locked out.
   *
   * @return  The maximum number of authentication failures that will be allowed
   *          before an account is locked out, or zero if no account lockout
   *          will be in effect.
   */
  public int getLockoutFailureCount()
  {
    if (lockoutFailureCount < 0)
    {
      return 0;
    }

    return lockoutFailureCount;
  }



  /**
   * Retrieves the maximum length of time in seconds that an account will be
   * locked out due to too many failed authentication attempts.
   *
   * @return  The maximum length of time in seconds that an account will be
   *          locked out due to too many failed authentication attempts, or
   *          zero if the account will remain locked until explicitly unlocked
   *          by an administrator.
   */
  public int getLockoutDuration()
  {
    if (lockoutDuration < 0)
    {
      return 0;
    }

    return lockoutDuration;
  }



  /**
   * Retrieves the maximum length of time in seconds that an authentication
   * failure will be held against a user before it is removed from the failed
   * login count.
   *
   * @return  The maximum length of time in seconds that an authentication
   *          failure will be held against a user before it is removed from the
   *          failed login count, or zero if authentication failures will never
   *          expire.
   */
  public int getLockoutFailureExpirationInterval()
  {
    if (lockoutFailureExpirationInterval < 0)
    {
      return 0;
    }

    return lockoutFailureExpirationInterval;
  }



  /**
   * Retrieves the time by which all users will be required to change their
   * passwords, expressed in the number of milliseconds since midnight of
   * January 1, 1970 (i.e., the zero time for
   * <CODE>System.currentTimeMillis()</CODE>).  Any passwords not changed before
   * this time will automatically enter a state in which they must be changed
   * before any other operation will be allowed.
   *
   * @return  The time by which all users will be required to change their
   *          passwords, or zero if no such constraint is in effect.
   */
  public long getRequireChangeByTime()
  {
    if (requireChangeByTime < 0)
    {
      return 0;
    }

    return requireChangeByTime;
  }



  /**
   * Retrieves the attribute type used to store the last login time.
   *
   * @return  The attribute type used to store the last login time, or
   *          <CODE>null</CODE> if the last login time is not to be maintained.
   */
  public AttributeType getLastLoginTimeAttribute()
  {
    return lastLoginTimeAttribute;
  }



  /**
   * Retrieves the format string that should be used for the last login time.
   *
   * @return  The format string that should be used to for the last login time,
   *          or <CODE>null</CODE> if the last login time is not to be
   *          maintained.
   */
  public String getLastLoginTimeFormat()
  {
    return lastLoginTimeFormat;
  }



  /**
   * Retrieves the list of previous last login time formats that might have been
   * used for users associated with this password policy.
   *
   * @return  The list of previous last login time formats that might have been
   *          used for users associated with this password policy.
   */
  public CopyOnWriteArrayList<String> getPreviousLastLoginTimeFormats()
  {
    return previousLastLoginTimeFormats;
  }



  /**
   * Retrieves the maximum length of time in seconds that an account will be
   * allowed to remain idle (no authentications performed as the user) before it
   * will be locked out.
   *
   * @return  The maximum length of time in seconds that an account will be
   *          allowed to remain idle before it will be locked out.
   */
  public int getIdleLockoutInterval()
  {
    if (idleLockoutInterval < 0)
    {
      return 0;
    }

    return idleLockoutInterval;
  }



  /**
   * Retrieves the state update failure policy for this password policy.
   *
   * @return  The state update failure policy for this password policy.
   */
  public PasswordPolicyCfgDefn.StateUpdateFailurePolicy
              getStateUpdateFailurePolicy()
  {
    return stateUpdateFailurePolicy;
  }



  /**
   * Retrieves a string representation of this password policy.
   *
   * @return  A string representation of this password policy.
   */
  public String toString()
  {
    StringBuilder buffer = new StringBuilder();
    toString(buffer);
    return buffer.toString();
  }



  /**
   * Appends a string representation of this password policy to the provided
   * buffer.
   *
   * @param  buffer  The buffer to which the information should be appended.
   */
  public void toString(StringBuilder buffer)
  {
    buffer.append("Password Attribute:                    ");
    buffer.append(passwordAttribute.getNameOrOID());
    buffer.append(EOL);

    buffer.append("Default Password Storage Schemes:      ");
    if ((defaultStorageSchemes == null) || defaultStorageSchemes.isEmpty())
    {
      buffer.append("{none specified}");
      buffer.append(EOL);
    }
    else
    {
      Iterator<PasswordStorageScheme<?>> iterator =
           defaultStorageSchemes.iterator();
      buffer.append(iterator.next().getStorageSchemeName());
      buffer.append(EOL);

      while (iterator.hasNext())
      {
        buffer.append("                                       ");
        buffer.append(iterator.next().getStorageSchemeName());
        buffer.append(EOL);
      }
    }

    buffer.append("Deprecated Password Storage Schemes:   ");
    if ((deprecatedStorageSchemes == null) ||
        deprecatedStorageSchemes.isEmpty())
    {
      buffer.append("{none specified}");
      buffer.append(EOL);
    }
    else
    {
      Iterator<String> iterator = deprecatedStorageSchemes.iterator();
      buffer.append(iterator.next());
      buffer.append(EOL);

      while (iterator.hasNext())
      {
        buffer.append("                                       ");
        buffer.append(iterator.next());
        buffer.append(EOL);
      }
    }

    buffer.append("Allow Multiple Password Values:        ");
    buffer.append(allowMultiplePasswordValues);
    buffer.append(EOL);

    buffer.append("Allow Pre-Encoded Passwords:           ");
    buffer.append(allowPreEncodedPasswords);
    buffer.append(EOL);

    buffer.append("Allow User Password Changes:           ");
    buffer.append(allowUserPasswordChanges);
    buffer.append(EOL);

    buffer.append("Force Password Change on Add:          ");
    buffer.append(forceChangeOnAdd);
    buffer.append(EOL);

    buffer.append("Force Password Change on Admin Reset:  ");
    buffer.append(forceChangeOnReset);
    buffer.append(EOL);

    buffer.append("Require Current Password:              ");
    buffer.append(requireCurrentPassword);
    buffer.append(EOL);

    buffer.append("Require Secure Authentication:         ");
    buffer.append(requireSecureAuthentication);
    buffer.append(EOL);

    buffer.append("Require Secure Password Changes:       ");
    buffer.append(requireSecurePasswordChanges);
    buffer.append(EOL);

    buffer.append("Lockout Failure Expiration Interval:   ");
    buffer.append(lockoutFailureExpirationInterval);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("Password Validators:                   ");
    if ((passwordValidators == null) || passwordValidators.isEmpty())
    {
      buffer.append("{none specified}");
      buffer.append(EOL);
    }
    else
    {
      Iterator<DN> iterator = passwordValidators.keySet().iterator();
      iterator.next().toString(buffer);
      buffer.append(EOL);

      while (iterator.hasNext())
      {
        buffer.append("                                       ");
        iterator.next().toString(buffer);
        buffer.append(EOL);
      }
    }

    buffer.append("Skip Validation for Administrators:    ");
    buffer.append(skipValidationForAdministrators);
    buffer.append(EOL);

    buffer.append("Password Generator:                    ");
    if (passwordGenerator == null)
    {
      buffer.append("{none specified}");
    }
    else
    {
      passwordGeneratorDN.toString(buffer);
    }
    buffer.append(EOL);

    buffer.append("Account Status Notification Handlers:  ");
    if ((notificationHandlers == null) || notificationHandlers.isEmpty())
    {
      buffer.append("{none specified}");
      buffer.append(EOL);
    }
    else
    {
      Iterator<DN> iterator = notificationHandlers.keySet().iterator();
      iterator.next().toString(buffer);
      buffer.append(EOL);

      while (iterator.hasNext())
      {
        buffer.append("                                       ");
        iterator.next().toString(buffer);
        buffer.append(EOL);
      }
    }

    buffer.append("Minimum Password Age:                  ");
    buffer.append(minimumPasswordAge);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("Maximum Password Age:                  ");
    buffer.append(maximumPasswordAge);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("Maximum Password Reset Age:            ");
    buffer.append(maximumPasswordResetAge);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("Expiration Warning Interval:           ");
    buffer.append(warningInterval);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("Expire Passwords Without Warning:      ");
    buffer.append(expirePasswordsWithoutWarning);
    buffer.append(EOL);

    buffer.append("Allow Expired Password Changes:        ");
    buffer.append(allowExpiredPasswordChanges);
    buffer.append(EOL);

    buffer.append("Grace Login Count:                     ");
    buffer.append(graceLoginCount);
    buffer.append(EOL);

    buffer.append("Lockout Failure Count:                 ");
    buffer.append(lockoutFailureCount);
    buffer.append(EOL);

    buffer.append("Lockout Duration:                      ");
    buffer.append(lockoutDuration);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("Lockout Count Expiration Interval:     ");
    buffer.append(lockoutFailureExpirationInterval);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("Required Password Change By Time:      ");
    if (requireChangeByTime <= 0)
    {
      buffer.append("{none specified}");
    }
    else
    {
      SimpleDateFormat dateFormat =
           new SimpleDateFormat(DATE_FORMAT_GENERALIZED_TIME);
      dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
      buffer.append(dateFormat.format(new Date(requireChangeByTime)));
    }
    buffer.append(EOL);

    buffer.append("Last Login Time Attribute:             ");
    if (lastLoginTimeAttribute == null)
    {
      buffer.append("{none specified}");
    }
    else
    {
      buffer.append(lastLoginTimeAttribute.getNameOrOID());
    }
    buffer.append(EOL);

    buffer.append("Last Login Time Format:                ");
    if (lastLoginTimeFormat == null)
    {
      buffer.append("{none specified}");
    }
    else
    {
      buffer.append(lastLoginTimeFormat);
    }
    buffer.append(EOL);

    buffer.append("Previous Last Login Time Formats:      ");
    if ((previousLastLoginTimeFormats == null) ||
        previousLastLoginTimeFormats.isEmpty())
    {
      buffer.append("{none specified}");
      buffer.append(EOL);
    }
    else
    {
      Iterator<String> iterator = previousLastLoginTimeFormats.iterator();

      buffer.append(iterator.next());
      buffer.append(EOL);

      while (iterator.hasNext())
      {
        buffer.append("                                       ");
        buffer.append(iterator.next());
        buffer.append(EOL);
      }
    }

    buffer.append("Idle Lockout Interval:                 ");
    buffer.append(idleLockoutInterval);
    buffer.append(" seconds");
    buffer.append(EOL);

    buffer.append("History Count:                         ");
    buffer.append(historyCount);
    buffer.append(EOL);

    buffer.append("Update Failure Policy:                 ");
    buffer.append(stateUpdateFailurePolicy.toString());
    buffer.append(EOL);
  }
}

