/*
 * 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn;
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.loggers.ErrorLogger;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.LDAPAttribute;
import org.opends.server.schema.AuthPasswordSyntax;
import org.opends.server.schema.GeneralizedTimeSyntax;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.types.*;
import org.opends.server.util.TimeThread;

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



/**
 * This class provides a data structure for holding password policy state
 * information for a user account.
 */
public class PasswordPolicyState
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();



  // The user entry with which this state information is associated.
  private final Entry userEntry;

  // Indicates whether the user entry itself should be updated or if the updates
  // should be stored as modifications.
  private final boolean updateEntry;

  // The string representation of the user's DN.
  private final String userDNString;

  // The password policy with which the account is associated.
  private final PasswordPolicy passwordPolicy;

  // The current time for use in all password policy calculations.
  private final long currentTime;

  // The time that the user's password was last changed.
  private long passwordChangedTime = Long.MIN_VALUE;

  // Indicates whether the user's account is expired.
  private ConditionResult isAccountExpired = ConditionResult.UNDEFINED;

  // Indicates whether the user's account is disabled.
  private ConditionResult isDisabled = ConditionResult.UNDEFINED;

  // Indicates whether the user's password is expired.
  private ConditionResult isPasswordExpired = ConditionResult.UNDEFINED;

  // Indicates whether the warning to send to the client would be the first
  // warning for the user.
  private ConditionResult isFirstWarning = ConditionResult.UNDEFINED;

  // Indicates whether the user's account is locked by the idle lockout.
  private ConditionResult isIdleLocked = ConditionResult.UNDEFINED;

  // Indicates whether the user may use a grace login if the password is expired
  // and there are one or more grace logins remaining.
  private ConditionResult mayUseGraceLogin = ConditionResult.UNDEFINED;

  // Indicates whether the user's password must be changed.
  private ConditionResult mustChangePassword = ConditionResult.UNDEFINED;

  // Indicates whether the user should be warned of an upcoming expiration.
  private ConditionResult shouldWarn = ConditionResult.UNDEFINED;

  // The number of seconds until the user's account is automatically unlocked.
  private int secondsUntilUnlock = Integer.MIN_VALUE;

  // The set of authentication failure times for this user.
  private List<Long> authFailureTimes = null;

  // The set of grace login times for this user.
  private List<Long> graceLoginTimes = null;

  // The time that the user's account should expire (or did expire).
  private long accountExpirationTime = Long.MIN_VALUE;

  // The time that the user's entry was locked due to too many authentication
  // failures.
  private long failureLockedTime = Long.MIN_VALUE;

  // The time that the user last authenticated to the Directory Server.
  private long lastLoginTime = Long.MIN_VALUE;

  // The time that the user's password should expire (or did expire).
  private long passwordExpirationTime = Long.MIN_VALUE;

  // The last required change time with which the user complied.
  private long requiredChangeTime = Long.MIN_VALUE;

  // The time that the user was first warned about an upcoming expiration.
  private long warnedTime = Long.MIN_VALUE;

  // The set of modifications that should be applied to the user's entry.
  private LinkedList<Modification> modifications
       = new LinkedList<Modification>();



  /**
   * Creates a new password policy state object with the provided information.
   *
   * @param  userEntry    The entry with the user account.
   * @param  updateEntry  Indicates whether changes should update the provided
   *                      user entry directly or whether they should be
   *                      collected as a set of modifications.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to
   *                              determine the password policy for the user or
   *                              perform any other state initialization.
   */
  public PasswordPolicyState(Entry userEntry, boolean updateEntry)
       throws DirectoryException
  {
    this(userEntry, updateEntry, TimeThread.getTime(), false);
  }



  /**
   * Creates a new password policy state object with the provided information.
   * Note that this version of the constructor should only be used for testing
   * purposes when the tests should be evaluated with a fixed time rather than
   * the actual current time.  For all other purposes, the other constructor
   * should be used.
   *
   * @param  userEntry          The entry with the user account.
   * @param  updateEntry        Indicates whether changes should update the
   *                            provided user entry directly or whether they
   *                            should be collected as a set of modifications.
   * @param  currentTime        The time to use as the current time for all
   *                            time-related determinations.
   * @param  useDefaultOnError  Indicates whether the server should fall back to
   *                            using the default password policy if there is a
   *                            problem with the configured policy for the user.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to
   *                              determine the password policy for the user or
   *                              perform any other state initialization.
   */
  public PasswordPolicyState(Entry userEntry, boolean updateEntry,
                             long currentTime, boolean useDefaultOnError)
       throws DirectoryException
  {
    this.userEntry   = userEntry;
    this.updateEntry = updateEntry;
    this.currentTime = currentTime;

    userDNString     = userEntry.getDN().toString();
    passwordPolicy   = getPasswordPolicyInternal(this.userEntry,
                                                 useDefaultOnError);

    // Get the password changed time for the user.
    AttributeType type
         = DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_CHANGED_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
           OP_ATTR_PWPOLICY_CHANGED_TIME);
    }

    passwordChangedTime = getGeneralizedTime(type);
    if (passwordChangedTime <= 0)
    {
      // Get the time that the user's account was created.
      AttributeType createTimeType
           = DirectoryServer.getAttributeType(OP_ATTR_CREATE_TIMESTAMP_LC);
      if (createTimeType == null)
      {
        createTimeType
            = DirectoryServer.getDefaultAttributeType(OP_ATTR_CREATE_TIMESTAMP);
      }
      passwordChangedTime = getGeneralizedTime(createTimeType);

      if (passwordChangedTime <= 0)
      {
        passwordChangedTime = 0;

        if (debugEnabled())
        {
          TRACER.debugWarning("Could not determine password changed time for " +
              "user %s.", userDNString);
        }
      }
    }
  }



  /**
   * Retrieves the password policy for the user. If the user entry contains the
   * ds-pwp-password-policy-dn attribute (whether real or virtual), that
   * password policy is returned, otherwise the default password policy is
   * returned.
   *
   * @param  userEntry          The user entry.
   * @param  useDefaultOnError  Indicates whether the server should fall back to
   *                            using the default password policy if there is a
   *                            problem with the configured policy for the user.
   *
   * @return  The password policy for the user.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to
   *                              determine the password policy for the user.
   */
  private static PasswordPolicy getPasswordPolicyInternal(Entry userEntry,
                                     boolean useDefaultOnError)
       throws DirectoryException
  {
    String userDNString = userEntry.getDN().toString();
    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_POLICY_DN, true);

    List<Attribute> attrList = userEntry.getAttribute(type);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;

        AttributeValue v = a.iterator().next();
        DN subentryDN;
        try
        {
          subentryDN = DN.decode(v.getValue());
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          if (debugEnabled())
          {
            TRACER.debugError("Could not parse password policy subentry " +
                "DN %s for user %s: %s",
                       v.getValue().toString(), userDNString,
                       stackTraceToSingleLineString(e));
          }

          Message message = ERR_PWPSTATE_CANNOT_DECODE_SUBENTRY_VALUE_AS_DN.get(
              v.getValue().toString(), userDNString, e.getMessage());
          if (useDefaultOnError)
          {
            ErrorLogger.logError(message);
            return DirectoryServer.getDefaultPasswordPolicy();
          }
          else
          {
            throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message,
                                         e);
          }
        }

        PasswordPolicy policy = DirectoryServer.getPasswordPolicy(subentryDN);
        if (policy == null)
        {
          if (debugEnabled())
          {
            TRACER.debugError("Password policy subentry %s for user %s " +
                 "is not defined in the Directory Server.",
                       String.valueOf(subentryDN), userDNString);
          }

          Message message = ERR_PWPSTATE_NO_SUCH_POLICY.get(
              userDNString, String.valueOf(subentryDN));
          if (useDefaultOnError)
          {
            ErrorLogger.logError(message);
            return DirectoryServer.getDefaultPasswordPolicy();
          }
          else
          {
            throw new DirectoryException(
                 DirectoryServer.getServerErrorResultCode(), message);
          }
        }

        if (debugEnabled())
        {
          TRACER.debugInfo("Using password policy subentry %s for user %s.",
              String.valueOf(subentryDN), userDNString);
        }

        return policy;
      }
    }

    // There is no policy subentry defined: use the default.
    if (debugEnabled())
    {
      TRACER.debugInfo("Using the default password policy for user %s",
          userDNString);
    }

    return DirectoryServer.getDefaultPasswordPolicy();
  }



   /**
    * Retrieves the value of the specified attribute as a string.
    *
    * @param  attributeType  The attribute type whose value should be retrieved.
    *
    * @return  The value of the specified attribute as a string, or
    *          <CODE>null</CODE> if there is no such value.
    */
  private String getValue(AttributeType attributeType)
  {
    String stringValue = null;

    List<Attribute> attrList = userEntry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;

        stringValue = a.iterator().next().getValue().toString();
        break ;
      }
    }

    if (stringValue == null)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning null because attribute %s does not " +
            "exist in user entry %s",
            attributeType.getNameOrOID(), userDNString);
      }
    }
    else
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning value %s for user %s",
            stringValue, userDNString);
      }
    }

    return stringValue;
  }



  /**
   * Retrieves the value of the specified attribute from the user's entry as a
   * time in generalized time format.
   *
   * @param  attributeType  The attribute type whose value should be parsed as a
   *                        generalized time value.
   *
   * @return  The requested time, or -1 if it could not be determined.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to
   *                              decode the value as a generalized time.
   */
  private long getGeneralizedTime(AttributeType attributeType)
          throws DirectoryException
  {
    long timeValue = -1 ;

    List<Attribute> attrList = userEntry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;

        AttributeValue v = a.iterator().next();
        try
        {
          timeValue = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
                          v.getNormalizedValue());
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);

            TRACER.debugWarning("Unable to decode value %s for attribute %s " +
                "in user entry %s: %s",
                v.getValue().toString(), attributeType.getNameOrOID(),
                userDNString, stackTraceToSingleLineString(e));
          }

          Message message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.
              get(v.getValue().toString(), attributeType.getNameOrOID(),
                  userDNString, String.valueOf(e));
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message, e);
        }
        break ;
      }
    }

    if (timeValue == -1)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning -1 because attribute %s does not " +
            "exist in user entry %s",
            attributeType.getNameOrOID(), userDNString);
      }
    }
    // FIXME: else to be consistent...

    return timeValue;
  }



  /**
   * Retrieves the set of values of the specified attribute from the user's
   * entry in generalized time format.
   *
   * @param  attributeType  The attribute type whose values should be parsed as
   *                        generalized time values.
   *
   * @return  The set of generalized time values, or an empty list if there are
   *          none.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to
   *                              decode a value as a generalized time.
   */
  private List<Long> getGeneralizedTimes(AttributeType attributeType)
          throws DirectoryException
  {
    ArrayList<Long> timeValues = new ArrayList<Long>();

    List<Attribute> attrList = userEntry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a)
        {
          try
          {
            timeValues.add(GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
                                                       v.getNormalizedValue()));
          }
          catch (Exception e)
          {
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, e);

              TRACER.debugWarning("Unable to decode value %s for attribute %s" +
                  "in user entry %s: %s",
                  v.getValue().toString(), attributeType.getNameOrOID(),
                  userDNString, stackTraceToSingleLineString(e));
            }

            Message message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.
                get(v.getValue().toString(), attributeType.getNameOrOID(),
                    userDNString, String.valueOf(e));
            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                         message, e);
          }
        }
      }
    }

    if (timeValues.isEmpty())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning an empty list because attribute %s " +
            "does not exist in user entry %s",
            attributeType.getNameOrOID(), userDNString);
      }
    }
    return timeValues;
  }



  /**
   * Retrieves the value of the specified attribute from the user's entry as a
   * Boolean.
   *
   * @param  attributeType  The attribute type whose value should be parsed as a
   *                        Boolean.
   *
   * @return  The attribute's value represented as a ConditionResult value, or
   *          ConditionResult.UNDEFINED if the specified attribute does not
   *          exist in the entry.
   *
   * @throws  DirectoryException  If the value cannot be decoded as a Boolean.
   */
  private ConditionResult getBoolean(AttributeType attributeType)
          throws DirectoryException
  {
    List<Attribute> attrList = userEntry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;

        String valueString
             = toLowerCase(a.iterator().next().getValue().toString());

        if (valueString.equals("true") || valueString.equals("yes") ||
            valueString.equals("on") || valueString.equals("1"))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Attribute %s resolves to true for user entry " +
                "%s", attributeType.getNameOrOID(), userDNString);
          }

          return ConditionResult.TRUE;
        }

        if (valueString.equals("false") || valueString.equals("no") ||
                 valueString.equals("off") || valueString.equals("0"))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Attribute %s resolves to false for user " +
                "entry %s", attributeType.getNameOrOID(), userDNString);
          }

          return ConditionResult.FALSE;
        }

        if(debugEnabled())
        {
          TRACER.debugError("Unable to resolve value %s for attribute %s " +
              "in user entry %s as a Boolean.",
              valueString, attributeType.getNameOrOID(),
              userDNString);
        }

        Message message = ERR_PWPSTATE_CANNOT_DECODE_BOOLEAN.get(
            valueString, attributeType.getNameOrOID(), userDNString);
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
            message);
      }
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Returning %s because attribute %s does not exist " +
          "in user entry %s",
          ConditionResult.UNDEFINED.toString(),
          attributeType.getNameOrOID(), userDNString);
    }

    return ConditionResult.UNDEFINED;
  }



  /**
   * Retrieves the password policy associated with this state information.
   *
   * @return  The password policy associated with this state information.
   */
  public PasswordPolicy getPolicy()
  {
    return passwordPolicy;
  }



  /**
   * Retrieves the time that the password was last changed.
   *
   * @return  The time that the password was last changed.
   */
  public long getPasswordChangedTime()
  {
    return passwordChangedTime;
  }



  /**
   * Retrieves the time that this password policy state object was created.
   *
   * @return  The time that this password policy state object was created.
   */
  public long getCurrentTime()
  {
    return currentTime;
  }



  /**
   * Retrieves the unmodifiable set of values for the password
   * attribute from the user entry.
   *
   * @return The unmodifiable set of values for the password attribute
   *         from the user entry.
   */
  public Set<AttributeValue> getPasswordValues()
  {
    List<Attribute> attrList = userEntry.getAttribute(passwordPolicy
        .getPasswordAttribute());
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;

        Set<AttributeValue> values =
          new LinkedHashSet<AttributeValue>(a.size());
        for (AttributeValue value : a)
        {
          values.add(value);
        }
        return Collections.unmodifiableSet(values);
      }
    }

    return Collections.emptySet();
  }



  /**
   * Sets a new value for the password changed time equal to the
   * current time.
   */
  public void setPasswordChangedTime()
  {
    setPasswordChangedTime(currentTime);
  }



  /**
   * Sets a new value for the password changed time equal to the specified time.
   * This method should generally only be used for testing purposes, since the
   * variant that uses the current time is preferred almost everywhere else.
   *
   * @param  passwordChangedTime  The time to use
   */
  public void setPasswordChangedTime(long passwordChangedTime)
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Setting password changed time for user %s to " +
          "current time of %d", userDNString, currentTime);
    }

    // passwordChangedTime is computed in the constructor from values in the
    // entry.
    if (this.passwordChangedTime != passwordChangedTime)
    {
      this.passwordChangedTime = passwordChangedTime;

      String timeValue = GeneralizedTimeSyntax.format(passwordChangedTime);
      Attribute a = Attributes.create(OP_ATTR_PWPOLICY_CHANGED_TIME,
          timeValue);

      if (updateEntry)
      {
        ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        userEntry.putAttribute(a.getAttributeType(), attrList);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE, a, true));
      }
    }
  }



  /**
   * Removes the password changed time value from the user's entry.  This should
   * only be used for testing purposes, as it can really mess things up if you
   * don't know what you're doing.
   */
  public void clearPasswordChangedTime()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing password changed time for user %s",
          userDNString);
    }

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_CHANGED_TIME_LC,
                                       true);
    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      Attribute a = Attributes.empty(type);
      modifications.add(new Modification(ModificationType.REPLACE, a, true));
    }


    // Fall back to using the entry creation time as the password changed time,
    // if it's defined.  Otherwise, use a value of zero.
    AttributeType createTimeType =
         DirectoryServer.getAttributeType(OP_ATTR_CREATE_TIMESTAMP_LC, true);
    try
    {
      passwordChangedTime = getGeneralizedTime(createTimeType);
      if (passwordChangedTime <= 0)
      {
        passwordChangedTime = 0;
      }
    }
    catch (Exception e)
    {
      passwordChangedTime = 0;
    }
  }




  /**
   * Indicates whether the user account has been administratively disabled.
   *
   * @return  <CODE>true</CODE> if the user account has been administratively
   *          disabled, or <CODE>false</CODE> otherwise.
   */
  public boolean isDisabled()
  {
    if (isDisabled != ConditionResult.UNDEFINED)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored result of %b for user %s",
            (isDisabled == ConditionResult.TRUE), userDNString);
      }

      return isDisabled == ConditionResult.TRUE;
    }

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_DISABLED, true);
    try
    {
      isDisabled = getBoolean(type);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      isDisabled = ConditionResult.TRUE;
      if (debugEnabled())
      {
          TRACER.debugWarning("User %s is considered administratively " +
              "disabled because an error occurred while attempting to make " +
              "the determination: %s.",
                       userDNString, stackTraceToSingleLineString(e));
      }

      return true;
    }

    if (isDisabled == ConditionResult.UNDEFINED)
    {
      isDisabled = ConditionResult.FALSE;
      if (debugEnabled())
      {
        TRACER.debugInfo("User %s is not administratively disabled since " +
            "the attribute \"%s\" is not present in the entry.",
            userDNString, OP_ATTR_ACCOUNT_DISABLED);
      }
      return false;
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("User %s %s administratively disabled.",
          userDNString,
          ((isDisabled == ConditionResult.TRUE) ? " is" : " is not"));
    }

    return isDisabled == ConditionResult.TRUE;
  }



  /**
   * Updates the user entry to indicate whether user account has been
   * administratively disabled.
   *
   * @param  isDisabled  Indicates whether the user account has been
   *                     administratively disabled.
   */
  public void setDisabled(boolean isDisabled)
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Updating user %s to set the disabled flag to %b",
          userDNString, isDisabled);
    }


    if (isDisabled == isDisabled())
    {
      return; // requested state matches current state
    }

    this.isDisabled = ConditionResult.inverseOf(this.isDisabled);

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_DISABLED, true);

    if (isDisabled)
    {
      Attribute a = Attributes.create(type, String.valueOf(true));

      if (updateEntry)
      {
        ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        userEntry.putAttribute(type, attrList);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE, a, true));
      }
    }
    else
    {
      // erase
      if (updateEntry)
      {
        userEntry.removeAttribute(type);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE,
                                           Attributes.empty(type), true));
      }
    }
  }



  /**
   * Indicates whether the user's account is currently expired.
   *
   * @return  <CODE>true</CODE> if the user's account is expired, or
   *          <CODE>false</CODE> if not.
   */
  public boolean isAccountExpired()
  {
    if (isAccountExpired != ConditionResult.UNDEFINED)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored result of %b for user %s",
            (isAccountExpired == ConditionResult.TRUE), userDNString);
      }

      return isAccountExpired == ConditionResult.TRUE;
    }

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_EXPIRATION_TIME,
                                          true);

    try
    {
      accountExpirationTime = getGeneralizedTime(type);
     }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      isAccountExpired = ConditionResult.TRUE;
      if (debugEnabled())
      {
          TRACER.debugWarning("User %s is considered to have an expired " +
               "account because an error occurred while attempting to make " +
               "the determination: %s.",
              userDNString, stackTraceToSingleLineString(e));
      }

      return true;
    }

    if (accountExpirationTime > currentTime)
    {
      // The user does have an expiration time, but it hasn't arrived yet.
      isAccountExpired = ConditionResult.FALSE;
      if (debugEnabled())
      {
        TRACER.debugInfo("The account for user %s is not expired because " +
            "the expiration time has not yet arrived.", userDNString);
      }
    }
    else if (accountExpirationTime >= 0)
    {
      // The user does have an expiration time, and it is in the past.
      isAccountExpired = ConditionResult.TRUE;
      if (debugEnabled())
      {
        TRACER.debugInfo("The account for user %s is expired because the " +
            "expiration time in that account has passed.", userDNString);
      }
    }
    else
    {
      // The user doesn't have an expiration time in their entry, so it
      // can't be expired.
      isAccountExpired = ConditionResult.FALSE;
      if (debugEnabled())
      {
        TRACER.debugInfo("The account for user %s is not expired because " +
            "there is no expiration time in the user's entry.",
            userDNString);
      }
    }

    return isAccountExpired == ConditionResult.TRUE;
  }



  /**
   * Retrieves the time at which the user's account will expire.
   *
   * @return  The time at which the user's account will expire, or -1 if it is
   *          not configured with an expiration time.
   */
  public long getAccountExpirationTime()
  {
    if (accountExpirationTime == Long.MIN_VALUE)
    {
      isAccountExpired();
    }

    return accountExpirationTime;
  }



  /**
   * Sets the user's account expiration time to the specified value.
   *
   * @param  accountExpirationTime  The time that the user's account should
   *                                expire.
   */
  public void setAccountExpirationTime(long accountExpirationTime)
  {
    if (accountExpirationTime < 0)
    {
      clearAccountExpirationTime();
    }
    else
    {
      String timeStr = GeneralizedTimeSyntax.format(accountExpirationTime);

      if (debugEnabled())
      {
        TRACER.debugInfo("Setting account expiration time for user %s to %s",
            userDNString, timeStr);
      }

      this.accountExpirationTime = accountExpirationTime;
      AttributeType type =
           DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_EXPIRATION_TIME,
                                            true);

      Attribute a = Attributes.create(type, timeStr);
      if (updateEntry)
      {
        ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        userEntry.putAttribute(type, attrList);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE, a, true));
      }
    }
  }



  /**
   * Clears the user's account expiration time.
   */
  public void clearAccountExpirationTime()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing account expiration time for user %s",
          userDNString);
    }

    accountExpirationTime = -1;

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_EXPIRATION_TIME,
                                          true);

    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE,
          Attributes.empty(type), true));
    }
  }



  /**
   * Retrieves the set of times of failed authentication attempts for
   * the user. If authentication failure time expiration is enabled,
   * and there are expired times in the entry, these times are removed
   * from the instance field and an update is provided to delete those
   * values from the entry.
   *
   * @return The set of times of failed authentication attempts for
   *         the user, which will be an empty list in the case of no
   *         valid (unexpired) times in the entry.
   */
  public List<Long> getAuthFailureTimes()
  {
    if (authFailureTimes != null)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored auth failure time list of %d " +
            "elements for user %s" +
            authFailureTimes.size(), userDNString);
      }

      return authFailureTimes;
    }

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
           OP_ATTR_PWPOLICY_FAILURE_TIME);
    }

    try
    {
      authFailureTimes = getGeneralizedTimes(type);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      if (debugEnabled())
      {
        TRACER.debugWarning("Error while processing auth failure times " +
             "for user %s: %s",
                     userDNString, stackTraceToSingleLineString(e));
      }

      authFailureTimes = new ArrayList<Long>();

      if (updateEntry)
      {
        userEntry.removeAttribute(type);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE,
            Attributes.empty(type), true));
      }

      return authFailureTimes;
    }

    if (authFailureTimes.isEmpty())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning an empty auth failure time list for " +
            "user %s because the attribute is absent from the entry.",
            userDNString);
      }

      return authFailureTimes;
    }

    // Remove any expired failures from the list.
    if (passwordPolicy.getLockoutFailureExpirationInterval() > 0)
    {
      LinkedHashSet<AttributeValue> valuesToRemove = null;

      long expirationTime = currentTime -
           (passwordPolicy.getLockoutFailureExpirationInterval() * 1000L);
      Iterator<Long> iterator = authFailureTimes.iterator();
      while (iterator.hasNext())
      {
        long l = iterator.next();
        if (l < expirationTime)
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Removing expired auth failure time %d for " +
                "user %s", l, userDNString);
          }

          iterator.remove();

          if (valuesToRemove == null)
          {
            valuesToRemove = new LinkedHashSet<AttributeValue>();
          }

          valuesToRemove.add(AttributeValues.create(type,
                                              GeneralizedTimeSyntax.format(l)));
        }
      }

      if (valuesToRemove != null)
      {
        if (updateEntry)
        {
          if (authFailureTimes.isEmpty())
          {
            userEntry.removeAttribute(type);
          }
          else
          {
            AttributeBuilder builder = new AttributeBuilder(type);
            for (Long l : authFailureTimes)
            {
              builder.add(
                 AttributeValues.create(type, GeneralizedTimeSyntax.format(l)));
            }
            ArrayList<Attribute> keepList = new ArrayList<Attribute>(1);
            keepList.add(builder.toAttribute());
            userEntry.putAttribute(type, keepList);
          }
        }
        else
        {
          AttributeBuilder builder = new AttributeBuilder(type);
          builder.addAll(valuesToRemove);
          Attribute a = builder.toAttribute();
          modifications.add(new Modification(ModificationType.DELETE, a,
                                             true));
        }
      }
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Returning auth failure time list of %d elements " +
          "for user %s", authFailureTimes.size(), userDNString);
    }

    return authFailureTimes;
  }



  /**
   * Updates the set of authentication failure times to include the current
   * time. If the number of failures reaches the policy configuration limit,
   * lock the account.
   */
  public void updateAuthFailureTimes()
  {
    if (passwordPolicy.getLockoutFailureCount() <= 0)
    {
      return;
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Updating authentication failure times for user %s",
          userDNString);
    }


    List<Long> failureTimes = getAuthFailureTimes();
    // Note: failureTimes == this.authFailureTimes
    long highestFailureTime = -1;
    for (Long l : failureTimes)
    {
      highestFailureTime = Math.max(l, highestFailureTime);
    }

    if (highestFailureTime >= currentTime)
    {
      highestFailureTime++;
    }
    else
    {
      highestFailureTime = currentTime;
    }
    failureTimes.add(highestFailureTime);

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
                                  OP_ATTR_PWPOLICY_FAILURE_TIME);
    }

    AttributeBuilder builder = new AttributeBuilder(type);
    for (Long l : failureTimes)
    {
      builder.add(AttributeValues.create(type,
          GeneralizedTimeSyntax.format(l)));
    }

    ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
    attrList.add(builder.toAttribute());

    Attribute addAttr = Attributes.create(type, AttributeValues.create(type,
        GeneralizedTimeSyntax.format(highestFailureTime)));

    if (updateEntry)
    {
      userEntry.putAttribute(type, attrList);
    }
    else
    {
      modifications.add(new Modification(ModificationType.ADD, addAttr, true));
    }

    // Now check to see if there have been sufficient failures to lock the
    // account.
    int lockoutCount = passwordPolicy.getLockoutFailureCount();
    if ((lockoutCount > 0) && (lockoutCount <= authFailureTimes.size()))
    {
      setFailureLockedTime(highestFailureTime);
      if (debugEnabled())
      {
        TRACER.debugInfo("Locking user account %s due to too many failures.",
            userDNString);
      }
    }
  }



  /**
   * Explicitly specifies the auth failure times for the associated user.  This
   * should generally only be used for testing purposes.  Note that it will also
   * set or clear the locked time as appropriate.
   *
   * @param  authFailureTimes  The set of auth failure times to use for the
   *                           account.  An empty list or {@code null} will
   *                           clear the account of any existing failures.
   */
  public void setAuthFailureTimes(List<Long> authFailureTimes)
  {
    if ((authFailureTimes == null) || authFailureTimes.isEmpty())
    {
      clearAuthFailureTimes();
      clearFailureLockedTime();
      return;
    }

    long highestFailureTime = -1;
    for (Long l : authFailureTimes)
    {
      highestFailureTime = Math.max(l, highestFailureTime);
    }

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC,
                                          true);

    AttributeBuilder builder = new AttributeBuilder(type);
    for (Long l : authFailureTimes)
    {
      builder
          .add(AttributeValues.create(type, GeneralizedTimeSyntax.format(l)));
    }
    Attribute a = builder.toAttribute();

    if (updateEntry)
    {
      ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
      attrList.add(a);
      userEntry.putAttribute(type, attrList);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE, a, true));
    }

    // Now check to see if there have been sufficient failures to lock the
    // account.
    int lockoutCount = passwordPolicy.getLockoutFailureCount();
    if ((lockoutCount > 0) && (lockoutCount <= authFailureTimes.size()))
    {
      setFailureLockedTime(highestFailureTime);
      if (debugEnabled())
      {
        TRACER.debugInfo("Locking user account %s due to too many failures.",
            userDNString);
      }
    }
  }



  /**
   * Updates the user entry to remove any record of previous authentication
   * failure times.
   */
  private void clearAuthFailureTimes()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing authentication failure times for user %s",
          userDNString);
    }

    List<Long> failureTimes = getAuthFailureTimes();
    if (failureTimes.isEmpty())
    {
      return;
    }

    failureTimes.clear(); // Note: failureTimes == this.authFailureTimes

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
                                  OP_ATTR_PWPOLICY_FAILURE_TIME);
    }

    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE,
                                         Attributes.empty(type), true));
    }
  }


  /**
   * Retrieves the time of an authentication failure lockout for the user.
   *
   * @return  The time of an authentication failure lockout for the user, or -1
   *          if no such time is present in the entry.
   */
  private long getFailureLockedTime()
  {
    if (failureLockedTime != Long.MIN_VALUE)
    {
      return failureLockedTime;
    }

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
           OP_ATTR_PWPOLICY_LOCKED_TIME);
    }

    try
    {
      failureLockedTime = getGeneralizedTime(type);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      failureLockedTime = currentTime;
      if (debugEnabled())
      {
        TRACER.debugWarning("Returning current time for user %s because an " +
            "error occurred: %s",
                     userDNString, stackTraceToSingleLineString(e));
      }

      return failureLockedTime;
    }

    // An expired locked time is handled in lockedDueToFailures.
    return failureLockedTime;
  }



  /**
    Sets the failure lockout attribute in the entry to the requested time.

    @param time  The time to which to set the entry's failure lockout attribute.
   */
  private void setFailureLockedTime(final long time)
  {
    if (time == getFailureLockedTime())
    {
      return;
    }

    failureLockedTime = time;

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
                                  OP_ATTR_PWPOLICY_LOCKED_TIME);
    }

    Attribute a = Attributes.create(type, AttributeValues.create(type,
        GeneralizedTimeSyntax.format(failureLockedTime)));

    if (updateEntry)
    {
      ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
      attrList.add(a);
      userEntry.putAttribute(type, attrList);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE, a, true));
    }
  }



  /**
   * Updates the user entry to remove any record of previous authentication
   * failure lockout.
   */
  private void clearFailureLockedTime()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing failure lockout time for user %s.",
          userDNString);
    }

    if (-1L == getFailureLockedTime())
    {
      return;
    }

    failureLockedTime = -1L;

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
                                  OP_ATTR_PWPOLICY_LOCKED_TIME);
    }

    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE,
                                         Attributes.empty(type), true));
    }
  }



  /**
   * Indicates whether the associated user should be considered locked out as a
   * result of too many authentication failures. In the case of an expired
   * lock-out, this routine produces the update to clear the lock-out attribute
   * and the authentication failure timestamps.
   * In case the failure lockout time is absent from the entry, but sufficient
   * authentication failure timestamps are present in the entry, this routine
   * produces the update to set the lock-out attribute.
   *
   * @return  <CODE>true</CODE> if the user is currently locked out due to too
   *          many authentication failures, or <CODE>false</CODE> if not.
   */
  public boolean lockedDueToFailures()
  {
    // FIXME: Introduce a state field to cache the computed value of this
    // method. Note that only a cached "locked" status can be returned due to
    // the possibility of intervening updates to this.failureLockedTime by
    // updateAuthFailureTimes.

    // Check if the feature is enabled in the policy.
    final int maxFailures = passwordPolicy.getLockoutFailureCount();
    if (maxFailures <= 0)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false for user %s because lockout due " +
            "to failures is not enabled.", userDNString);
      }

      return false;
    }

    // Get the locked time from the user's entry. If it is present and not
    // expired, the account is locked. If it is absent, the failure timestamps
    // must be checked, since failure timestamps sufficient to lock the
    // account could be produced across the synchronization topology within the
    // synchronization latency. Also, note that IETF
    // draft-behera-ldap-password-policy-09 specifies "19700101000000Z" as
    // the value to be set under a "locked until reset" regime; however, this
    // implementation accepts the value as a locked entry, but observes the
    // lockout expiration policy for all values including this one.
    // FIXME: This "getter" is unusual in that it might produce an update to the
    // entry in two cases. Does it make sense to factor the methods so that,
    // e.g., an expired lockout is reported, and clearing the lockout is left to
    // the caller?
    if (getFailureLockedTime() < 0L)
    {
      // There was no locked time present in the entry; however, sufficient
      // failure times might have accumulated to trigger a lockout.
      if (getAuthFailureTimes().size() < maxFailures)
      {
        if (debugEnabled())
        {
          TRACER.debugInfo("Returning false for user %s because there is " +
              "no locked time.", userDNString);
        }

        return false;
      }

      // The account isn't locked but should be, so do so now.
      setFailureLockedTime(currentTime);// FIXME: set to max(failureTimes)?

      if (debugEnabled())
      {
        TRACER.debugInfo("Locking user %s because there were enough " +
            "existing failures even though there was no account locked time.",
            userDNString);
      }
      // Fall through...
    }

    // There is a failure locked time, but it may be expired.
    if (passwordPolicy.getLockoutDuration() > 0)
    {
      final long unlockTime = getFailureLockedTime() +
           (1000L * passwordPolicy.getLockoutDuration());
      if (unlockTime > currentTime)
      {
        secondsUntilUnlock = (int) ((unlockTime - currentTime) / 1000);

        if (debugEnabled())
        {
          TRACER.debugInfo("Returning true for user %s because there is a " +
              "locked time and the lockout duration has not been reached.",
              userDNString);
        }

        return true;
      }

      // The lockout in the entry has expired...
      clearFailureLockout();

      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false for user %s " +
            "because the existing lockout has expired.", userDNString);
      }

      assert -1L == getFailureLockedTime();
      return false;
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Returning true for user %s " +
          "because there is a locked time and no lockout duration.",
          userDNString);
    }

    assert -1L <= getFailureLockedTime();
    return true;
  }



  /**
   * Retrieves the length of time in seconds until the user's account is
   * automatically unlocked.  This should only be called after calling
   * <CODE>lockedDueToFailures</CODE>.
   *
   * @return  The length of time in seconds until the user's account is
   *          automatically unlocked, or -1 if the account is not locked or the
   *          lockout requires administrative action to clear.
   */
  public int getSecondsUntilUnlock()
  {
    // secondsUntilUnlock is only set when failureLockedTime is present and
    // PasswordPolicy.getLockoutDuration is enabled; hence it is not
    // unreasonable to find secondsUntilUnlock uninitialized.
    assert failureLockedTime != Long.MIN_VALUE;

    return (secondsUntilUnlock < 0) ? -1 : secondsUntilUnlock;
  }



  /**
   * Updates the user account to remove any record of a previous lockout due to
   * failed authentications.
   */
  public void clearFailureLockout()
  {
    clearAuthFailureTimes();
    clearFailureLockedTime();
  }



  /**
   * Retrieves the time that the user last authenticated to the Directory
   * Server.
   *
   * @return  The time that the user last authenticated to the Directory Server,
   *          or -1 if it cannot be determined.
   */
  public long getLastLoginTime()
  {
    if (lastLoginTime != Long.MIN_VALUE)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored last login time of %d for " +
            "user %s.", lastLoginTime, userDNString);
      }

      return lastLoginTime;
    }

    // The policy configuration must be checked since the entry cannot be
    // evaluated without both an attribute name and timestamp format.
    AttributeType type   = passwordPolicy.getLastLoginTimeAttribute();
    String        format = passwordPolicy.getLastLoginTimeFormat();

    if ((type == null) || (format == null))
    {
      lastLoginTime = -1;
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning -1 for user %s because no last login " +
            "time will be maintained.", userDNString);
      }

      return lastLoginTime;
    }

    lastLoginTime = -1;
    List<Attribute> attrList = userEntry.getAttribute(type);

    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;

        String valueString = a.iterator().next().getValue().toString();

        try
        {
          SimpleDateFormat dateFormat = new SimpleDateFormat(format);
          lastLoginTime = dateFormat.parse(valueString).getTime();

          if (debugEnabled())
          {
            TRACER.debugInfo("Returning last login time of %d for user %s" +
                "decoded using current last login time format.",
                lastLoginTime, userDNString);
          }

          return lastLoginTime;
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          // This could mean that the last login time was encoded using a
          // previous format.
          for (String f : passwordPolicy.getPreviousLastLoginTimeFormats())
          {
            try
            {
              SimpleDateFormat dateFormat = new SimpleDateFormat(f);
              lastLoginTime = dateFormat.parse(valueString).getTime();

              if (debugEnabled())
              {
                TRACER.debugInfo("Returning last login time of %d for " +
                    "user %s decoded using previous last login time format " +
                    "of %s.", lastLoginTime, userDNString, f);
              }

              return lastLoginTime;
            }
            catch (Exception e2)
            {
              if (debugEnabled())
              {
                TRACER.debugCaught(DebugLogLevel.ERROR, e);
              }
            }
          }

          assert lastLoginTime == -1;
          if (debugEnabled())
          {
              TRACER.debugWarning("Returning -1 for user %s because the " +
                  "last login time value %s could not be parsed using any " +
                  "known format.", userDNString, valueString);
          }

          return lastLoginTime;
        }
      }
    }

    assert lastLoginTime == -1;
    if (debugEnabled())
    {
      TRACER.debugInfo("Returning %d for user %s because no last " +
          "login time value exists.", lastLoginTime, userDNString);
    }

    return lastLoginTime;
  }



  /**
   * Updates the user entry to set the current time as the last login time.
   */
  public void setLastLoginTime()
  {
    setLastLoginTime(currentTime);
  }



  /**
   * Updates the user entry to use the specified last login time.  This should
   * be used primarily for testing purposes, as the variant that uses the
   * current time should be used most of the time.
   *
   * @param  lastLoginTime  The last login time to set in the user entry.
   */
  public void setLastLoginTime(long lastLoginTime)
  {
    AttributeType type = passwordPolicy.getLastLoginTimeAttribute();
    String format = passwordPolicy.getLastLoginTimeFormat();

    if ((type == null) || (format == null))
    {
      return;
    }

    String timestamp;
    try
    {
      SimpleDateFormat dateFormat = new SimpleDateFormat(format);
      timestamp = dateFormat.format(new Date(lastLoginTime));
      this.lastLoginTime = dateFormat.parse(timestamp).getTime();
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      if (debugEnabled())
      {
        TRACER.debugWarning("Unable to set last login time for user %s " +
            "because an error occurred: %s",
                     userDNString, stackTraceToSingleLineString(e));
      }

      return;
    }


    String existingTimestamp = getValue(type);
    if ((existingTimestamp != null) && timestamp.equals(existingTimestamp))
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Not updating last login time for user %s " +
            "because the new value matches the existing value.",
            userDNString);
      }

      return;
    }


    Attribute a = Attributes.create(type, timestamp);
    if (updateEntry)
    {
      ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
      attrList.add(a);
      userEntry.putAttribute(type, attrList);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE, a, true));
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Updated the last login time for user %s to %s",
          userDNString, timestamp);
    }
  }



  /**
   * Clears the last login time from the user's entry.  This should generally be
   * used only for testing purposes.
   */
  public void clearLastLoginTime()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing last login time for user %s", userDNString);
    }

    lastLoginTime = -1;

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_LAST_LOGIN_TIME, true);

    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE,
                                         Attributes.empty(type), true));
    }
  }



  /**
   * Indicates whether the user's account is currently locked because it has
   * been idle for too long.
   *
   * @return  <CODE>true</CODE> if the user's account is locked because it has
   *          been idle for too long, or <CODE>false</CODE> if not.
   */
  public boolean lockedDueToIdleInterval()
  {
    if (isIdleLocked != ConditionResult.UNDEFINED)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored result of %b for user %s",
            (isIdleLocked == ConditionResult.TRUE), userDNString);
      }

      return isIdleLocked == ConditionResult.TRUE;
    }

    // Return immediately if this feature is disabled, since the feature is not
    // responsible for any state attribute in the entry.
    if (passwordPolicy.getIdleLockoutInterval() <= 0)
    {
      isIdleLocked = ConditionResult.FALSE;

      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false for user %s because no idle " +
            "lockout interval is defined.", userDNString);
      }
      return false;
    }

    long lockTime = currentTime -
                         (1000L * passwordPolicy.getIdleLockoutInterval());
    if(lockTime < 0) lockTime = 0;

    long lastLoginTime = getLastLoginTime();
    if (lastLoginTime > lockTime || passwordChangedTime > lockTime)
    {
      isIdleLocked = ConditionResult.FALSE;
      if (debugEnabled())
      {
        StringBuilder reason = new StringBuilder();
        if(lastLoginTime > lockTime)
        {
          reason.append("the last login time is in an acceptable window");
        }
        else
        {
          if(lastLoginTime < 0)
          {
            reason.append("there is no last login time, but ");
          }
          reason.append(
              "the password changed time is in an acceptable window");
        }
        TRACER.debugInfo("Returning false for user %s because %s.",
            userDNString, reason.toString());
      }
    }
    else
    {
      isIdleLocked = ConditionResult.TRUE;
      if (debugEnabled())
      {
        String reason = (lastLoginTime < 0)
            ? "there is no last login time and the password " +
            "changed time is not in an acceptable window"
            : "neither last login time nor password " +
            "changed time are in an acceptable window";
        TRACER.debugInfo("Returning true for user %s because %s.",
            userDNString, reason);
      }
    }

    return isIdleLocked == ConditionResult.TRUE;
  }



/**
* Indicates whether the user's password must be changed before any other
* operation can be performed.
*
* @return  <CODE>true</CODE> if the user's password must be changed before
*          any other operation can be performed.
*/
  public boolean mustChangePassword()
  {
    if(mustChangePassword != ConditionResult.UNDEFINED)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored result of %b for user %s.",
            (mustChangePassword == ConditionResult.TRUE), userDNString);
      }

      return mustChangePassword == ConditionResult.TRUE;
    }

    // If the password policy doesn't use force change on add or force change on
    // reset, or if it forbids the user from changing his password, then return
    // false.
    // FIXME: the only getter responsible for a state attribute (pwdReset) that
    // considers the policy before checking the entry for the presence of the
    // attribute.
    if (! (passwordPolicy.allowUserPasswordChanges()
           && (passwordPolicy.forceChangeOnAdd()
               || passwordPolicy.forceChangeOnReset())))
    {
      mustChangePassword = ConditionResult.FALSE;
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false for user %s because neither " +
            "force change on add nor force change on reset is enabled, " +
            "or users are not allowed to self-modify passwords.",
            userDNString);

      }

      return false;
    }

    AttributeType type =
           DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
           OP_ATTR_PWPOLICY_RESET_REQUIRED);
    }

    try
    {
      mustChangePassword = getBoolean(type);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);

        TRACER.debugWarning("Returning true for user %s because an error " +
            "occurred: %s", userDNString, stackTraceToSingleLineString(e));
      }

      mustChangePassword = ConditionResult.TRUE;

      return true;
    }

    if(mustChangePassword == ConditionResult.UNDEFINED)
    {
      mustChangePassword = ConditionResult.FALSE;
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning %b for user since the attribute \"%s\"" +
            " is not present in the entry.",
            false, userDNString, OP_ATTR_PWPOLICY_RESET_REQUIRED);
      }

      return false;
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Returning %b for user %s.",
          (mustChangePassword == ConditionResult.TRUE), userDNString);
    }

    return mustChangePassword == ConditionResult.TRUE;
  }



/**
* Updates the user entry to indicate whether the user's password must be
* changed.
*
* @param  mustChangePassword  Indicates whether the user's password must be
*                             changed.
*/
  public void setMustChangePassword(boolean mustChangePassword)
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Updating user %s to set the reset flag to %b",
          userDNString, mustChangePassword);
    }

    if (mustChangePassword == mustChangePassword())
    {
      return;  // requested state matches current state
    }

    this.mustChangePassword =
            ConditionResult.inverseOf(this.mustChangePassword);

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
                                  OP_ATTR_PWPOLICY_RESET_REQUIRED);
    }

    if (mustChangePassword)
    {
      Attribute a = Attributes.create(type, String.valueOf(true));
      if (updateEntry)
      {
        ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        userEntry.putAttribute(type, attrList);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE, a, true));
      }
    }
    else
    {
      // erase
      if (updateEntry)
      {
        userEntry.removeAttribute(type);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE,
                                           Attributes.empty(type), true));
      }
    }
  }



  /**
   * Indicates whether the user's account is locked because the password has
   * been reset by an administrator but the user did not change the password in
   * a timely manner.
   *
   * @return  <CODE>true</CODE> if the user's account is locked because of the
   *          maximum reset age, or <CODE>false</CODE> if not.
   */
  public boolean lockedDueToMaximumResetAge()
  {
    // This feature is reponsible for neither a state field nor an entry state
    // attribute.
    if (passwordPolicy.getMaximumPasswordResetAge() <= 0)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false for user %s because there is no " +
            "maximum reset age.", userDNString);
      }

      return false;
    }

    if (! mustChangePassword())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false for user %s because the user's " +
            "password has not been reset.", userDNString);
      }

      return false;
    }

    long maxResetTime = passwordChangedTime +
        (1000L * passwordPolicy.getMaximumPasswordResetAge());
    boolean locked = (maxResetTime < currentTime);

    if (debugEnabled())
    {
      TRACER.debugInfo("Returning %b for user %s after comparing the " +
          "current and max reset times.", locked, userDNString);
    }

    return locked;
  }



  /**
   * Retrieves the time that the user's password should expire (if the
   * expiration is in the future) or did expire (if the expiration was in the
   * past).  Note that this method should be called after the
   * <CODE>lockedDueToMaximumResetAge</CODE> method because grace logins will
   * not be allowed in the case that the maximum reset age has passed whereas
   * they may be used for expiration due to maximum password age or forced
   * change time.
   *
   * @return  The time that the user's password should/did expire, or -1 if it
   *          should not expire.
   */
  public long getPasswordExpirationTime()
  {
    if (passwordExpirationTime == Long.MIN_VALUE)
    {
      passwordExpirationTime = Long.MAX_VALUE;

      boolean checkWarning = false;

      int maxAge = passwordPolicy.getMaximumPasswordAge();
      if (maxAge > 0)
      {
        long expTime = passwordChangedTime + (1000L*maxAge);
        if (expTime < passwordExpirationTime)
        {
          passwordExpirationTime = expTime;
          checkWarning   = true;
        }
      }

      int maxResetAge = passwordPolicy.getMaximumPasswordResetAge();
      if (mustChangePassword() && (maxResetAge > 0))
      {
        long expTime = passwordChangedTime + (1000L*maxResetAge);
        if (expTime < passwordExpirationTime)
        {
          passwordExpirationTime = expTime;
          checkWarning   = false;
        }
      }

      long mustChangeTime = passwordPolicy.getRequireChangeByTime();
      if (mustChangeTime > 0)
      {
        long reqChangeTime = getRequiredChangeTime();
        if ((reqChangeTime != mustChangeTime) &&
            (mustChangeTime < passwordExpirationTime))
        {
          passwordExpirationTime = mustChangeTime;
          checkWarning   = true;
        }
      }

      if (passwordExpirationTime == Long.MAX_VALUE)
      {
        passwordExpirationTime = -1;
        shouldWarn             = ConditionResult.FALSE;
        isFirstWarning         = ConditionResult.FALSE;
        isPasswordExpired      = ConditionResult.FALSE;
        mayUseGraceLogin       = ConditionResult.TRUE;
      }
      else if (checkWarning)
      {
        mayUseGraceLogin = ConditionResult.TRUE;

        int warningInterval = passwordPolicy.getWarningInterval();
        if (warningInterval > 0)
        {
          long shouldWarnTime =
                    passwordExpirationTime - (warningInterval*1000L);
          if (shouldWarnTime > currentTime)
          {
            // The warning time is in the future, so we know the password isn't
            // expired.
            shouldWarn        = ConditionResult.FALSE;
            isFirstWarning    = ConditionResult.FALSE;
            isPasswordExpired = ConditionResult.FALSE;
          }
          else
          {
            // We're at least in the warning period, but the password may be
            // expired.
            long warnedTime = getWarnedTime();

            if (passwordExpirationTime > currentTime)
            {
              // The password is not expired but we should warn the user.
              shouldWarn        = ConditionResult.TRUE;
              isPasswordExpired = ConditionResult.FALSE;

              if (warnedTime < 0)
              {
                isFirstWarning = ConditionResult.TRUE;
                setWarnedTime();

                if (! passwordPolicy.expirePasswordsWithoutWarning())
                {
                  passwordExpirationTime =
                       currentTime + (warningInterval*1000L);
                }
              }
              else
              {
                isFirstWarning = ConditionResult.FALSE;

                if (! passwordPolicy.expirePasswordsWithoutWarning())
                {
                  passwordExpirationTime = warnedTime + (warningInterval*1000L);
                }
              }
            }
            else
            {
              // The expiration time has passed, but we may not actually be
              // expired if the user has not yet seen a warning.
              if (passwordPolicy.expirePasswordsWithoutWarning())
              {
                shouldWarn        = ConditionResult.FALSE;
                isFirstWarning    = ConditionResult.FALSE;
                isPasswordExpired = ConditionResult.TRUE;
              }
              else if (warnedTime > 0)
              {
                passwordExpirationTime = warnedTime + (warningInterval*1000L);
                if (passwordExpirationTime > currentTime)
                {
                  shouldWarn        = ConditionResult.TRUE;
                  isFirstWarning    = ConditionResult.FALSE;
                  isPasswordExpired = ConditionResult.FALSE;
                }
                else
                {
                  shouldWarn        = ConditionResult.FALSE;
                  isFirstWarning    = ConditionResult.FALSE;
                  isPasswordExpired = ConditionResult.TRUE;
                }
              }
              else
              {
                shouldWarn             = ConditionResult.TRUE;
                isFirstWarning         = ConditionResult.TRUE;
                isPasswordExpired      = ConditionResult.FALSE;
                passwordExpirationTime = currentTime + (warningInterval*1000L);
              }
            }
          }
        }
        else
        {
          // There will never be a warning, and the user's password may be
          // expired.
          shouldWarn     = ConditionResult.FALSE;
          isFirstWarning = ConditionResult.FALSE;

          if (currentTime > passwordExpirationTime)
          {
            isPasswordExpired = ConditionResult.TRUE;
          }
          else
          {
            isPasswordExpired = ConditionResult.FALSE;
          }
        }
      }
      else
      {
        mayUseGraceLogin = ConditionResult.FALSE;
        shouldWarn       = ConditionResult.FALSE;
        isFirstWarning   = ConditionResult.FALSE;

        if (passwordExpirationTime < currentTime)
        {
          isPasswordExpired = ConditionResult.TRUE;
        }
        else
        {
          isPasswordExpired = ConditionResult.FALSE;
        }
      }
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Returning password expiration time of %d for user " +
          "%s.", passwordExpirationTime, userDNString);
    }

    return passwordExpirationTime;
  }



  /**
   * Indicates whether the user's password is currently expired.
   *
   * @return  <CODE>true</CODE> if the user's password is currently expired, or
   *          <CODE>false</CODE> if not.
   */
  public boolean isPasswordExpired()
  {
    if ((isPasswordExpired == null) ||
        (isPasswordExpired == ConditionResult.UNDEFINED))
    {
      getPasswordExpirationTime();
    }

    return isPasswordExpired == ConditionResult.TRUE;
  }



  /**
   * Indicates whether the user's last password change was within the minimum
   * password age.
   *
   * @return  <CODE>true</CODE> if the password minimum age is nonzero, the
   *          account is not in force-change mode, and the last password change
   *          was within the minimum age, or <CODE>false</CODE> otherwise.
   */
  public boolean isWithinMinimumAge()
  {
    // This feature is reponsible for neither a state field nor entry state
    // attribute.
    int minAge = passwordPolicy.getMinimumPasswordAge();
    if (minAge <= 0)
    {
      // There is no minimum age, so the user isn't in it.
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false because there is no minimum age.");
      }

      return false;
    }
    else if ((passwordChangedTime + (minAge*1000L)) < currentTime)
    {
      // It's been long enough since the user changed their password.
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false because the minimum age has " +
            "expired.");
      }

      return false;
    }
    else if (mustChangePassword())
    {
      // The user is in a must-change mode, so the minimum age doesn't apply.
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false because the account is in a " +
            "must-change state.");
      }

      return false;
    }
    else
    {
      // The user is within the minimum age.
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning true.");
      }

      return true;
    }
  }



  /**
   * Indicates whether the user may use a grace login if the password is expired
   * and there is at least one grace login remaining.  Note that this does not
   * check to see if the user's password is expired, does not verify that there
   * are any remaining grace logins, and does not update the set of grace login
   * times.
   *
   * @return  <CODE>true</CODE> if the user may use a grace login if the
   *          password is expired and there is at least one grace login
   *          remaining, or <CODE>false</CODE> if the user may not use a grace
   *          login for some reason.
   */
  public boolean mayUseGraceLogin()
  {
    if ((mayUseGraceLogin == null) ||
        (mayUseGraceLogin == ConditionResult.UNDEFINED))
    {
      getPasswordExpirationTime();
    }

    return mayUseGraceLogin == ConditionResult.TRUE;
  }



  /**
   * Indicates whether the user should receive a warning notification that the
   * password is about to expire.
   *
   * @return  <CODE>true</CODE> if the user should receive a warning
   *          notification that the password is about to expire, or
   *          <CODE>false</CODE> if not.
   */
  public boolean shouldWarn()
  {
    if ((shouldWarn == null) || (shouldWarn == ConditionResult.UNDEFINED))
    {
      getPasswordExpirationTime();
    }

    return shouldWarn == ConditionResult.TRUE;
  }



  /**
   * Indicates whether the warning that the user should receive would be the
   * first warning for the user.
   *
   * @return  <CODE>true</CODE> if the warning that should be sent to the user
   *          would be the first warning, or <CODE>false</CODE> if not.
   */
  public boolean isFirstWarning()
  {
    if ((isFirstWarning == null) ||
        (isFirstWarning == ConditionResult.UNDEFINED))
    {
      getPasswordExpirationTime();
    }

    return isFirstWarning == ConditionResult.TRUE;
  }



  /**
   * Retrieves the length of time in seconds until the user's password expires.
   *
   * @return  The length of time in seconds until the user's password expires,
   *          0 if the password is currently expired, or -1 if the password
   *          should not expire.
   */
  public int getSecondsUntilExpiration()
  {
    long expirationTime = getPasswordExpirationTime();
    if (expirationTime < 0)
    {
      return -1;
    }
    else if (expirationTime < currentTime)
    {
      return 0;
    }
    else
    {
      return (int) ((expirationTime - currentTime) / 1000);
    }
  }



  /**
   * Retrieves the timestamp for the last required change time that the user
   * complied with.
   *
   * @return  The timestamp for the last required change time that the user
   *          complied with, or -1 if the user's password has not been changed
   *          in compliance with this configuration.
   */
  public long getRequiredChangeTime()
  {
    if (requiredChangeTime != Long.MIN_VALUE)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored required change time of %d for " +
            "user %s", requiredChangeTime, userDNString);
      }

      return requiredChangeTime;
    }

    AttributeType type = DirectoryServer.getAttributeType(
                              OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, true);

    try
    {
      requiredChangeTime = getGeneralizedTime(type);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }

      requiredChangeTime = -1;
      if (debugEnabled())
      {
        TRACER.debugWarning("Returning %d for user %s because an error " +
            "occurred: %s", requiredChangeTime, userDNString,
                     stackTraceToSingleLineString(e));
      }

      return requiredChangeTime;
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Returning required change time of %d for user %s",
          requiredChangeTime, userDNString);
    }

    return requiredChangeTime;
  }



  /**
   * Updates the user entry with a timestamp indicating that the password has
   * been changed in accordance with the require change time.
   */
  public void setRequiredChangeTime()
  {
    long requiredChangeByTimePolicy = passwordPolicy.getRequireChangeByTime();
    if (requiredChangeByTimePolicy > 0)
    {
      setRequiredChangeTime(requiredChangeByTimePolicy);
    }
  }



  /**
   * Updates the user entry with a timestamp indicating that the password has
   * been changed in accordance with the require change time.
   *
   * @param  requiredChangeTime  The timestamp to use for the required change
   *                             time value.
   */
  public void setRequiredChangeTime(long requiredChangeTime)
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Updating required change time for user %s",
          userDNString);
    }

    if (getRequiredChangeTime() != requiredChangeTime)
    {
      AttributeType type = DirectoryServer.getAttributeType(
                               OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, true);

      String timeValue = GeneralizedTimeSyntax.format(requiredChangeTime);
      Attribute a = Attributes.create(type, timeValue);

      if (updateEntry)
      {
        ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        userEntry.putAttribute(type, attrList);
      }
      else
      {
        modifications.add(new Modification(ModificationType.REPLACE, a, true));
      }
    }
  }



  /**
   * Updates the user entry to remove any timestamp indicating that the password
   * has been changed in accordance with the required change time.
   */
  public void clearRequiredChangeTime()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing required change time for user %s",
          userDNString);
    }

    AttributeType type = DirectoryServer.getAttributeType(
                             OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, true);
    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE,
                                         Attributes.empty(type), true));
    }
  }



  /**
   * Retrieves the time that the user was first warned about an upcoming
   * expiration.
   *
   * @return  The time that the user was first warned about an upcoming
   *          expiration, or -1 if the user has not been warned.
   */
  public long getWarnedTime()
  {
    if (warnedTime == Long.MIN_VALUE)
    {
      AttributeType type =
           DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
      try
      {
        warnedTime = getGeneralizedTime(type);
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        if (debugEnabled())
        {
          TRACER.debugWarning("Unable to decode the warned time for user %s: " +
              "%s", userDNString, stackTraceToSingleLineString(e));
        }

        warnedTime = -1;
      }
    }


    if (debugEnabled())
    {
      TRACER.debugInfo("Returning a warned time of %d for user %s",
          warnedTime, userDNString);
    }

    return warnedTime;
  }



  /**
   * Updates the user entry to set the warned time to the current time.
   */
  public void setWarnedTime()
  {
    setWarnedTime(currentTime);
  }



  /**
   * Updates the user entry to set the warned time to the specified time.  This
   * method should generally only be used for testing purposes, since the
   * variant that uses the current time is preferred almost everywhere else.
   *
   * @param  warnedTime  The value to use for the warned time.
   */
  public void setWarnedTime(long warnedTime)
  {
    long warnTime = getWarnedTime();
    if (warnTime == warnedTime)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Not updating warned time for user %s because " +
            "the warned time is the same as the specified time.",
            userDNString);
      }

      return;
    }

    this.warnedTime = warnedTime;

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
    Attribute a = Attributes.create(type, GeneralizedTimeSyntax
        .createGeneralizedTimeValue(currentTime));

    if (updateEntry)
    {
      ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
      attrList.add(a);
      userEntry.putAttribute(type, attrList);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE, a, true));
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Updated the warned time for user %s", userDNString);
    }
  }



  /**
   * Updates the user entry to clear the warned time.
   */
  public void clearWarnedTime()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing warned time for user %s", userDNString);
    }

    if (getWarnedTime() < 0)
    {
      return;
    }
    warnedTime = -1;

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      Attribute a = Attributes.empty(type);
      modifications.add(new Modification(ModificationType.REPLACE, a, true));
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Cleared the warned time for user %s", userDNString);
    }
  }



  /**
   * Retrieves the times that the user has authenticated to the server using a
   * grace login.
   *
   * @return  The times that the user has authenticated to the server using a
   *          grace login.
   */
  public List<Long> getGraceLoginTimes()
  {
    if (graceLoginTimes == null)
    {
      AttributeType type = DirectoryServer.getAttributeType(
                                OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC);
      if (type == null)
      {
        type = DirectoryServer.getDefaultAttributeType(
                                    OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
      }

      try
      {
        graceLoginTimes = getGeneralizedTimes(type);
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }

        if (debugEnabled())
        {
          TRACER.debugWarning("Error while processing grace login times " +
               "for user %s: %s",
                       userDNString, stackTraceToSingleLineString(e));
        }

        graceLoginTimes = new ArrayList<Long>();

        if (updateEntry)
        {
          userEntry.removeAttribute(type);
        }
        else
        {
          modifications.add(new Modification(ModificationType.REPLACE,
              Attributes.empty(type), true));
        }
      }
    }


    if (debugEnabled())
    {
      TRACER.debugInfo("Returning grace login times for user %s",
          userDNString);
    }

    return graceLoginTimes;
  }



  /**
   * Retrieves the number of grace logins that the user has left.
   *
   * @return  The number of grace logins that the user has left, or -1 if grace
   *          logins are not allowed.
   */
  public int getGraceLoginsRemaining()
  {
    int maxGraceLogins = passwordPolicy.getGraceLoginCount();
    if (maxGraceLogins <= 0)
    {
      return -1;
    }

    List<Long> graceLoginTimes = getGraceLoginTimes();
    return maxGraceLogins - graceLoginTimes.size();
  }



  /**
   * Updates the set of grace login times for the user to include the current
   * time.
   */
  public void updateGraceLoginTimes()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Updating grace login times for user %s",
          userDNString);
    }

    List<Long> graceTimes = getGraceLoginTimes();
    long highestGraceTime = -1;
    for (Long l : graceTimes)
    {
      highestGraceTime = Math.max(l, highestGraceTime);
    }

    if (highestGraceTime >= currentTime)
    {
      highestGraceTime++;
    }
    else
    {
      highestGraceTime = currentTime;
    }
    graceTimes.add(highestGraceTime); // graceTimes == this.graceLoginTimes

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
                                  OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
    }

    if (updateEntry)
    {
      AttributeBuilder builder = new AttributeBuilder(type);
      for (Long l : graceTimes)
      {
        builder.add(AttributeValues.create(type, GeneralizedTimeSyntax
            .format(l)));
      }

      ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
      attrList.add(builder.toAttribute());

      userEntry.putAttribute(type, attrList);
    }
    else
    {
      Attribute addAttr = Attributes.create(type, AttributeValues.create(
          type, GeneralizedTimeSyntax.format(highestGraceTime)));

      modifications.add(new Modification(ModificationType.ADD, addAttr, true));
    }
  }



  /**
   * Specifies the set of grace login use times for the associated user.  If
   * the provided list is empty or {@code null}, then the set will be cleared.
   *
   * @param  graceLoginTimes  The grace login use times for the associated user.
   */
  public void setGraceLoginTimes(List<Long> graceLoginTimes)
  {
    if ((graceLoginTimes == null) || graceLoginTimes.isEmpty())
    {
      clearGraceLoginTimes();
      return;
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Updating grace login times for user %s",
          userDNString);
    }

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC,
                                          true);
    AttributeBuilder builder = new AttributeBuilder(type);
    for (Long l : graceLoginTimes)
    {
      builder
          .add(AttributeValues.create(type, GeneralizedTimeSyntax.format(l)));
    }
    Attribute a = builder.toAttribute();

    if (updateEntry)
    {
      ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
      attrList.add(a);

      userEntry.putAttribute(type, attrList);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE, a, true));
    }
  }



  /**
   * Updates the user entry to remove any record of previous grace logins.
   */
  public void clearGraceLoginTimes()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing grace login times for user %s",
          userDNString);
    }

    List<Long> graceTimes = getGraceLoginTimes();
    if (graceTimes.isEmpty())
    {
      return;
    }
    graceTimes.clear(); // graceTimes == this.graceLoginTimes

    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC);
    if (type == null)
    {
      type = DirectoryServer.getDefaultAttributeType(
                                  OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
    }

    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE,
                                         Attributes.empty(type), true));
    }
  }



  /**
   * Retrieves a list of the clear-text passwords for the user.  If the user
   * does not have any passwords in the clear, then the list will be empty.
   *
   * @return  A list of the clear-text passwords for the user.
   */
  public List<ByteString> getClearPasswords()
  {
    LinkedList<ByteString> clearPasswords = new LinkedList<ByteString>();

    List<Attribute> attrList =
         userEntry.getAttribute(passwordPolicy.getPasswordAttribute());

    if (attrList == null)
    {
      return clearPasswords;
    }

    for (Attribute a : attrList)
    {
      boolean usesAuthPasswordSyntax = passwordPolicy.usesAuthPasswordSyntax();

      for (AttributeValue v : a)
      {
        try
        {
          StringBuilder[] pwComponents;
          if (usesAuthPasswordSyntax)
          {
            pwComponents =
                 AuthPasswordSyntax.decodeAuthPassword(v.getValue().toString());
          }
          else
          {
            String[] userPwComponents =
                 UserPasswordSyntax.decodeUserPassword(v.getValue().toString());
            pwComponents = new StringBuilder[userPwComponents.length];
            for (int i = 0; i < userPwComponents.length; ++i)
            {
              pwComponents[i] = new StringBuilder(userPwComponents[i]);
            }
          }

          String schemeName = pwComponents[0].toString();
          PasswordStorageScheme<?> scheme = (usesAuthPasswordSyntax)
                    ? DirectoryServer.getAuthPasswordStorageScheme(schemeName)
                    : DirectoryServer.getPasswordStorageScheme(schemeName);
          if (scheme == null)
          {
            if (debugEnabled())
            {
              TRACER.debugWarning("User entry %s contains a password with " +
                  "scheme %s that is not defined in the server.",
                                  userDNString, schemeName);
            }

            continue;
          }

          if (scheme.isReversible())
          {
            ByteString clearValue = (usesAuthPasswordSyntax)
                         ? scheme.getAuthPasswordPlaintextValue(
                               pwComponents[1].toString(),
                               pwComponents[2].toString())
                         : scheme.getPlaintextValue(
                ByteString.valueOf(pwComponents[1].toString()));
            clearPasswords.add(clearValue);
          }
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          if (debugEnabled())
          {
            TRACER.debugWarning("Cannot get clear password value foruser %s: " +
                "%s", userDNString, e);
          }
        }
      }
    }

    return clearPasswords;
  }



  /**
   * Indicates whether the provided password value matches any of the stored
   * passwords in the user entry.
   *
   * @param  password  The user-provided password to verify.
   *
   * @return  <CODE>true</CODE> if the provided password matches any of the
   *          stored password values, or <CODE>false</CODE> if not.
   */
  public boolean passwordMatches(ByteString password)
  {
    List<Attribute> attrList =
         userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
    if ((attrList == null) || attrList.isEmpty())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false because user %s does not have " +
            "any values for password attribute %s", userDNString,
            passwordPolicy.getPasswordAttribute().getNameOrOID());
      }

      return false;
    }

    for (Attribute a : attrList)
    {
      boolean usesAuthPasswordSyntax = passwordPolicy.usesAuthPasswordSyntax();

      for (AttributeValue v : a)
      {
        try
        {
          StringBuilder[] pwComponents;
          if (usesAuthPasswordSyntax)
          {
            pwComponents =
                 AuthPasswordSyntax.decodeAuthPassword(v.getValue().toString());
          }
          else
          {
            String[] userPwComponents =
                 UserPasswordSyntax.decodeUserPassword(v.getValue().toString());
            pwComponents = new StringBuilder[userPwComponents.length];
            for (int i = 0; i < userPwComponents.length; ++i)
            {
              pwComponents[i] = new StringBuilder(userPwComponents[i]);
            }
          }

          String schemeName = pwComponents[0].toString();
          PasswordStorageScheme<?> scheme = (usesAuthPasswordSyntax)
                     ? DirectoryServer.getAuthPasswordStorageScheme(schemeName)
                     : DirectoryServer.getPasswordStorageScheme(schemeName);
          if (scheme == null)
          {
            if (debugEnabled())
            {
              TRACER.debugWarning("User entry %s contains a password with " +
                  "scheme %s that is not defined in the server.",
                                  userDNString, schemeName);
            }

            continue;
          }

          boolean passwordMatches = (usesAuthPasswordSyntax)
                     ? scheme.authPasswordMatches(password,
                                                  pwComponents[1].toString(),
                                                  pwComponents[2].toString())
                     : scheme.passwordMatches(password,
              ByteString.valueOf(pwComponents[1].toString()));
          if (passwordMatches)
          {
            if (debugEnabled())
            {
              TRACER.debugInfo("Returning true for user %s because the " +
                  "provided password matches a value encoded with scheme %s",
                  userDNString, schemeName);
            }

            return true;
          }
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          if (debugEnabled())
          {
            TRACER.debugWarning("An error occurred while attempting to " +
                "process a password value for user %s: %s",
                     userDNString, stackTraceToSingleLineString(e));
          }
        }
      }
    }

    // If we've gotten here, then we couldn't find a match.
    if (debugEnabled())
    {
      TRACER.debugInfo("Returning false because the provided password does " +
          "not match any of the stored password values for user %s",
          userDNString);
    }

    return false;
  }



  /**
   * Indicates whether the provided password value is pre-encoded.
   *
   * @param  passwordValue  The value for which to make the determination.
   *
   * @return  <CODE>true</CODE> if the provided password value is pre-encoded,
   *          or <CODE>false</CODE> if it is not.
   */
  public boolean passwordIsPreEncoded(ByteString passwordValue)
  {
    if (passwordPolicy.usesAuthPasswordSyntax())
    {
      return AuthPasswordSyntax.isEncoded(passwordValue);
    }
    else
    {
      return UserPasswordSyntax.isEncoded(passwordValue);
    }
  }



  /**
   * Encodes the provided password using the default storage schemes (using the
   * appropriate syntax for the password attribute).
   *
   * @param  password  The password to be encoded.
   *
   * @return  The password encoded using the default schemes.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to encode
   *                              the password.
   */
  public List<ByteString> encodePassword(ByteString password)
         throws DirectoryException
  {
    List<PasswordStorageScheme<?>> schemes =
         passwordPolicy.getDefaultStorageSchemes();
    List<ByteString> encodedPasswords =
         new ArrayList<ByteString>(schemes.size());

    if (passwordPolicy.usesAuthPasswordSyntax())
    {
      for (PasswordStorageScheme<?> s : schemes)
      {
        encodedPasswords.add(s.encodeAuthPassword(password));
      }
    }
    else
    {
      for (PasswordStorageScheme<?> s : schemes)
      {
        encodedPasswords.add(s.encodePasswordWithScheme(password));
      }
    }

    return encodedPasswords;
  }



  /**
   * Indicates whether the provided password appears to be acceptable according
   * to the password validators.
   *
   * @param  operation         The operation that provided the password.
   * @param  userEntry         The user entry in which the password is used.
   * @param  newPassword       The password to be validated.
   * @param  currentPasswords  The set of clear-text current passwords for the
   *                           user (this may be a subset if not all of them are
   *                           available in the clear, or empty if none of them
   *                           are available in the clear).
   * @param  invalidReason     A buffer that may be used to hold the invalid
   *                           reason if the password is rejected.
   *
   * @return  <CODE>true</CODE> if the password is acceptable for use, or
   *          <CODE>false</CODE> if it is not.
   */
  public boolean passwordIsAcceptable(Operation operation, Entry userEntry,
                                      ByteString newPassword,
                                      Set<ByteString> currentPasswords,
                                      MessageBuilder invalidReason)
  {
    for (DN validatorDN : passwordPolicy.getPasswordValidators().keySet())
    {
      PasswordValidator<? extends PasswordValidatorCfg> validator =
           passwordPolicy.getPasswordValidators().get(validatorDN);

      if (! validator.passwordIsAcceptable(newPassword, currentPasswords,
                                           operation, userEntry, invalidReason))
      {
        if (debugEnabled())
        {
          TRACER.debugInfo("The password provided for user %s failed " +
              "the %s password validator.",
              userDNString, validatorDN.toString());
        }

        return false;
      }
      else
      {
        if (debugEnabled())
        {
          TRACER.debugInfo("The password provided for user %s passed " +
              "the %s password validator.",
              userDNString, validatorDN.toString());
        }
      }
    }

    return true;
  }



  /**
   * Performs any processing that may be necessary to remove deprecated storage
   * schemes from the user's entry that match the provided password and
   * re-encodes them using the default schemes.
   *
   * @param  password  The clear-text password provided by the user.
   */
  public void handleDeprecatedStorageSchemes(ByteString password)
  {
    if (passwordPolicy.getDefaultStorageSchemes().isEmpty())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Doing nothing for user %s because no " +
            "deprecated storage schemes have been defined.", userDNString);
      }

      return;
    }


    AttributeType type = passwordPolicy.getPasswordAttribute();
    List<Attribute> attrList = userEntry.getAttribute(type);
    if ((attrList == null) || attrList.isEmpty())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Doing nothing for entry %s because no password " +
            "values were found.", userDNString);
      }

      return;
    }


    HashSet<String> existingDefaultSchemes = new HashSet<String>();
    LinkedHashSet<AttributeValue> removedValues =
         new LinkedHashSet<AttributeValue>();
    LinkedHashSet<AttributeValue> updatedValues =
         new LinkedHashSet<AttributeValue>();

    boolean usesAuthPasswordSyntax = passwordPolicy.usesAuthPasswordSyntax();

    for (Attribute a : attrList)
    {
      Iterator<AttributeValue> iterator = a.iterator();
      while (iterator.hasNext())
      {
        AttributeValue v = iterator.next();

        try
        {
          StringBuilder[] pwComponents;
          if (usesAuthPasswordSyntax)
          {
            pwComponents =
                 AuthPasswordSyntax.decodeAuthPassword(v.getValue().toString());
          }
          else
          {
            String[] userPwComponents =
                 UserPasswordSyntax.decodeUserPassword(v.getValue().toString());
            pwComponents = new StringBuilder[userPwComponents.length];
            for (int i = 0; i < userPwComponents.length; ++i)
            {
              pwComponents[i] = new StringBuilder(userPwComponents[i]);
            }
          }

          String schemeName = pwComponents[0].toString();
          PasswordStorageScheme<?> scheme = (usesAuthPasswordSyntax)
                    ? DirectoryServer.getAuthPasswordStorageScheme(schemeName)
                    : DirectoryServer.getPasswordStorageScheme(schemeName);
          if (scheme == null)
          {
            if (debugEnabled())
            {
              TRACER.debugWarning("Skipping password value for user %s " +
                  "because the associated storage scheme %s is not " +
                  "configured for use.", userDNString, schemeName);
            }

            continue;
          }

          boolean passwordMatches = (usesAuthPasswordSyntax)
                     ? scheme.authPasswordMatches(password,
                                                  pwComponents[1].toString(),
                                                  pwComponents[2].toString())
                     : scheme.passwordMatches(password,
              ByteString.valueOf(pwComponents[1].toString()));
          if (passwordMatches)
          {
            if (passwordPolicy.isDefaultStorageScheme(schemeName))
            {
              existingDefaultSchemes.add(schemeName);
              updatedValues.add(v);
            }
            else if (passwordPolicy.isDeprecatedStorageScheme(schemeName))
            {
              if (debugEnabled())
              {
                TRACER.debugInfo("Marking password with scheme %s for " +
                    "removal from user entry %s.", schemeName, userDNString);
              }

              iterator.remove();
              removedValues.add(v);
            }
            else
            {
              updatedValues.add(v);
            }
          }
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);

            TRACER.debugWarning("Skipping password value for user %s because " +
                "an error occurred while attempting to decode it based on " +
                "the user password syntax: %s",
                userDNString, stackTraceToSingleLineString(e));
          }
        }
      }
    }

    if (removedValues.isEmpty())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("User entry %s does not have any password values " +
            "encoded using deprecated schemes.", userDNString);
      }

      return;
    }

    LinkedHashSet<AttributeValue> addedValues = new
         LinkedHashSet<AttributeValue>();
    for (PasswordStorageScheme<?> s :
         passwordPolicy.getDefaultStorageSchemes())
    {
      if (! existingDefaultSchemes.contains(
           toLowerCase(s.getStorageSchemeName())))
      {
        try
        {
          ByteString encodedPassword = (usesAuthPasswordSyntax)
                                       ? s.encodeAuthPassword(password)
                                       : s.encodePasswordWithScheme(password);
          AttributeValue v =
              AttributeValues.create(type, encodedPassword);
          addedValues.add(v);
          updatedValues.add(v);
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }

          if (debugEnabled())
          {
            TRACER.debugWarning("Unable to encode password for user %s using " +
                 "default scheme %s: %s",
                         userDNString, s.getStorageSchemeName(),
                         stackTraceToSingleLineString(e));
          }
        }
      }
    }

    if (updatedValues.isEmpty())
    {
      if (debugEnabled())
      {
        TRACER.debugWarning("Not updating user entry %s because removing " +
             "deprecated schemes would leave the user without a password.",
                     userDNString);
      }

      return;
    }

    if (updateEntry)
    {
      AttributeBuilder builder = new AttributeBuilder(type);
      builder.addAll(updatedValues);
      ArrayList<Attribute> newList = new ArrayList<Attribute>(1);
      newList.add(builder.toAttribute());
      userEntry.putAttribute(type, newList);
    }
    else
    {
      AttributeBuilder builder = new AttributeBuilder(type);
      builder.addAll(removedValues);
      Attribute a = builder.toAttribute();
      modifications.add(new Modification(ModificationType.DELETE, a, true));

      if (! addedValues.isEmpty())
      {
        builder = new AttributeBuilder(type);
        builder.addAll(addedValues);
        Attribute a2 = builder.toAttribute();
        modifications.add(new Modification(ModificationType.ADD, a2, true));
      }
    }

    if (debugEnabled())
    {
      TRACER.debugInfo("Updating user entry %s to replace password values " +
          "encoded with deprecated schemes with values encoded " +
          "with the default schemes.", userDNString);
    }
  }



  /**
   * Indicates whether password history information should be matained for this
   * user.
   *
   * @return  {@code true} if password history information should be maintained
   *          for this user, or {@code false} if not.
   */
  public boolean maintainHistory()
  {
    return ((passwordPolicy.getPasswordHistoryCount() > 0) ||
            (passwordPolicy.getPasswordHistoryDuration() > 0));
  }



  /**
   * Indicates whether the provided password is equal to any of the current
   * passwords, or any of the passwords in the history.
   *
   * @param  password  The password for which to make the determination.
   *
   * @return  {@code true} if the provided password is equal to any of the
   *          current passwords or any of the passwords in the history, or
   *          {@code false} if not.
   */
  public boolean isPasswordInHistory(ByteString password)
  {
    if (! maintainHistory())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning false because password history " +
            "checking is disabled.");
      }

      // Password history checking is disabled, so we don't care if it is in the
      // list or not.
      return false;
    }


    // Check to see if the provided password is equal to any of the current
    // passwords.  If so, then we'll consider it to be in the history.
    if (passwordMatches(password))
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning true because the provided password " +
            "is currently in use.");
      }

      return true;
    }


    // Get the attribute containing the history and check to see if any of the
    // values is equal to the provided password.  However, first prune the list
    // by size and duration if necessary.
    TreeMap<Long,AttributeValue> historyMap = getSortedHistoryValues(null);

    int historyCount = passwordPolicy.getPasswordHistoryCount();
    if ((historyCount > 0) && (historyMap.size() > historyCount))
    {
      int numToDelete = historyMap.size() - historyCount;
      Iterator<Long> iterator = historyMap.keySet().iterator();
      while ((iterator.hasNext()) && (numToDelete > 0))
      {
        iterator.next();
        iterator.remove();
        numToDelete--;
      }
    }

    int historyDuration = passwordPolicy.getPasswordHistoryDuration();
    if (historyDuration > 0)
    {
      long retainDate = currentTime - (1000 * historyDuration);
      Iterator<Long> iterator = historyMap.keySet().iterator();
      while (iterator.hasNext())
      {
        long historyDate = iterator.next();
        if (historyDate < retainDate)
        {
          iterator.remove();
        }
        else
        {
          break;
        }
      }
    }

    for (AttributeValue v : historyMap.values())
    {
      if (historyValueMatches(password, v))
      {
        if (debugEnabled())
        {
          TRACER.debugInfo("Returning true because the password is in " +
              "the history.");
        }

        return true;
      }
    }


    // If we've gotten here, then the password isn't in the history.
    if (debugEnabled())
    {
      TRACER.debugInfo("Returning false because the password isn't in the " +
          "history.");
    }

    return false;
  }



  /**
   * Gets a sorted list of the password history values contained in the user's
   * entry.  The values will be sorted by timestamp.
   *
   * @param  removeAttrs  A list into which any values will be placed that could
   *                      not be properly decoded.  It may be {@code null} if
   *                      this is not needed.
   */
  private TreeMap<Long,AttributeValue> getSortedHistoryValues(List<Attribute>
                                                                   removeAttrs)
  {
    TreeMap<Long,AttributeValue> historyMap =
         new TreeMap<Long,AttributeValue>();
    AttributeType historyType =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
    List<Attribute> attrList = userEntry.getAttribute(historyType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a)
        {
          String histStr = v.getValue().toString();
          int    hashPos = histStr.indexOf('#');
          if (hashPos <= 0)
          {
            if (debugEnabled())
            {
              TRACER.debugInfo("Found value " + histStr + " in the " +
                  "history with no timestamp.  Marking it " +
                  "for removal.");
            }

            if (removeAttrs != null)
            {
              removeAttrs.add(Attributes.create(a.getAttributeType(), v));
            }
          }
          else
          {
            try
            {
              long timestamp =
                   GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
                       ByteString.valueOf(histStr.substring(0, hashPos)));
              historyMap.put(timestamp, v);
            }
            catch (Exception e)
            {
              if (debugEnabled())
              {
                TRACER.debugCaught(DebugLogLevel.ERROR, e);

                TRACER.debugInfo("Could not decode the timestamp in " +
                    "history value " + histStr + " -- " + e +
                    ".  Marking it for removal.");
              }

              if (removeAttrs != null)
              {
                removeAttrs.add(Attributes
                    .create(a.getAttributeType(), v));
              }
            }
          }
        }
      }
    }

    return historyMap;
  }



  /**
   * Indicates whether the provided password matches the given history value.
   *
   * @param  password      The clear-text password for which to make the
   *                       determination.
   * @param  historyValue  The encoded history value to compare against the
   *                       clear-text password.
   *
   * @return  {@code true} if the provided password matches the history value,
   *          or {@code false} if not.
   */
  private boolean historyValueMatches(ByteString password,
                                      AttributeValue historyValue)
  {
    // According to draft-behera-ldap-password-policy, password history values
    // should be in the format time#syntaxoid#encodedvalue.  In this method,
    // we only care about the syntax OID and encoded password.
    try
    {
      String histStr  = historyValue.getValue().toString();
      int    hashPos1 = histStr.indexOf('#');
      if (hashPos1 <= 0)
      {
        if (debugEnabled())
        {
          TRACER.debugInfo("Returning false because the password history " +
              "value didn't include any hash characters.");
        }

        return false;
      }

      int hashPos2 = histStr.indexOf('#', hashPos1+1);
      if (hashPos2 < 0)
      {
        if (debugEnabled())
        {
          TRACER.debugInfo("Returning false because the password history " +
              "value only had one hash character.");
        }

        return false;
      }

      String syntaxOID = toLowerCase(histStr.substring(hashPos1+1, hashPos2));
      if (syntaxOID.equals(SYNTAX_AUTH_PASSWORD_OID))
      {
        StringBuilder[] authPWComponents =
             AuthPasswordSyntax.decodeAuthPassword(
                  histStr.substring(hashPos2+1));
        PasswordStorageScheme<?> scheme =
             DirectoryServer.getAuthPasswordStorageScheme(
                  authPWComponents[0].toString());
        if (scheme.authPasswordMatches(password, authPWComponents[1].toString(),
                                       authPWComponents[2].toString()))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Returning true because the auth password " +
                "history value matched.");
          }

          return true;
        }
        else
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Returning false because the auth password " +
                "history value did not match.");
          }

          return false;
        }
      }
      else if (syntaxOID.equals(SYNTAX_USER_PASSWORD_OID))
      {
        String[] userPWComponents =
             UserPasswordSyntax.decodeUserPassword(
                  histStr.substring(hashPos2+1));
        PasswordStorageScheme<?> scheme =
             DirectoryServer.getPasswordStorageScheme(userPWComponents[0]);
        if (scheme.passwordMatches(password,
            ByteString.valueOf(userPWComponents[1])))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Returning true because the user password " +
                "history value matched.");
          }

          return true;
        }
        else
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Returning false because the user password " +
                "history value did not match.");
          }

          return false;
        }
      }
      else
      {
        if (debugEnabled())
        {
          TRACER.debugInfo("Returning false because the syntax OID " +
              syntaxOID + " didn't match for either the auth " +
              "or user password syntax.");
        }

        return false;
      }
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);

        if (debugEnabled())
        {
          TRACER.debugInfo("Returning false because of an exception:  " +
                           stackTraceToSingleLineString(e));
        }
      }

      return false;
    }
  }



  /**
   * Updates the password history information for this user by adding all
   * current passwords to it.
   */
  public void updatePasswordHistory()
  {
    List<Attribute> attrList =
         userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a)
        {
          addPasswordToHistory(v.getValue().toString());
        }
      }
    }
  }



  /**
   * Adds the provided password to the password history.  If appropriate, one or
   * more old passwords may be evicted from the list if the total size would
   * exceed the configured count, or if passwords are older than the configured
   * duration.
   *
   * @param  encodedPassword  The encoded password (in either user password or
   *                          auth password format) to be added to the history.
   */
  private void addPasswordToHistory(String encodedPassword)
  {
    if (! maintainHistory())
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Not doing anything because password history " +
            "maintenance is disabled.");
      }

      return;
    }


    // Get a sorted list of the existing values to see if there are any that
    // should be removed.
    LinkedList<Attribute> removeAttrs = new LinkedList<Attribute>();
    TreeMap<Long,AttributeValue> historyMap =
         getSortedHistoryValues(removeAttrs);


    // If there is a maximum number of values to retain and we would be over the
    // limit with the new value, then get rid of enough values (oldest first)
    // to satisfy the count.
    AttributeType historyType =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
    int historyCount = passwordPolicy.getPasswordHistoryCount();
    if  ((historyCount > 0) && (historyMap.size() >= historyCount))
    {
      int numToDelete = (historyMap.size() - historyCount) + 1;
      LinkedHashSet<AttributeValue> removeValues =
           new LinkedHashSet<AttributeValue>(numToDelete);
      Iterator<AttributeValue> iterator = historyMap.values().iterator();
      while (iterator.hasNext() && (numToDelete > 0))
      {
        AttributeValue v = iterator.next();
        removeValues.add(v);
        iterator.remove();
        numToDelete--;

        if (debugEnabled())
        {
          TRACER.debugInfo("Removing history value " +
              v.getValue().toString() + " to preserve the history count.");
        }
      }

      if (! removeValues.isEmpty())
      {
        AttributeBuilder builder = new AttributeBuilder(historyType);
        builder.addAll(removeValues);
        removeAttrs.add(builder.toAttribute());
      }
    }


    // If there is a maximum duration, then get rid of any values that would be
    // over the duration.
    int historyDuration = passwordPolicy.getPasswordHistoryDuration();
    if (historyDuration > 0)
    {
      long minAgeToKeep = currentTime - (1000L * historyDuration);
      Iterator<Long> iterator = historyMap.keySet().iterator();
      LinkedHashSet<AttributeValue> removeValues =
           new LinkedHashSet<AttributeValue>();
      while (iterator.hasNext())
      {
        long timestamp = iterator.next();
        if (timestamp < minAgeToKeep)
        {
          AttributeValue v = historyMap.get(timestamp);
          removeValues.add(v);
          iterator.remove();

          if (debugEnabled())
          {
            TRACER.debugInfo("Removing history value " +
                v.getValue().toString() +
                " to preserve the history duration.");
          }
        }
        else
        {
          break;
        }
      }

      if (! removeValues.isEmpty())
      {
        AttributeBuilder builder = new AttributeBuilder(historyType);
        builder.addAll(removeValues);
        removeAttrs.add(builder.toAttribute());
      }
    }


    // At this point, we can add the new value.  However, we want to make sure
    // that its timestamp (which is the current time) doesn't conflict with any
    // value already in the list.  If there is a conflict, then simply add one
    // to it until we don't have any more conflicts.
    long newTimestamp = currentTime;
    while (historyMap.containsKey(newTimestamp))
    {
      newTimestamp++;
    }
    String newHistStr = GeneralizedTimeSyntax.format(newTimestamp) + "#" +
                        passwordPolicy.getPasswordAttribute().getSyntaxOID() +
                        "#" + encodedPassword;
    Attribute newHistAttr = Attributes.create(historyType, newHistStr);

    if (debugEnabled())
    {
      TRACER.debugInfo("Going to add history value " + newHistStr);
    }


    // Apply the changes, either by adding modifications or by directly updating
    // the entry.
    if (updateEntry)
    {
      LinkedList<AttributeValue> valueList = new LinkedList<AttributeValue>();
      for (Attribute a : removeAttrs)
      {
        userEntry.removeAttribute(a, valueList);
      }

      userEntry.addAttribute(newHistAttr, valueList);
    }
    else
    {
      for (Attribute a : removeAttrs)
      {
        modifications.add(new Modification(ModificationType.DELETE, a, true));
      }

      modifications.add(new Modification(ModificationType.ADD, newHistAttr,
                                         true));
    }
  }



  /**
   * Retrieves the password history state values for the user.  This is only
   * intended for testing purposes.
   *
   * @return  The password history state values for the user.
   */
  public String[] getPasswordHistoryValues()
  {
    ArrayList<String> historyValues = new ArrayList<String>();
    AttributeType historyType =
         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
    List<Attribute> attrList = userEntry.getAttribute(historyType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        for (AttributeValue v : a)
        {
          historyValues.add(v.getValue().toString());
        }
      }
    }

    String[] historyArray = new String[historyValues.size()];
    return historyValues.toArray(historyArray);
  }



  /**
   * Clears the password history state information for the user.  This is only
   * intended for testing purposes.
   */
  public void clearPasswordHistory()
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("Clearing password history for user %s", userDNString);
    }

    AttributeType type = DirectoryServer.getAttributeType(
                             OP_ATTR_PWPOLICY_HISTORY_LC, true);
    if (updateEntry)
    {
      userEntry.removeAttribute(type);
    }
    else
    {
      modifications.add(new Modification(ModificationType.REPLACE,
                                         Attributes.empty(type), true));
    }
  }



  /**
   * Generates a new password for the user.
   *
   * @return  The new password that has been generated, or <CODE>null</CODE> if
   *          no password generator has been defined.
   *
   * @throws  DirectoryException  If an error occurs while attempting to
   *                              generate the new password.
   */
  public ByteString generatePassword()
      throws DirectoryException
  {
    PasswordGenerator<?> generator = passwordPolicy.getPasswordGenerator();
    if (generator == null)
    {
      if (debugEnabled())
      {
        TRACER.debugWarning("Unable to generate a new password for user " +
            "%s because no password generator has been defined in the " +
            "associated password policy.", userDNString);
      }

      return null;
    }

    return generator.generatePassword(userEntry);
  }



  /**
   * Generates an account status notification for this user.
   *
   * @param  notificationType        The type for the account status
   *                                 notification.
   * @param  userEntry               The entry for the user to which this
   *                                 notification applies.
   * @param  message                 The human-readable message for the
   *                                 notification.
   * @param  notificationProperties  The set of properties for the notification.
   */
  public void generateAccountStatusNotification(
          AccountStatusNotificationType notificationType,
          Entry userEntry, Message message,
          Map<AccountStatusNotificationProperty,List<String>>
               notificationProperties)
  {
    generateAccountStatusNotification(new AccountStatusNotification(
         notificationType, userEntry, message, notificationProperties));
  }



  /**
   * Generates an account status notification for this user.
   *
   * @param  notification  The account status notification that should be
   *                       generated.
   */
  public void generateAccountStatusNotification(
                   AccountStatusNotification notification)
  {
    Collection<AccountStatusNotificationHandler<?>> handlers =
         passwordPolicy.getAccountStatusNotificationHandlers().values();
    if ((handlers == null) || handlers.isEmpty())
    {
      return;
    }

    for (AccountStatusNotificationHandler<?> handler : handlers)
    {
      handler.handleStatusNotification(notification);
    }
  }



  /**
   * Retrieves the set of modifications that correspond to changes made in
   * password policy processing that may need to be applied to the user entry.
   *
   * @return  The set of modifications that correspond to changes made in
   *          password policy processing that may need to be applied to the user
   *          entry.
   */
  public List<Modification> getModifications()
  {
    return modifications;
  }



  /**
   * Performs an internal modification to update the user's entry, if necessary.
   * This will do nothing if no modifications are required.
   *
   * @throws  DirectoryException  If a problem occurs while processing the
   *                              internal modification.
   */
  public void updateUserEntry()
         throws DirectoryException
  {
    // If there are no modifications, then there's nothing to do.
    if (modifications.isEmpty())
    {
      return;
    }


    // Convert the set of modifications to a set of LDAP modifications.
    ArrayList<RawModification> modList = new ArrayList<RawModification>();
    for (Modification m : modifications)
    {
      modList.add(RawModification.create(m.getModificationType(),
                       new LDAPAttribute(m.getAttribute())));
    }

    InternalClientConnection conn =
         InternalClientConnection.getRootConnection();
    ModifyOperation internalModify =
         conn.processModify(ByteString.valueOf(userDNString), modList);

    ResultCode resultCode = internalModify.getResultCode();
    if (resultCode != ResultCode.SUCCESS)
    {
      Message message = ERR_PWPSTATE_CANNOT_UPDATE_USER_ENTRY.get(userDNString,
                            String.valueOf(internalModify.getErrorMessage()));

      // If this is a root user, or if the password policy says that we should
      // ignore these problems, then log a warning message.  Otherwise, cause
      // the bind to fail.
      if ((DirectoryServer.isRootDN(userEntry.getDN()) ||
          (passwordPolicy.getStateUpdateFailurePolicy() ==
           PasswordPolicyCfgDefn.StateUpdateFailurePolicy.IGNORE)))
      {
        ErrorLogger.logError(message);
      }
      else
      {
        throw new DirectoryException(resultCode, message);
      }
    }
  }
}

