From 4ad090cb3d463eb6d5807e779505f89a15b420f6 Mon Sep 17 00:00:00 2001
From: jarnou <jarnou@localhost>
Date: Tue, 03 Jul 2007 10:06:12 +0000
Subject: [PATCH] Commits the refactoring of the core server to support proxy/distribution/virtual functionnalities This includes the new set of local operations as well as the workflows and the networkgroups.

---
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java |  203 +
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java |  111 
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java | 6638 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendBindOperation.java   |   57 
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java |   82 
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java    |  335 ++
 6 files changed, 7,426 insertions(+), 0 deletions(-)

diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java
new file mode 100644
index 0000000..681aff6
--- /dev/null
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java
@@ -0,0 +1,335 @@
+/*
+ * 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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.workflowelement.localbackend;
+
+import static org.opends.server.config.ConfigConstants.*;
+import static org.opends.server.messages.CoreMessages.*;
+import static org.opends.server.messages.MessageHandler.getMessage;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.opends.server.api.PasswordStorageScheme;
+import org.opends.server.api.PasswordValidator;
+import org.opends.server.core.AddOperation;
+import org.opends.server.core.AddOperationWrapper;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.PasswordPolicy;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.schema.AuthPasswordSyntax;
+import org.opends.server.schema.BooleanSyntax;
+import org.opends.server.schema.UserPasswordSyntax;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ObjectClass;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.operation.PostOperationAddOperation;
+import org.opends.server.types.operation.PostResponseAddOperation;
+import org.opends.server.types.operation.PreOperationAddOperation;
+import org.opends.server.util.TimeThread;
+
+/**
+ * This class defines an operation used to add an entry in a local backend
+ * of the Directory Server.
+ */
+public class LocalBackendAddOperation extends AddOperationWrapper
+  implements PreOperationAddOperation,
+             PostOperationAddOperation,
+             PostResponseAddOperation
+{
+
+  // The entry being added to the server.
+  private Entry entry;
+
+  /**
+   * Creates a new operation that may be used to add a new entry in a
+   * local backend of the Directory Server.
+   *
+   * @param add The operation to enhance.
+   */
+  public LocalBackendAddOperation(AddOperation add)
+  {
+    super(add);
+    LocalBackendWorkflowElement.attachLocalOperation (add, this);
+  }
+
+
+  /**
+   * Retrieves the entry to be added to the server.  Note that this will not be
+   * available to pre-parse plugins or during the conflict resolution portion of
+   * the synchronization processing.
+   *
+   * @return  The entry to be added to the server, or <CODE>null</CODE> if it is
+   *          not yet available.
+   */
+  public final Entry getEntryToAdd()
+  {
+    return entry;
+  }
+
+  /**
+   * Sets the entry to be added to the server.
+   *
+   * @param  entry - The entry to be added to the server, or <CODE>null</CODE>
+   *                 if it is not yet available.
+   */
+  public final void setEntryToAdd(Entry entry){
+    this.entry = entry;
+  }
+
+  /**
+   * Performs all password policy processing necessary for the provided add
+   * operation.
+   *
+   * @param  passwordPolicy  The password policy associated with the entry to be
+   *                         added.
+   * @param  userEntry       The user entry being added.
+   *
+   * @throws  DirectoryException  If a problem occurs while performing password
+   *                              policy processing for the add operation.
+   */
+  public final void handlePasswordPolicy(PasswordPolicy passwordPolicy,
+                                          Entry userEntry)
+         throws DirectoryException
+  {
+    // See if a password was specified.
+    AttributeType passwordAttribute = passwordPolicy.getPasswordAttribute();
+    List<Attribute> attrList = userEntry.getAttribute(passwordAttribute);
+    if ((attrList == null) || attrList.isEmpty())
+    {
+      // The entry doesn't have a password, so no action is required.
+      return;
+    }
+    else if (attrList.size() > 1)
+    {
+      // This must mean there are attribute options, which we won't allow for
+      // passwords.
+      int msgID = MSGID_PWPOLICY_ATTRIBUTE_OPTIONS_NOT_ALLOWED;
+      String message = getMessage(msgID, passwordAttribute.getNameOrOID());
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
+                                   msgID);
+    }
+
+    Attribute passwordAttr = attrList.get(0);
+    if (passwordAttr.hasOptions())
+    {
+      int msgID = MSGID_PWPOLICY_ATTRIBUTE_OPTIONS_NOT_ALLOWED;
+      String message = getMessage(msgID, passwordAttribute.getNameOrOID());
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
+                                   msgID);
+    }
+
+    LinkedHashSet<AttributeValue> values = passwordAttr.getValues();
+    if (values.isEmpty())
+    {
+      // This will be treated the same as not having a password.
+      return;
+    }
+
+    if ((! passwordPolicy.allowMultiplePasswordValues()) && (values.size() > 1))
+    {
+      // FIXME -- What if they're pre-encoded and might all be the same?
+      int    msgID   = MSGID_PWPOLICY_MULTIPLE_PW_VALUES_NOT_ALLOWED;
+      String message = getMessage(msgID, passwordAttribute.getNameOrOID());
+      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
+                                   msgID);
+    }
+
+    CopyOnWriteArrayList<PasswordStorageScheme> defaultStorageSchemes =
+         passwordPolicy.getDefaultStorageSchemes();
+    LinkedHashSet<AttributeValue> newValues =
+         new LinkedHashSet<AttributeValue>(defaultStorageSchemes.size());
+    for (AttributeValue v : values)
+    {
+      ByteString value = v.getValue();
+
+      // See if the password is pre-encoded.
+      if (passwordPolicy.usesAuthPasswordSyntax())
+      {
+        if (AuthPasswordSyntax.isEncoded(value))
+        {
+          if (passwordPolicy.allowPreEncodedPasswords())
+          {
+            newValues.add(v);
+            continue;
+          }
+          else
+          {
+            int    msgID   = MSGID_PWPOLICY_PREENCODED_NOT_ALLOWED;
+            String message = getMessage(msgID,
+                                        passwordAttribute.getNameOrOID());
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+                                         message, msgID);
+          }
+        }
+      }
+      else
+      {
+        if (UserPasswordSyntax.isEncoded(value))
+        {
+          if (passwordPolicy.allowPreEncodedPasswords())
+          {
+            newValues.add(v);
+            continue;
+          }
+          else
+          {
+            int    msgID   = MSGID_PWPOLICY_PREENCODED_NOT_ALLOWED;
+            String message = getMessage(msgID,
+                                        passwordAttribute.getNameOrOID());
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+                                         message, msgID);
+          }
+        }
+      }
+
+
+      // See if the password passes validation.  We should only do this if
+      // validation should be performed for administrators.
+      if (! passwordPolicy.skipValidationForAdministrators())
+      {
+        // There are never any current passwords for an add operation.
+        HashSet<ByteString> currentPasswords = new HashSet<ByteString>(0);
+        StringBuilder invalidReason = new StringBuilder();
+        for (PasswordValidator<?> validator :
+             passwordPolicy.getPasswordValidators().values())
+        {
+          if (! validator.passwordIsAcceptable(value, currentPasswords, this,
+                                               userEntry, invalidReason))
+          {
+            int    msgID   = MSGID_PWPOLICY_VALIDATION_FAILED;
+            String message = getMessage(msgID, passwordAttribute.getNameOrOID(),
+                                        String.valueOf(invalidReason));
+            throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+                                         message, msgID);
+          }
+        }
+      }
+
+
+      // Encode the password.
+      if (passwordPolicy.usesAuthPasswordSyntax())
+      {
+        for (PasswordStorageScheme s : defaultStorageSchemes)
+        {
+          ByteString encodedValue = s.encodeAuthPassword(value);
+          newValues.add(new AttributeValue(passwordAttribute, encodedValue));
+        }
+      }
+      else
+      {
+        for (PasswordStorageScheme s : defaultStorageSchemes)
+        {
+          ByteString encodedValue = s.encodePasswordWithScheme(value);
+          newValues.add(new AttributeValue(passwordAttribute, encodedValue));
+        }
+      }
+    }
+
+
+    // Put the new encoded values in the entry.
+    passwordAttr.setValues(newValues);
+
+
+    // Set the password changed time attribute.
+    ByteString timeString =
+         new ASN1OctetString(TimeThread.getGeneralizedTime());
+    AttributeType changedTimeType =
+         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_CHANGED_TIME_LC);
+    if (changedTimeType == null)
+    {
+      changedTimeType = DirectoryServer.getDefaultAttributeType(
+                                             OP_ATTR_PWPOLICY_CHANGED_TIME);
+    }
+
+    LinkedHashSet<AttributeValue> changedTimeValues =
+         new LinkedHashSet<AttributeValue>(1);
+    changedTimeValues.add(new AttributeValue(changedTimeType, timeString));
+
+    ArrayList<Attribute> changedTimeList = new ArrayList<Attribute>(1);
+    changedTimeList.add(new Attribute(changedTimeType,
+                                      OP_ATTR_PWPOLICY_CHANGED_TIME,
+                                      changedTimeValues));
+
+    userEntry.putAttribute(changedTimeType, changedTimeList);
+
+
+    // If we should force change on add, then set the appropriate flag.
+    if (passwordPolicy.forceChangeOnAdd())
+    {
+      AttributeType resetType =
+           DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED_LC);
+      if (resetType == null)
+      {
+        resetType = DirectoryServer.getDefaultAttributeType(
+                                         OP_ATTR_PWPOLICY_RESET_REQUIRED);
+      }
+
+      LinkedHashSet<AttributeValue> resetValues = new
+           LinkedHashSet<AttributeValue>(1);
+      resetValues.add(BooleanSyntax.createBooleanValue(true));
+
+      ArrayList<Attribute> resetList = new ArrayList<Attribute>(1);
+      resetList.add(new Attribute(resetType, OP_ATTR_PWPOLICY_RESET_REQUIRED,
+                                  resetValues));
+      userEntry.putAttribute(resetType, resetList);
+    }
+  }
+
+  /**
+   * Adds the provided objectClass to the entry, along with its superior classes
+   * if appropriate.
+   *
+   * @param  objectClass  The objectclass to add to the entry.
+   */
+  public final void addObjectClassChain(ObjectClass objectClass)
+  {
+    Map<ObjectClass, String> objectClasses = getObjectClasses();
+    if (objectClasses != null){
+      if (! objectClasses.containsKey(objectClass))
+      {
+        objectClasses.put(objectClass, objectClass.getNameOrOID());
+      }
+
+      ObjectClass superiorClass = objectClass.getSuperiorClass();
+      if ((superiorClass != null) &&
+          (! objectClasses.containsKey(superiorClass)))
+      {
+        addObjectClassChain(superiorClass);
+      }
+    }
+  }
+
+}
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendBindOperation.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendBindOperation.java
new file mode 100644
index 0000000..e101a13
--- /dev/null
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendBindOperation.java
@@ -0,0 +1,57 @@
+/*
+ * 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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.workflowelement.localbackend;
+
+import org.opends.server.core.BindOperation;
+import org.opends.server.core.BindOperationWrapper;
+import org.opends.server.types.operation.PostOperationBindOperation;
+import org.opends.server.types.operation.PostResponseBindOperation;
+import org.opends.server.types.operation.PreOperationBindOperation;
+
+/**
+ * This class defines an operation used to bind against the Directory Server,
+ * with the bound user entry within a local backend.
+ */
+public class LocalBackendBindOperation extends BindOperationWrapper
+  implements PreOperationBindOperation,
+             PostOperationBindOperation,
+             PostResponseBindOperation
+{
+
+  /**
+   * Creates a new operation that may be used to bind where
+   * the bound user entry is stored in a local backend of the Directory Server.
+   *
+   * @param bind The operation to enhance.
+   */
+  public LocalBackendBindOperation(BindOperation bind)
+  {
+    super(bind);
+    LocalBackendWorkflowElement.attachLocalOperation (bind, this);
+  }
+
+}
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java
new file mode 100644
index 0000000..b2d68ad
--- /dev/null
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java
@@ -0,0 +1,82 @@
+/*
+ * 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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.workflowelement.localbackend;
+
+
+import org.opends.server.core.DeleteOperationWrapper;
+import org.opends.server.core.DeleteOperation;
+import org.opends.server.types.Entry;
+import org.opends.server.types.operation.PostOperationDeleteOperation;
+import org.opends.server.types.operation.PostResponseDeleteOperation;
+import org.opends.server.types.operation.PreOperationDeleteOperation;
+
+/**
+ * This class defines an operation used to delete an entry in a local backend
+ * of the Directory Server.
+ */
+public class LocalBackendDeleteOperation extends DeleteOperationWrapper
+  implements PreOperationDeleteOperation,
+             PostOperationDeleteOperation,
+             PostResponseDeleteOperation
+{
+  // The entry to be deleted.
+  private Entry entry;
+
+  /**
+   * Creates a new operation that may be used to delete an entry from a
+   * local backend of the Directory Server.
+   *
+   * @param delete The operation to enhance.
+   */
+  public LocalBackendDeleteOperation(DeleteOperation delete)
+  {
+    super(delete);
+    LocalBackendWorkflowElement.attachLocalOperation (delete, this);
+  }
+
+
+  /**
+   * Retrieves the entry to be deleted.
+   *
+   * @return  The entry to be deleted, or <CODE>null</CODE> if the entry is not
+   *          yet available.
+   */
+  public Entry getEntryToDelete()
+  {
+    return entry;
+  }
+
+  /**
+   * Sets the entry to be deleted.
+   *
+   * @param  entry - The entry to be deleted
+   */
+  public void setEntryToDelete(Entry entry){
+    this.entry = entry;
+  }
+
+}
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java
new file mode 100644
index 0000000..28449d3
--- /dev/null
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java
@@ -0,0 +1,203 @@
+/*
+ * 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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.workflowelement.localbackend;
+
+import java.util.List;
+
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.core.ModifyOperationWrapper;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.Entry;
+import org.opends.server.types.Modification;
+import org.opends.server.types.operation.PostOperationModifyOperation;
+import org.opends.server.types.operation.PostResponseModifyOperation;
+import org.opends.server.types.operation.PreOperationModifyOperation;
+
+/**
+ * This class defines an operation used to modify an entry in a local backend
+ * of the Directory Server.
+ */
+public class LocalBackendModifyOperation extends ModifyOperationWrapper
+  implements PreOperationModifyOperation,
+             PostOperationModifyOperation,
+             PostResponseModifyOperation
+{
+  // The current entry, before any changes are applied.
+  private Entry currentEntry = null;
+
+  // The modified entry that will be stored in the backend.
+  private Entry modifiedEntry = null;
+
+  // The set of clear-text current passwords (if any were provided).
+  private List<AttributeValue> currentPasswords = null;
+
+  // The set of clear-text new passwords (if any were provided).
+  private List<AttributeValue> newPasswords = null;
+
+  /**
+   * Creates a new operation that may be used to modify an entry in a
+   * local backend of the Directory Server.
+   *
+   * @param modify The operation to enhance.
+   */
+  public LocalBackendModifyOperation(ModifyOperation modify)
+  {
+    super(modify);
+    LocalBackendWorkflowElement.attachLocalOperation (modify, this);
+  }
+
+  /**
+   * Retrieves the current entry before any modifications are applied.  This
+   * will not be available to pre-parse plugins.
+   *
+   * @return  The current entry, or <CODE>null</CODE> if it is not yet
+   *          available.
+   */
+  public final Entry getCurrentEntry()
+  {
+    return currentEntry;
+  }
+
+  /**
+   * Retrieves the set of clear-text current passwords for the user, if
+   * available.  This will only be available if the modify operation contains
+   * one or more delete elements that target the password attribute and provide
+   * the values to delete in the clear.  It will not be available to pre-parse
+   * plugins.
+   *
+   * @return  The set of clear-text current password values as provided in the
+   *          modify request, or <CODE>null</CODE> if there were none or this
+   *          information is not yet available.
+   */
+  public final List<AttributeValue> getCurrentPasswords()
+  {
+    return currentPasswords;
+  }
+
+  /**
+   * Retrieves the modified entry that is to be written to the backend.  This
+   * will be available to pre-operation plugins, and if such a plugin does make
+   * a change to this entry, then it is also necessary to add that change to
+   * the set of modifications to ensure that the update will be consistent.
+   *
+   * @return  The modified entry that is to be written to the backend, or
+   *          <CODE>null</CODE> if it is not yet available.
+   */
+  public final Entry getModifiedEntry()
+  {
+    return modifiedEntry;
+  }
+
+  /**
+   * Retrieves the set of clear-text new passwords for the user, if available.
+   * This will only be available if the modify operation contains one or more
+   * add or replace elements that target the password attribute and provide the
+   * values in the clear.  It will not be available to pre-parse plugins.
+   *
+   * @return  The set of clear-text new passwords as provided in the modify
+   *          request, or <CODE>null</CODE> if there were none or this
+   *          information is not yet available.
+   */
+  public final List<AttributeValue> getNewPasswords()
+  {
+    return newPasswords;
+  }
+
+  /**
+   * Retrieves the current entry before any modifications are applied.  This
+   * will not be available to pre-parse plugins.
+   *
+   * @param currentEntry The current entry.
+   */
+  public final void setCurrentEntry(Entry currentEntry)
+  {
+    this.currentEntry = currentEntry;
+  }
+
+  /**
+   * Register the set of clear-text current passwords for the user, if
+   * available.  This will only be available if the modify operation contains
+   * one or more delete elements that target the password attribute and provide
+   * the values to delete in the clear.
+   *
+   * @param currentPasswords The set of clear-text current password values as
+   *                         provided in the modify request.
+   */
+  public final void setCurrentPasswords(List<AttributeValue> currentPasswords)
+  {
+    this.currentPasswords = currentPasswords;
+  }
+
+  /**
+   * Register the modified entry that is to be written to the backend.
+   *
+   * @param modifiedEntry  The modified entry that is to be written to the
+   *                       backend, or <CODE>null</CODE> if it is not yet
+   *                       available.
+   */
+  public final void setModifiedEntry(Entry modifiedEntry)
+  {
+    this.modifiedEntry = modifiedEntry;
+  }
+
+  /**
+   * Register the set of clear-text new passwords for the user, if available.
+   * This will only be available if the modify operation contains one or more
+   * add or replace elements that target the password attribute and provide the
+   * values in the clear.
+   *
+   * @param newPasswords  The set of clear-text new passwords as provided in
+   *                      the modify request, or <CODE>null</CODE> if there
+   *                      were none or this information is not yet available.
+   */
+  public final void setNewPasswords(List<AttributeValue> newPasswords)
+  {
+    this.newPasswords = newPasswords;
+  }
+
+
+  /**
+   * Adds the provided modification to the set of modifications to this modify
+   * operation.
+   * In addition, the modification is applied to the modified entry.
+   *
+   * This may only be called by pre-operation plugins.
+   *
+   * @param  modification  The modification to add to the set of changes for
+   *                       this modify operation.
+   *
+   * @throws  DirectoryException  If an unexpected problem occurs while applying
+   *                              the modification to the entry.
+   */
+  public void addModification(Modification modification)
+    throws DirectoryException
+  {
+    modifiedEntry.applyModification(modification);
+    super.addModification(modification);
+  }
+}
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java
new file mode 100644
index 0000000..c13fe27
--- /dev/null
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java
@@ -0,0 +1,111 @@
+/*
+ * 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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.workflowelement.localbackend;
+
+
+import org.opends.server.api.Backend;
+import org.opends.server.core.SearchOperationWrapper;
+import org.opends.server.core.SearchOperation;
+import org.opends.server.types.CancelResult;
+import org.opends.server.types.CancelledOperationException;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.operation.PostOperationSearchOperation;
+import org.opends.server.types.operation.PreOperationSearchOperation;
+import org.opends.server.types.operation.SearchEntrySearchOperation;
+import org.opends.server.types.operation.SearchReferenceSearchOperation;
+
+/**
+ * This class defines an operation used to search for entries in a local backend
+ * of the Directory Server.
+ */
+public class LocalBackendSearchOperation extends SearchOperationWrapper
+  implements PreOperationSearchOperation,
+             PostOperationSearchOperation,
+             SearchEntrySearchOperation,
+             SearchReferenceSearchOperation
+{
+
+  /**
+   * Creates a new operation that may be used to search for entries in a
+   * local backend of the Directory Server.
+   *
+   * @param search The operation to enhance.
+   */
+  public LocalBackendSearchOperation(SearchOperation search){
+    super(search);
+    LocalBackendWorkflowElement.attachLocalOperation(search, this);
+  }
+
+  /**
+   * Processes the search in the provided backend and recursively through its
+   * subordinate backends.
+   *
+   * @param  backend  The backend in which to process the search.
+   *
+   * @throws  DirectoryException  If a problem occurs while processing the
+   *                              search.
+   *
+   * @throws  CancelledOperationException  If the backend noticed and reacted
+   *                                       to a request to cancel or abandon the
+   *                                       search operation.
+   */
+  public final void searchBackend(Backend backend)
+          throws DirectoryException, CancelledOperationException
+  {
+    // Check for and handle a request to cancel this operation.
+    if (getCancelRequest() != null)
+    {
+      setCancelResult(CancelResult.CANCELED);
+      setProcessingStopTime();
+      return;
+    }
+
+    // Perform the search in the provided backend.
+    backend.search(this);
+
+    // Search in the subordinate backends is now done by the workflows.
+
+    // If there are any subordinate backends, then process the search there as
+    // well.
+    // FIXME jdemendi - From now on, do not search in the subordinate backends
+    // because this is done by the workflow topology.
+//    Backend[] subBackends = backend.getSubordinateBackends();
+//    for (Backend b : subBackends)
+//    {
+//      DN[] baseDNs = b.getBaseDNs();
+//      for (DN dn : baseDNs)
+//      {
+//        if (dn.isDescendantOf(getBaseDN()))
+//        {
+//          searchBackend(b);
+//          break;
+//        }
+//      }
+//    }
+
+  }
+}
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
new file mode 100644
index 0000000..1fe984b
--- /dev/null
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
@@ -0,0 +1,6638 @@
+/*
+ * 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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.workflowelement.localbackend;
+
+import static org.opends.server.config.ConfigConstants.*;
+import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
+import static org.opends.server.messages.CoreMessages.*;
+import static org.opends.server.messages.MessageHandler.getMessage;
+import static org.opends.server.util.ServerConstants.*;
+import static org.opends.server.util.StaticUtils.getExceptionMessage;
+import static org.opends.server.util.StaticUtils.secondsToTimeString;
+import static org.opends.server.util.StaticUtils.toLowerCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+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.concurrent.locks.Lock;
+
+import org.opends.server.api.AttributeSyntax;
+import org.opends.server.api.Backend;
+import org.opends.server.api.ChangeNotificationListener;
+import org.opends.server.api.ClientConnection;
+import org.opends.server.api.PasswordStorageScheme;
+import org.opends.server.api.SASLMechanismHandler;
+import org.opends.server.api.SynchronizationProvider;
+import org.opends.server.api.plugin.PostOperationPluginResult;
+import org.opends.server.api.plugin.PreOperationPluginResult;
+import org.opends.server.controls.AuthorizationIdentityResponseControl;
+import org.opends.server.controls.GetEffectiveRights;
+import org.opends.server.controls.LDAPAssertionRequestControl;
+import org.opends.server.controls.LDAPPostReadRequestControl;
+import org.opends.server.controls.LDAPPostReadResponseControl;
+import org.opends.server.controls.LDAPPreReadRequestControl;
+import org.opends.server.controls.LDAPPreReadResponseControl;
+import org.opends.server.controls.MatchedValuesControl;
+import org.opends.server.controls.PasswordExpiredControl;
+import org.opends.server.controls.PasswordExpiringControl;
+import org.opends.server.controls.PasswordPolicyErrorType;
+import org.opends.server.controls.PasswordPolicyResponseControl;
+import org.opends.server.controls.PasswordPolicyWarningType;
+import org.opends.server.controls.PersistentSearchControl;
+import org.opends.server.controls.ProxiedAuthV1Control;
+import org.opends.server.controls.ProxiedAuthV2Control;
+import org.opends.server.core.AccessControlConfigManager;
+import org.opends.server.core.AddOperation;
+import org.opends.server.core.BindOperation;
+import org.opends.server.core.DeleteOperation;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.core.PasswordPolicy;
+import org.opends.server.core.PasswordPolicyState;
+import org.opends.server.core.PersistentSearch;
+import org.opends.server.core.PluginConfigManager;
+import org.opends.server.core.SearchOperation;
+import org.opends.server.loggers.debug.DebugLogger;
+import org.opends.server.loggers.debug.DebugTracer;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.types.LDAPException;
+import org.opends.server.schema.AuthPasswordSyntax;
+import org.opends.server.schema.BooleanSyntax;
+import org.opends.server.schema.UserPasswordSyntax;
+import org.opends.server.types.AcceptRejectWarn;
+import org.opends.server.types.AccountStatusNotificationType;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
+import org.opends.server.types.AuthenticationInfo;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.CancelResult;
+import org.opends.server.types.CancelledOperationException;
+import org.opends.server.types.Control;
+import org.opends.server.types.DN;
+import org.opends.server.types.DebugLogLevel;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ErrorLogCategory;
+import org.opends.server.types.ErrorLogSeverity;
+import org.opends.server.types.LockManager;
+import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ObjectClass;
+import org.opends.server.types.Privilege;
+import org.opends.server.types.RDN;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchResultEntry;
+import org.opends.server.types.SynchronizationProviderResult;
+import org.opends.server.types.Operation;
+import org.opends.server.util.Validator;
+import org.opends.server.workflowelement.LeafWorkflowElement;
+
+import static org.opends.server.loggers.ErrorLogger.*;
+import static org.opends.server.loggers.AccessLogger.*;
+
+/**
+ * This class defines a local backend workflow element; e-g an entity that
+ * handle the processing of an operation aginst a local backend.
+ */
+public class LocalBackendWorkflowElement extends LeafWorkflowElement
+{
+  /**
+   * The tracer object for the debug logger.
+   */
+  private static final DebugTracer TRACER = DebugLogger.getTracer();
+
+  // the backend associated to that workflow element
+  Backend backend;
+
+
+  /**
+   * Creates a new instance of the local backend workflow element.
+   *
+   * @param backend  the backend associated to that workflow element
+   */
+  public LocalBackendWorkflowElement(
+      Backend backend
+      )
+  {
+    this.backend  = backend;
+
+    if (this.backend != null)
+    {
+      isPrivate = this.backend.isPrivateBackend();
+    }
+  }
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public void execute(Operation operation)
+  {
+    switch (operation.getOperationType())
+    {
+    case BIND:
+      processBind((BindOperation) operation);
+      break;
+    case SEARCH:
+      processSearch((SearchOperation) operation);
+      break;
+    case ADD:
+      processAdd((AddOperation) operation);
+      break;
+    case DELETE:
+      processDelete((DeleteOperation) operation);
+      break;
+    case MODIFY:
+      processModify((ModifyOperation) operation);
+      break;
+    default:
+      // jdemendi - temporary code, just make sure that we don't fell into
+      // that incomplete code...
+      Validator.ensureTrue(false);
+      break;
+    }
+  }
+
+
+  /**
+   * Perform a modify operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  public void processModify(ModifyOperation operation)
+  {
+    LocalBackendModifyOperation localOperation =
+      new LocalBackendModifyOperation(operation);
+
+    processLocalModify(localOperation);
+  }
+
+  /**
+   * Perform a local modify operation against the local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  private void processLocalModify(LocalBackendModifyOperation localOp)
+  {
+    ClientConnection clientConnection = localOp.getClientConnection();
+
+    // Get the plugin config manager that will be used for invoking plugins.
+    PluginConfigManager pluginConfigManager =
+      DirectoryServer.getPluginConfigManager();
+    boolean skipPostOperation = false;
+
+    // Create a labeled block of code that we can break out of if a problem is
+    // detected.
+    modifyProcessing:
+    {
+      DN entryDN = localOp.getEntryDN();
+      if (entryDN == null){
+        break modifyProcessing;
+      }
+
+      // Process the modifications to convert them from their raw form to the
+      // form required for the rest of the modify processing.
+      List<Modification> modifications = localOp.getModifications();
+      if (modifications == null)
+      {
+        break modifyProcessing;
+      }
+
+      if (modifications.isEmpty())
+      {
+        localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+        localOp.appendErrorMessage(
+            getMessage(MSGID_MODIFY_NO_MODIFICATIONS,
+            String.valueOf(entryDN)));
+        break modifyProcessing;
+      }
+
+
+      // If the user must change their password before doing anything else, and
+      // if the target of the modify operation isn't the user's own entry, then
+      // reject the request.
+      if ((! localOp.isInternalOperation()) &&
+          clientConnection.mustChangePassword())
+      {
+        DN authzDN = localOp.getAuthorizationDN();
+        if ((authzDN != null) && (! authzDN.equals(entryDN)))
+        {
+          // The user will not be allowed to do anything else before
+          // the password gets changed.
+          localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+          int msgID = MSGID_MODIFY_MUST_CHANGE_PASSWORD;
+          localOp.appendErrorMessage(getMessage(msgID));
+          break modifyProcessing;
+        }
+      }
+
+
+      // Check for and handle a request to cancel this operation.
+      if (localOp.getCancelRequest() != null)
+      {
+        localOp.indicateCancelled(localOp.getCancelRequest());
+        localOp.setProcessingStopTime();
+      }
+
+      // Acquire a write lock on the target entry.
+      Lock entryLock = null;
+      for (int i=0; i < 3; i++)
+      {
+        entryLock = LockManager.lockWrite(entryDN);
+        if (entryLock != null)
+        {
+          break;
+        }
+      }
+
+      if (entryLock == null)
+      {
+        localOp.setResultCode(DirectoryServer.getServerErrorResultCode());
+        localOp.appendErrorMessage(
+            getMessage(MSGID_MODIFY_CANNOT_LOCK_ENTRY,
+            String.valueOf(entryDN)));
+
+        skipPostOperation = true;
+        break modifyProcessing;
+      }
+
+
+      try
+      {
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+
+        // Get the entry to modify.  If it does not exist, then fail.
+        Entry currentEntry = null;
+        try
+        {
+          currentEntry = backend.getEntry(entryDN);
+          localOp.setCurrentEntry(currentEntry);
+        }
+        catch (DirectoryException de)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, de);
+          }
+
+          localOp.setResultCode(de.getResultCode());
+          localOp.appendErrorMessage(de.getErrorMessage());
+          localOp.setMatchedDN(de.getMatchedDN());
+          localOp.setReferralURLs(de.getReferralURLs());
+
+          break modifyProcessing;
+        }
+
+        if (currentEntry == null)
+        {
+          localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+          localOp.appendErrorMessage(getMessage(MSGID_MODIFY_NO_SUCH_ENTRY,
+              String.valueOf(entryDN)));
+
+          // See if one of the entry's ancestors exists.
+          DN parentDN = entryDN.getParentDNInSuffix();
+          while (parentDN != null)
+          {
+            try
+            {
+              if (DirectoryServer.entryExists(parentDN))
+              {
+                localOp.setMatchedDN(parentDN);
+                break;
+              }
+            }
+            catch (Exception e)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, e);
+              }
+              break;
+            }
+
+            parentDN = parentDN.getParentDNInSuffix();
+          }
+
+          break modifyProcessing;
+        }
+
+        // Check to see if there are any controls in the request.  If so, then
+        // see if there is any special processing required.
+        boolean                    noOp            = false;
+        LDAPPreReadRequestControl  preReadRequest  = null;
+        LDAPPostReadRequestControl postReadRequest = null;
+        List<Control> requestControls = localOp.getRequestControls();
+        if ((requestControls != null) && (! requestControls.isEmpty()))
+        {
+          for (int i=0; i < requestControls.size(); i++)
+          {
+            Control c   = requestControls.get(i);
+            String  oid = c.getOID();
+
+            if (oid.equals(OID_LDAP_ASSERTION))
+            {
+              LDAPAssertionRequestControl assertControl;
+              if (c instanceof LDAPAssertionRequestControl)
+              {
+                assertControl = (LDAPAssertionRequestControl) c;
+              }
+              else
+              {
+                try
+                {
+                  assertControl = LDAPAssertionRequestControl.decodeControl(c);
+                  requestControls.set(i, assertControl);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break modifyProcessing;
+                }
+              }
+
+              try
+              {
+                // FIXME -- We need to determine whether the current user has
+                //          permission to make this determination.
+                SearchFilter filter = assertControl.getSearchFilter();
+                if (! filter.matchesEntry(currentEntry))
+                {
+                  localOp.setResultCode(ResultCode.ASSERTION_FAILED);
+
+                  localOp.appendErrorMessage(
+                      getMessage(MSGID_MODIFY_ASSERTION_FAILED,
+                      String.valueOf(entryDN)));
+
+                  break modifyProcessing;
+                }
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(ResultCode.PROTOCOL_ERROR);
+
+                int msgID = MSGID_MODIFY_CANNOT_PROCESS_ASSERTION_FILTER;
+                localOp.appendErrorMessage(getMessage(
+                    msgID, String.valueOf(entryDN),
+                    de.getErrorMessage()));
+
+                break modifyProcessing;
+              }
+            }
+            else if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED))
+            {
+              noOp = true;
+            }
+            else if (oid.equals(OID_LDAP_READENTRY_PREREAD))
+            {
+              if (c instanceof LDAPAssertionRequestControl)
+              {
+                preReadRequest = (LDAPPreReadRequestControl) c;
+              }
+              else
+              {
+                try
+                {
+                  preReadRequest = LDAPPreReadRequestControl.decodeControl(c);
+                  requestControls.set(i, preReadRequest);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break modifyProcessing;
+                }
+              }
+            }
+            else if (oid.equals(OID_LDAP_READENTRY_POSTREAD))
+            {
+              if (c instanceof LDAPAssertionRequestControl)
+              {
+                postReadRequest = (LDAPPostReadRequestControl) c;
+              }
+              else
+              {
+                try
+                {
+                  postReadRequest = LDAPPostReadRequestControl.decodeControl(c);
+                  requestControls.set(i, postReadRequest);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break modifyProcessing;
+                }
+              }
+            }
+            else if (oid.equals(OID_PROXIED_AUTH_V1))
+            {
+              // The requester must have the PROXIED_AUTH privilige in order to
+              // be able to use this control.
+              if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH,
+                  localOp))
+              {
+                int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+                break modifyProcessing;
+              }
+
+
+              ProxiedAuthV1Control proxyControl;
+              if (c instanceof ProxiedAuthV1Control)
+              {
+                proxyControl = (ProxiedAuthV1Control) c;
+              }
+              else
+              {
+                try
+                {
+                  proxyControl = ProxiedAuthV1Control.decodeControl(c);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break modifyProcessing;
+                }
+              }
+
+
+              Entry authorizationEntry;
+              try
+              {
+                authorizationEntry = proxyControl.getAuthorizationEntry();
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(de.getResultCode());
+                localOp.appendErrorMessage(de.getErrorMessage());
+
+                break modifyProcessing;
+              }
+
+
+              if (AccessControlConfigManager.getInstance().
+                      getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                      authorizationEntry) == false) {
+                localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+                int msgID = MSGID_MODIFY_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+                localOp.appendErrorMessage(getMessage(msgID,
+                    String.valueOf(entryDN)));
+
+                skipPostOperation = true;
+                break modifyProcessing;
+              }
+              localOp.setAuthorizationEntry(authorizationEntry);
+              if (authorizationEntry == null)
+              {
+                localOp.setProxiedAuthorizationDN(DN.nullDN());
+              }
+              else
+              {
+                localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+              }
+            }
+            else if (oid.equals(OID_PROXIED_AUTH_V2))
+            {
+              // The requester must have the PROXIED_AUTH privilige in order to
+              // be able to use this control.
+              if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH,
+                  localOp))
+              {
+                int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+                break modifyProcessing;
+              }
+
+
+              ProxiedAuthV2Control proxyControl;
+              if (c instanceof ProxiedAuthV2Control)
+              {
+                proxyControl = (ProxiedAuthV2Control) c;
+              }
+              else
+              {
+                try
+                {
+                  proxyControl = ProxiedAuthV2Control.decodeControl(c);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break modifyProcessing;
+                }
+              }
+
+
+              Entry authorizationEntry;
+              try
+              {
+                authorizationEntry = proxyControl.getAuthorizationEntry();
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(de.getResultCode());
+                localOp.appendErrorMessage(de.getErrorMessage());
+
+                break modifyProcessing;
+              }
+
+              if (AccessControlConfigManager.getInstance().
+                  getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                      authorizationEntry) == false) {
+                localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+                int msgID = MSGID_MODIFY_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+                localOp.appendErrorMessage(getMessage(msgID,
+                    String.valueOf(entryDN)));
+
+                skipPostOperation = true;
+                break modifyProcessing;
+              }
+              localOp.setAuthorizationEntry(authorizationEntry);
+              if (authorizationEntry == null)
+              {
+                localOp.setProxiedAuthorizationDN(DN.nullDN());
+              }
+              else
+              {
+                localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+              }
+            }
+
+            // NYI -- Add support for additional controls.
+            else if (c.isCritical())
+            {
+              if ((backend == null) || (! backend.supportsControl(oid)))
+              {
+                localOp.setResultCode(
+                    ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+
+                int msgID = MSGID_MODIFY_UNSUPPORTED_CRITICAL_CONTROL;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                    oid));
+
+                break modifyProcessing;
+              }
+            }
+          }
+        }
+
+
+        // Get the password policy state object for the entry that can be used
+        // to perform any appropriate password policy processing.  Also, see if
+        // the entry is being updated by the end user or an administrator.
+        PasswordPolicyState pwPolicyState;
+        boolean selfChange = entryDN.equals(localOp.getAuthorizationDN());
+        try
+        {
+          // FIXME -- Need a way to enable debug mode.
+          pwPolicyState = new PasswordPolicyState(currentEntry, false, false);
+        }
+        catch (DirectoryException de)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, de);
+          }
+
+          localOp.setResultCode(de.getResultCode());
+          localOp.appendErrorMessage(de.getErrorMessage());
+
+          break modifyProcessing;
+        }
+
+
+        // Create a duplicate of the entry and apply the changes to it.
+        Entry modifiedEntry = currentEntry.duplicate(false);
+        localOp.setModifiedEntry(modifiedEntry);
+
+        if (! noOp)
+        {
+          // Invoke any conflict resolution processing that might be needed by
+          // the synchronization provider.
+          for (SynchronizationProvider provider :
+            DirectoryServer.getSynchronizationProviders())
+          {
+            try
+            {
+              SynchronizationProviderResult result =
+                provider.handleConflictResolution(localOp);
+              if (! result.continueOperationProcessing())
+              {
+                break modifyProcessing;
+              }
+            }
+            catch (DirectoryException de)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+
+              logError(ErrorLogCategory.SYNCHRONIZATION,
+                  ErrorLogSeverity.SEVERE_ERROR,
+                  MSGID_MODIFY_SYNCH_CONFLICT_RESOLUTION_FAILED,
+                  localOp.getConnectionID(),
+                  localOp.getOperationID(),
+                  getExceptionMessage(de));
+
+              localOp.setResponseData(de);
+              break modifyProcessing;
+            }
+          }
+        }
+
+
+        // Declare variables used for password policy state processing.
+        boolean passwordChanged = false;
+        boolean currentPasswordProvided = false;
+        boolean isEnabled = true;
+        boolean enabledStateChanged = false;
+        int numPasswords;
+        if (currentEntry.hasAttribute(
+            pwPolicyState.getPolicy().getPasswordAttribute()))
+        {
+          // It may actually have more than one, but we can't tell the
+          // difference if the values are encoded, and its enough for our
+          // purposes just to know that there is at least one.
+          numPasswords = 1;
+        }
+        else
+        {
+          numPasswords = 0;
+        }
+
+
+        // If it's not an internal or synchronization operation, then iterate
+        // through the set of modifications to see if a password is included in
+        // the changes.  If so, then add the appropriate state changes to the
+        // set of modifications.
+        if (! (localOp.isInternalOperation()
+            || localOp.isSynchronizationOperation()))
+        {
+          for (Modification m : modifications)
+          {
+            if (m.getAttribute().getAttributeType().equals(
+                pwPolicyState.getPolicy().getPasswordAttribute()))
+            {
+              passwordChanged = true;
+              if (! selfChange)
+              {
+                if (! clientConnection.hasPrivilege(
+                    Privilege.PASSWORD_RESET,
+                    localOp))
+                {
+                  int msgID = MSGID_MODIFY_PWRESET_INSUFFICIENT_PRIVILEGES;
+                  localOp.appendErrorMessage(getMessage(msgID));
+                  localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+                  break modifyProcessing;
+                }
+              }
+
+              break;
+            }
+          }
+        }
+
+
+        for (Modification m : modifications)
+        {
+          Attribute     a = m.getAttribute();
+          AttributeType t = a.getAttributeType();
+
+
+          // If the attribute type is marked "NO-USER-MODIFICATION" then fail
+          // unless this is an internal operation or is related to
+          // synchronization in some way.
+          if (t.isNoUserModification())
+          {
+            if (! (localOp.isInternalOperation() ||
+                localOp.isSynchronizationOperation() ||
+                m.isInternal()))
+            {
+              localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+              localOp.appendErrorMessage(
+                  getMessage(MSGID_MODIFY_ATTR_IS_NO_USER_MOD,
+                  String.valueOf(entryDN),
+                  a.getName()));
+              break modifyProcessing;
+            }
+          }
+
+          // If the attribute type is marked "OBSOLETE" and the modification
+          // is setting new values, then fail unless this is an internal
+          // operation or is related to synchronization in some way.
+          if (t.isObsolete())
+          {
+            if (a.hasValue() &&
+                (m.getModificationType() != ModificationType.DELETE))
+            {
+              if (! (localOp.isInternalOperation() ||
+                  localOp.isSynchronizationOperation() ||
+                  m.isInternal()))
+              {
+                localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+                localOp.appendErrorMessage(
+                    getMessage(MSGID_MODIFY_ATTR_IS_OBSOLETE,
+                    String.valueOf(entryDN),
+                    a.getName()));
+                break modifyProcessing;
+              }
+            }
+          }
+
+
+          // See if the attribute is one which controls the privileges available
+          // for a user.  If it is, then the client must have the
+          // PRIVILEGE_CHANGE privilege.
+          if (t.hasName(OP_ATTR_PRIVILEGE_NAME))
+          {
+            if (! clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE,
+                localOp))
+            {
+              int msgID = MSGID_MODIFY_CHANGE_PRIVILEGE_INSUFFICIENT_PRIVILEGES;
+              localOp.appendErrorMessage(getMessage(msgID));
+              localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+              break modifyProcessing;
+            }
+          }
+
+
+          // If the modification is updating the password attribute, then
+          // perform any necessary password policy processing.  This processing
+          // should be skipped for synchronization operations.
+          boolean isPassword
+          = t.equals(pwPolicyState.getPolicy().getPasswordAttribute());
+          if (isPassword && (!(localOp.isSynchronizationOperation())))
+          {
+            // If the attribute contains any options, then reject it.  Passwords
+            // will not be allowed to have options. Skipped for internal
+            // operations.
+            if(!localOp.isInternalOperation())
+            {
+              if (a.hasOptions())
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                int msgID = MSGID_MODIFY_PASSWORDS_CANNOT_HAVE_OPTIONS;
+                localOp.appendErrorMessage(getMessage(msgID));
+                break modifyProcessing;
+              }
+
+
+              // If it's a self change, then see if that's allowed.
+              if (selfChange &&
+                  (! pwPolicyState.getPolicy().allowUserPasswordChanges()))
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                int msgID = MSGID_MODIFY_NO_USER_PW_CHANGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                break modifyProcessing;
+              }
+
+
+              // If we require secure password changes, then makes sure it's a
+              // secure communication channel.
+              if (pwPolicyState.getPolicy().requireSecurePasswordChanges() &&
+                  (! clientConnection.isSecure()))
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                int msgID = MSGID_MODIFY_REQUIRE_SECURE_CHANGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                break modifyProcessing;
+              }
+
+
+              // If it's a self change and it's not been long enough since the
+              // previous change, then reject it.
+              if (selfChange && pwPolicyState.isWithinMinimumAge())
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                int msgID = MSGID_MODIFY_WITHIN_MINIMUM_AGE;
+                localOp.appendErrorMessage(getMessage(msgID));
+                break modifyProcessing;
+              }
+            }
+
+            // Check to see whether this will adding, deleting, or replacing
+            // password values (increment doesn't make any sense for passwords).
+            // Then perform the appropriate type of processing for that kind of
+            // modification.
+            boolean isAdd = false;
+            LinkedHashSet<AttributeValue> pwValues = a.getValues();
+            LinkedHashSet<AttributeValue> encodedValues =
+              new LinkedHashSet<AttributeValue>();
+            switch (m.getModificationType())
+            {
+            case ADD:
+            case REPLACE:
+              int passwordsToAdd = pwValues.size();
+
+              if (m.getModificationType() == ModificationType.ADD)
+              {
+                numPasswords += passwordsToAdd;
+                isAdd = true;
+              }
+              else
+              {
+                numPasswords = passwordsToAdd;
+              }
+              // If there were multiple password values provided, then make
+              // sure that's OK.
+
+              if (! localOp.isInternalOperation() &&
+                  ! pwPolicyState.getPolicy().allowExpiredPasswordChanges() &&
+                  (passwordsToAdd > 1))
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                int msgID = MSGID_MODIFY_MULTIPLE_VALUES_NOT_ALLOWED;
+                localOp.appendErrorMessage(getMessage(msgID));
+                break modifyProcessing;
+              }
+
+              // Iterate through the password values and see if any of them
+              // are pre-encoded.  If so, then check to see if we'll allow it.
+              // Otherwise, store the clear-text values for later validation
+              // and update the attribute with the encoded values.
+              for (AttributeValue v : pwValues)
+              {
+                if (pwPolicyState.passwordIsPreEncoded(v.getValue()))
+                {
+                  if ((!localOp.isInternalOperation()) &&
+                      ! pwPolicyState.getPolicy().allowPreEncodedPasswords())
+                  {
+                    localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                    int msgID = MSGID_MODIFY_NO_PREENCODED_PASSWORDS;
+                    localOp.appendErrorMessage(getMessage(msgID));
+                    break modifyProcessing;
+                  }
+                  else
+                  {
+                    encodedValues.add(v);
+                  }
+                }
+                else
+                {
+                  if (isAdd)
+                  {
+                    // Make sure that the password value doesn't already
+                    // exist.
+                    if (pwPolicyState.passwordMatches(v.getValue()))
+                    {
+                      localOp.setResultCode(
+                          ResultCode.ATTRIBUTE_OR_VALUE_EXISTS);
+
+                      int msgID = MSGID_MODIFY_PASSWORD_EXISTS;
+                      localOp.appendErrorMessage(getMessage(msgID));
+                      break modifyProcessing;
+                    }
+                  }
+
+                  List<AttributeValue> newPasswords =
+                    localOp.getNewPasswords() ;
+                  if (newPasswords == null)
+                  {
+                    newPasswords = new LinkedList<AttributeValue>();
+                    localOp.setNewPasswords(newPasswords);
+                  }
+
+                  newPasswords.add(v);
+
+                  try
+                  {
+                    for (ByteString s :
+                      pwPolicyState.encodePassword(v.getValue()))
+                    {
+                      encodedValues.add(new AttributeValue(
+                          a.getAttributeType(), s));
+                    }
+                  }
+                  catch (DirectoryException de)
+                  {
+                    if (debugEnabled())
+                    {
+                      TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                    }
+
+                    localOp.setResultCode(de.getResultCode());
+                    localOp.appendErrorMessage(de.getErrorMessage());
+                    break modifyProcessing;
+                  }
+                }
+              }
+
+              a.setValues(encodedValues);
+
+              break;
+
+            case DELETE:
+              // Iterate through the password values and see if any of them
+              // are pre-encoded.  We will never allow pre-encoded passwords
+              // for user password changes, but we will allow them for
+              // administrators.  For each clear-text value, verify that at
+              // least one value in the entry matches and replace the
+              // clear-text value with the appropriate encoded forms.
+              for (AttributeValue v : pwValues)
+              {
+                if (pwPolicyState.passwordIsPreEncoded(v.getValue()))
+                {
+                  if ((!localOp.isInternalOperation()) && selfChange)
+                  {
+                    localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                    int msgID = MSGID_MODIFY_NO_PREENCODED_PASSWORDS;
+                    localOp.appendErrorMessage(getMessage(msgID));
+                    break modifyProcessing;
+                  }
+                  else
+                  {
+                    encodedValues.add(v);
+                  }
+                }
+                else
+                {
+                  List<Attribute> attrList = currentEntry.getAttribute(t);
+                  if ((attrList == null) || (attrList.isEmpty()))
+                  {
+                    localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                    int msgID = MSGID_MODIFY_NO_EXISTING_VALUES;
+                    localOp.appendErrorMessage(getMessage(msgID));
+                    break modifyProcessing;
+                  }
+                  boolean found = false;
+                  for (Attribute attr : attrList)
+                  {
+                    for (AttributeValue av : attr.getValues())
+                    {
+                      if (pwPolicyState.getPolicy().usesAuthPasswordSyntax())
+                      {
+                        if (AuthPasswordSyntax.isEncoded(av.getValue()))
+                        {
+                          try
+                          {
+                            StringBuilder[] compoenents =
+                              AuthPasswordSyntax.decodeAuthPassword(
+                                  av.getStringValue());
+                            PasswordStorageScheme scheme =
+                              DirectoryServer.
+                              getAuthPasswordStorageScheme(
+                                  compoenents[0].toString());
+                            if (scheme != null)
+                            {
+                              if (scheme.authPasswordMatches(
+                                  v.getValue(),
+                                  compoenents[1].toString(),
+                                  compoenents[2].toString()))
+                              {
+                                encodedValues.add(av);
+                                found = true;
+                              }
+                            }
+                          }
+                          catch (DirectoryException de)
+                          {
+                            if (debugEnabled())
+                            {
+                              TRACER.debugCaught(
+                                  DebugLogLevel.ERROR, de);
+                            }
+
+                            localOp.setResultCode(de.getResultCode());
+
+                            int msgID = MSGID_MODIFY_CANNOT_DECODE_PW;
+                            localOp.appendErrorMessage(
+                                getMessage(msgID, de.getErrorMessage()));
+                            break modifyProcessing;
+                          }
+                        }
+                        else
+                        {
+                          if (av.equals(v))
+                          {
+                            encodedValues.add(v);
+                            found = true;
+                          }
+                        }
+                      }
+                      else
+                      {
+                        if (UserPasswordSyntax.isEncoded(av.getValue()))
+                        {
+                          try
+                          {
+                            String[] compoenents =
+                              UserPasswordSyntax.decodeUserPassword(
+                                  av.getStringValue());
+                            PasswordStorageScheme scheme =
+                              DirectoryServer.getPasswordStorageScheme(
+                                  toLowerCase(compoenents[0]));
+                            if (scheme != null)
+                            {
+                              if (scheme.passwordMatches(
+                                  v.getValue(),
+                                  new ASN1OctetString(compoenents[1])))
+                              {
+                                encodedValues.add(av);
+                                found = true;
+                              }
+                            }
+                          }
+                          catch (DirectoryException de)
+                          {
+                            if (debugEnabled())
+                            {
+                              TRACER.debugCaught(
+                                  DebugLogLevel.ERROR, de);
+                            }
+
+                            localOp.setResultCode(de.getResultCode());
+
+                            int msgID = MSGID_MODIFY_CANNOT_DECODE_PW;
+                            localOp.appendErrorMessage(getMessage(msgID,
+                                de.getErrorMessage()));
+                            break modifyProcessing;
+                          }
+                        }
+                        else
+                        {
+                          if (av.equals(v))
+                          {
+                            encodedValues.add(v);
+                            found = true;
+                          }
+                        }
+                      }
+                    }
+                  }
+
+                  if (found)
+                  {
+                    List<AttributeValue> currentPasswords =
+                      localOp.getCurrentPasswords();
+                    if (currentPasswords == null)
+                    {
+                      currentPasswords = new LinkedList<AttributeValue>();
+                      localOp.setCurrentPasswords(currentPasswords);
+                    }
+                    currentPasswords.add(v);
+
+                    numPasswords--;
+                  }
+                  else
+                  {
+                    localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                    int msgID = MSGID_MODIFY_INVALID_PASSWORD;
+                    localOp.appendErrorMessage(getMessage(msgID));
+                    break modifyProcessing;
+                  }
+
+                  currentPasswordProvided = true;
+                }
+              }
+
+              a.setValues(encodedValues);
+
+              break;
+
+            default:
+              localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+            int msgID = MSGID_MODIFY_INVALID_MOD_TYPE_FOR_PASSWORD;
+            localOp.appendErrorMessage(getMessage(msgID,
+                String.valueOf(m.getModificationType()), a.getName()));
+
+            break modifyProcessing;
+            }
+          }
+          else
+          {
+            // See if it's an attribute used to maintain the account
+            // enabled/disabled state.
+            AttributeType disabledAttr =
+              DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_DISABLED,
+                  true);
+            if (t.equals(disabledAttr))
+            {
+              enabledStateChanged = true;
+              for (AttributeValue v : a.getValues())
+              {
+                try
+                {
+                  isEnabled = (! BooleanSyntax.decodeBooleanValue(
+                      v.getNormalizedValue()));
+                }
+                catch (DirectoryException de)
+                {
+                  localOp.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+
+                  int msgID = MSGID_MODIFY_INVALID_DISABLED_VALUE;
+                  String message =
+                    getMessage(msgID, OP_ATTR_ACCOUNT_DISABLED,
+                        String.valueOf(de.getErrorMessage()));
+                  localOp.appendErrorMessage(message);
+                  break modifyProcessing;
+                }
+              }
+            }
+          }
+
+
+          switch (m.getModificationType())
+          {
+          case ADD:
+            // Make sure that one or more values have been provided for the
+            // attribute.
+            LinkedHashSet<AttributeValue> newValues = a.getValues();
+            if ((newValues == null) || newValues.isEmpty())
+            {
+              localOp.setResultCode(ResultCode.PROTOCOL_ERROR);
+              localOp.appendErrorMessage(getMessage(MSGID_MODIFY_ADD_NO_VALUES,
+                  String.valueOf(entryDN),
+                  a.getName()));
+              break modifyProcessing;
+            }
+
+
+            // Make sure that all the new values are valid according to the
+            // associated syntax.
+            if (DirectoryServer.checkSchema())
+            {
+              AcceptRejectWarn syntaxPolicy =
+                DirectoryServer.getSyntaxEnforcementPolicy();
+              AttributeSyntax syntax = t.getSyntax();
+
+              if (syntaxPolicy == AcceptRejectWarn.REJECT)
+              {
+                StringBuilder invalidReason =
+                  new StringBuilder();
+
+                for (AttributeValue v : newValues)
+                {
+                  if (! syntax.valueIsAcceptable(v.getValue(), invalidReason))
+                  {
+                    localOp.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+
+                    int msgID = MSGID_MODIFY_ADD_INVALID_SYNTAX;
+                    localOp.appendErrorMessage(getMessage(msgID,
+                        String.valueOf(entryDN),
+                        a.getName(),
+                        v.getStringValue(),
+                        invalidReason.toString()));
+
+                    break modifyProcessing;
+                  }
+                }
+              }
+              else if (syntaxPolicy == AcceptRejectWarn.WARN)
+              {
+                StringBuilder invalidReason = new StringBuilder();
+
+                for (AttributeValue v : newValues)
+                {
+                  if (! syntax.valueIsAcceptable(v.getValue(), invalidReason))
+                  {
+                    localOp.setResultCode(
+                        ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+
+                    int msgID = MSGID_MODIFY_ADD_INVALID_SYNTAX;
+                    logError(ErrorLogCategory.SCHEMA,
+                        ErrorLogSeverity.SEVERE_WARNING, msgID,
+                        String.valueOf(entryDN), a.getName(),
+                        v.getStringValue(), invalidReason.toString());
+
+                    invalidReason = new StringBuilder();
+                  }
+                }
+              }
+            }
+
+
+            // Add the provided attribute or merge an existing attribute with
+            // the values of the new attribute.  If there are any duplicates,
+            // then fail.
+            LinkedList<AttributeValue> duplicateValues =
+              new LinkedList<AttributeValue>();
+            if (a.getAttributeType().isObjectClassType())
+            {
+              try
+              {
+                modifiedEntry.addObjectClasses(newValues);
+                break;
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResponseData(de);
+                break modifyProcessing;
+              }
+            }
+            else
+            {
+              modifiedEntry.addAttribute(a, duplicateValues);
+              if (duplicateValues.isEmpty())
+              {
+                break;
+              }
+              else
+              {
+                StringBuilder buffer = new StringBuilder();
+                Iterator<AttributeValue> iterator =
+                  duplicateValues.iterator();
+                buffer.append(iterator.next().getStringValue());
+                while (iterator.hasNext())
+                {
+                  buffer.append(", ");
+                  buffer.append(iterator.next().getStringValue());
+                }
+
+                localOp.setResultCode(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS);
+
+                int msgID = MSGID_MODIFY_ADD_DUPLICATE_VALUE;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                    a.getName(),
+                    buffer.toString()));
+
+                break modifyProcessing;
+              }
+            }
+
+
+          case DELETE:
+            // Remove the specified attribute values or the entire attribute
+            // from the value.  If there are any specified values that were
+            // not present, then fail.  If the RDN attribute value would be
+            // removed, then fail.
+            LinkedList<AttributeValue> missingValues =
+              new LinkedList<AttributeValue>();
+            boolean attrExists =
+              modifiedEntry.removeAttribute(a, missingValues);
+
+            if (attrExists)
+            {
+              if (missingValues.isEmpty())
+              {
+                RDN rdn = modifiedEntry.getDN().getRDN();
+                if ((rdn !=  null) && rdn.hasAttributeType(t) &&
+                    (! modifiedEntry.hasValue(t, a.getOptions(),
+                        rdn.getAttributeValue(t))))
+                {
+                  localOp.setResultCode(ResultCode.NOT_ALLOWED_ON_RDN);
+
+                  int msgID = MSGID_MODIFY_DELETE_RDN_ATTR;
+                  localOp.appendErrorMessage(getMessage(msgID,
+                      String.valueOf(entryDN),
+                      a.getName()));
+                  break modifyProcessing;
+                }
+
+                break;
+              }
+              else
+              {
+                StringBuilder buffer = new StringBuilder();
+                Iterator<AttributeValue> iterator = missingValues.iterator();
+                buffer.append(iterator.next().getStringValue());
+                while (iterator.hasNext())
+                {
+                  buffer.append(", ");
+                  buffer.append(iterator.next().getStringValue());
+                }
+
+                localOp.setResultCode(ResultCode.NO_SUCH_ATTRIBUTE);
+
+                int msgID = MSGID_MODIFY_DELETE_MISSING_VALUES;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                    a.getName(),
+                    buffer.toString()));
+
+                break modifyProcessing;
+              }
+            }
+            else
+            {
+              localOp.setResultCode(ResultCode.NO_SUCH_ATTRIBUTE);
+
+              int msgID = MSGID_MODIFY_DELETE_NO_SUCH_ATTR;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, String.valueOf(entryDN),
+                  a.getName()));
+              break modifyProcessing;
+            }
+
+
+          case REPLACE:
+            // If it is the objectclass attribute, then treat that separately.
+            if (a.getAttributeType().isObjectClassType())
+            {
+              try
+              {
+                modifiedEntry.setObjectClasses(a.getValues());
+                break;
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResponseData(de);
+                break modifyProcessing;
+              }
+            }
+
+
+            // If the provided attribute does not have any values, then we
+            // will simply remove the attribute from the entry (if it exists).
+            if (! a.hasValue())
+            {
+              modifiedEntry.removeAttribute(t, a.getOptions());
+              RDN rdn = modifiedEntry.getDN().getRDN();
+              if ((rdn !=  null) && rdn.hasAttributeType(t) &&
+                  (! modifiedEntry.hasValue(t, a.getOptions(),
+                      rdn.getAttributeValue(t))))
+              {
+                localOp.setResultCode(ResultCode.NOT_ALLOWED_ON_RDN);
+
+                int msgID = MSGID_MODIFY_DELETE_RDN_ATTR;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                    a.getName()));
+                break modifyProcessing;
+              }
+              break;
+            }
+
+
+            // Make sure that all the new values are valid according to the
+            // associated syntax.
+            newValues = a.getValues();
+            if (DirectoryServer.checkSchema())
+            {
+              AcceptRejectWarn syntaxPolicy =
+                DirectoryServer.getSyntaxEnforcementPolicy();
+              AttributeSyntax syntax = t.getSyntax();
+
+              if (syntaxPolicy == AcceptRejectWarn.REJECT)
+              {
+                StringBuilder invalidReason = new StringBuilder();
+
+                for (AttributeValue v : newValues)
+                {
+                  if (! syntax.valueIsAcceptable(v.getValue(), invalidReason))
+                  {
+                    localOp.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+
+                    int msgID = MSGID_MODIFY_REPLACE_INVALID_SYNTAX;
+                    localOp.appendErrorMessage(getMessage(msgID,
+                        String.valueOf(entryDN),
+                        a.getName(),
+                        v.getStringValue(),
+                        invalidReason.toString()));
+
+                    break modifyProcessing;
+                  }
+                }
+              }
+              else if (syntaxPolicy == AcceptRejectWarn.WARN)
+              {
+                StringBuilder invalidReason = new StringBuilder();
+
+                for (AttributeValue v : newValues)
+                {
+                  if (! syntax.valueIsAcceptable(v.getValue(), invalidReason))
+                  {
+                    localOp.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+
+                    int msgID = MSGID_MODIFY_REPLACE_INVALID_SYNTAX;
+                    logError(ErrorLogCategory.SCHEMA,
+                        ErrorLogSeverity.SEVERE_WARNING, msgID,
+                        String.valueOf(entryDN), a.getName(),
+                        v.getStringValue(), invalidReason.toString());
+
+                    invalidReason = new StringBuilder();
+                  }
+                }
+              }
+            }
+
+
+            // If the provided attribute does not have any options, then we
+            // will simply use it in place of any existing attribute of the
+            // provided type (or add it if it doesn't exist).
+            if (! a.hasOptions())
+            {
+              List<Attribute> attrList = new ArrayList<Attribute>(1);
+              attrList.add(a);
+              modifiedEntry.putAttribute(t, attrList);
+
+              RDN rdn = modifiedEntry.getDN().getRDN();
+              if ((rdn !=  null) && rdn.hasAttributeType(t) &&
+                  (! modifiedEntry.hasValue(t, a.getOptions(),
+                      rdn.getAttributeValue(t))))
+              {
+                localOp.setResultCode(ResultCode.NOT_ALLOWED_ON_RDN);
+
+                int msgID = MSGID_MODIFY_DELETE_RDN_ATTR;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                    a.getName()));
+                break modifyProcessing;
+              }
+              break;
+            }
+
+
+            // See if there is an existing attribute of the provided type.  If
+            // not, then we'll use the new one.
+            List<Attribute> attrList = modifiedEntry.getAttribute(t);
+            if ((attrList == null) || attrList.isEmpty())
+            {
+              attrList = new ArrayList<Attribute>(1);
+              attrList.add(a);
+              modifiedEntry.putAttribute(t, attrList);
+
+              RDN rdn = modifiedEntry.getDN().getRDN();
+              if ((rdn !=  null) && rdn.hasAttributeType(t) &&
+                  (! modifiedEntry.hasValue(t, a.getOptions(),
+                      rdn.getAttributeValue(t))))
+              {
+                localOp.setResultCode(ResultCode.NOT_ALLOWED_ON_RDN);
+
+                int msgID = MSGID_MODIFY_DELETE_RDN_ATTR;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                    a.getName()));
+                break modifyProcessing;
+              }
+              break;
+            }
+
+
+            // There must be an existing occurrence of the provided attribute
+            // in the entry.  If there is a version with exactly the set of
+            // options provided, then replace it.  Otherwise, add a new one.
+            boolean found = false;
+            for (int i=0; i < attrList.size(); i++)
+            {
+              if (attrList.get(i).optionsEqual(a.getOptions()))
+              {
+                attrList.set(i, a);
+                found = true;
+                break;
+              }
+            }
+
+            if (! found)
+            {
+              attrList.add(a);
+            }
+
+            RDN rdn = modifiedEntry.getDN().getRDN();
+            if ((rdn !=  null) && rdn.hasAttributeType(t) &&
+                (! modifiedEntry.hasValue(t, a.getOptions(),
+                    rdn.getAttributeValue(t))))
+            {
+              localOp.setResultCode(ResultCode.NOT_ALLOWED_ON_RDN);
+
+              int msgID = MSGID_MODIFY_DELETE_RDN_ATTR;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, String.valueOf(entryDN),
+                  a.getName()));
+              break modifyProcessing;
+            }
+            break;
+
+
+          case INCREMENT:
+            // The specified attribute type must not be an RDN attribute.
+            rdn = modifiedEntry.getDN().getRDN();
+            if ((rdn !=  null) && rdn.hasAttributeType(t))
+            {
+              localOp.setResultCode(ResultCode.NOT_ALLOWED_ON_RDN);
+              localOp.appendErrorMessage(getMessage(MSGID_MODIFY_INCREMENT_RDN,
+                  String.valueOf(entryDN),
+                  a.getName()));
+            }
+
+
+            // The provided attribute must have a single value, and it must be
+            // an integer.
+            LinkedHashSet<AttributeValue> values = a.getValues();
+            if ((values == null) || values.isEmpty())
+            {
+              localOp.setResultCode(ResultCode.PROTOCOL_ERROR);
+
+              int msgID = MSGID_MODIFY_INCREMENT_REQUIRES_VALUE;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, String.valueOf(entryDN),
+                  a.getName()));
+
+              break modifyProcessing;
+            }
+            else if (values.size() > 1)
+            {
+              localOp.setResultCode(ResultCode.PROTOCOL_ERROR);
+
+              int msgID = MSGID_MODIFY_INCREMENT_REQUIRES_SINGLE_VALUE;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, String.valueOf(entryDN),
+                  a.getName()));
+              break modifyProcessing;
+            }
+
+            AttributeValue v = values.iterator().next();
+
+            long incrementValue;
+            try
+            {
+              incrementValue = Long.parseLong(v.getNormalizedStringValue());
+            }
+            catch (Exception e)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, e);
+              }
+
+              localOp.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+
+              int msgID = MSGID_MODIFY_INCREMENT_PROVIDED_VALUE_NOT_INTEGER;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, String.valueOf(entryDN),
+                  a.getName(), v.getStringValue()));
+
+              break modifyProcessing;
+            }
+
+
+            // Get the corresponding attribute from the entry and make sure
+            // that it has a single integer value.
+            attrList = modifiedEntry.getAttribute(t, a.getOptions());
+            if ((attrList == null) || attrList.isEmpty())
+            {
+              localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+
+              int msgID = MSGID_MODIFY_INCREMENT_REQUIRES_EXISTING_VALUE;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, String.valueOf(entryDN),
+                  a.getName()));
+
+              break modifyProcessing;
+            }
+
+            boolean updated = false;
+            for (Attribute attr : attrList)
+            {
+              LinkedHashSet<AttributeValue> valueList = attr.getValues();
+              if ((valueList == null) || valueList.isEmpty())
+              {
+                continue;
+              }
+
+              LinkedHashSet<AttributeValue> newValueList =
+                new LinkedHashSet<AttributeValue>(valueList.size());
+              for (AttributeValue existingValue : valueList)
+              {
+                long newIntValue;
+                try
+                {
+                  long existingIntValue =
+                    Long.parseLong(existingValue.getStringValue());
+                  newIntValue = existingIntValue + incrementValue;
+                }
+                catch (Exception e)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                  }
+
+                  localOp.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+
+                  int msgID = MSGID_MODIFY_INCREMENT_REQUIRES_INTEGER_VALUE;
+                  localOp.appendErrorMessage(getMessage(msgID,
+                      String.valueOf(entryDN),
+                      a.getName(),
+                      existingValue.getStringValue()));
+                  break modifyProcessing;
+                }
+
+                ByteString newValue =
+                  new ASN1OctetString(String.valueOf(newIntValue));
+                newValueList.add(new AttributeValue(t, newValue));
+              }
+
+              attr.setValues(newValueList);
+              updated = true;
+            }
+
+            if (! updated)
+            {
+              localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+
+              int msgID = MSGID_MODIFY_INCREMENT_REQUIRES_EXISTING_VALUE;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, String.valueOf(entryDN),
+                  a.getName()));
+
+              break modifyProcessing;
+            }
+
+            break;
+
+          default:
+          }
+        }
+
+
+        // If there was a password change, then perform any additional checks
+        // that may be necessary.
+        if (passwordChanged)
+        {
+          // If it was a self change, then see if the current password was
+          // provided and handle accordingly.
+          if (selfChange &&
+              pwPolicyState.getPolicy().requireCurrentPassword() &&
+              (! currentPasswordProvided))
+          {
+            localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+            int msgID = MSGID_MODIFY_PW_CHANGE_REQUIRES_CURRENT_PW;
+            localOp.appendErrorMessage(getMessage(msgID));
+            break modifyProcessing;
+          }
+
+
+          // If this change would result in multiple password values, then see
+          // if that's OK.
+          if ((numPasswords > 1) &&
+              (! pwPolicyState.getPolicy().allowMultiplePasswordValues()))
+          {
+            localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+            int msgID = MSGID_MODIFY_MULTIPLE_PASSWORDS_NOT_ALLOWED;
+            localOp.appendErrorMessage(getMessage(msgID));
+            break modifyProcessing;
+          }
+
+
+          // If any of the password values should be validated, then do so now.
+          if (selfChange ||
+              (! pwPolicyState.getPolicy().skipValidationForAdministrators()))
+          {
+            List<AttributeValue> newPasswords =
+              localOp.getNewPasswords();
+            List<AttributeValue> currentPasswords =
+              localOp.getCurrentPasswords();
+
+            if (newPasswords != null)
+            {
+              HashSet<ByteString> clearPasswords = new HashSet<ByteString>();
+              clearPasswords.addAll(pwPolicyState.getClearPasswords());
+
+              if (currentPasswords != null)
+              {
+                if (clearPasswords.isEmpty())
+                {
+                  for (AttributeValue v : currentPasswords)
+                  {
+                    clearPasswords.add(v.getValue());
+                  }
+                }
+                else
+                {
+                  // NOTE:  We can't rely on the fact that Set doesn't allow
+                  // duplicates because technically it's possible that the
+                  // values aren't duplicates if they are ASN.1 elements with
+                  // different types (like 0x04 for a standard universal octet
+                  // string type versus 0x80 for a simple password in a bind
+                  // operation).  So we have to manually check for duplicates.
+                  for (AttributeValue v : currentPasswords)
+                  {
+                    ByteString pw = v.getValue();
+
+                    boolean found = false;
+                    for (ByteString s : clearPasswords)
+                    {
+                      if (Arrays.equals(s.value(), pw.value()))
+                      {
+                        found = true;
+                        break;
+                      }
+                    }
+
+                    if (! found)
+                    {
+                      clearPasswords.add(pw);
+                    }
+                  }
+                }
+              }
+
+              for (AttributeValue v : newPasswords)
+              {
+                StringBuilder invalidReason = new StringBuilder();
+                if (! pwPolicyState.passwordIsAcceptable(localOp, modifiedEntry,
+                    v.getValue(),
+                    clearPasswords,
+                    invalidReason))
+                {
+                  localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                  int msgID = MSGID_MODIFY_PW_VALIDATION_FAILED;
+                  localOp.appendErrorMessage(getMessage(msgID,
+                      invalidReason.toString()));
+                  break modifyProcessing;
+                }
+              }
+            }
+          }
+        }
+
+
+        // Check to see if the client has permission to perform the
+        // modify.
+        // The access control check is not made any earlier because the
+        // handler needs access to the modified entry.
+
+        // FIXME: for now assume that this will check all permission
+        // pertinent to the operation. This includes proxy authorization
+        // and any other controls specified.
+
+        // FIXME: earlier checks to see if the entry already exists may
+        // have already exposed sensitive information to the client.
+        if (!AccessControlConfigManager.getInstance()
+            .getAccessControlHandler().isAllowed(localOp)) {
+          localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+          int msgID = MSGID_MODIFY_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+          localOp.appendErrorMessage(
+              getMessage(msgID, String.valueOf(entryDN)));
+
+          skipPostOperation = true;
+          break modifyProcessing;
+        }
+
+        boolean wasLocked = false;
+        if (passwordChanged)
+        {
+          // See if the account was locked for any reason.
+          wasLocked = pwPolicyState.lockedDueToIdleInterval() ||
+          pwPolicyState.lockedDueToMaximumResetAge() ||
+          pwPolicyState.lockedDueToFailures();
+
+          // Update the password policy state attributes in the user's entry.
+          // If the modification fails, then these changes won't be applied.
+          pwPolicyState.setPasswordChangedTime();
+          pwPolicyState.clearFailureLockout();
+          pwPolicyState.clearGraceLoginTimes();
+          pwPolicyState.clearWarnedTime();
+
+          if (pwPolicyState.getPolicy().forceChangeOnAdd() ||
+              pwPolicyState.getPolicy().forceChangeOnReset())
+          {
+            if (selfChange)
+            {
+              pwPolicyState.setMustChangePassword(false);
+            }
+            else
+            {
+              pwPolicyState.setMustChangePassword(
+                   pwPolicyState.getPolicy().forceChangeOnReset());
+            }
+          }
+
+          if (pwPolicyState.getPolicy().getRequireChangeByTime() > 0)
+          {
+            pwPolicyState.setRequiredChangeTime();
+          }
+          modifications.addAll(pwPolicyState.getModifications());
+          //Apply pwd Policy modifications to modified entry.
+          try {
+            modifiedEntry.applyModifications(pwPolicyState.getModifications());
+          } catch (DirectoryException e) {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, e);
+            }
+
+            localOp.setResponseData(e);
+            break modifyProcessing;
+          }
+        }
+        else if((! localOp.isInternalOperation()) &&
+            pwPolicyState.mustChangePassword())
+        {
+            // The user will not be allowed to do anything else before
+            // the password gets changed.
+            localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+            int msgID = MSGID_MODIFY_MUST_CHANGE_PASSWORD;
+            localOp.appendErrorMessage(getMessage(msgID));
+            break modifyProcessing;
+        }
+
+        // Make sure that the new entry is valid per the server schema.
+        if (DirectoryServer.checkSchema())
+        {
+          StringBuilder invalidReason = new StringBuilder();
+          if (! modifiedEntry.conformsToSchema(null, false, false, false,
+              invalidReason))
+          {
+            localOp.setResultCode(ResultCode.OBJECTCLASS_VIOLATION);
+            localOp.appendErrorMessage(getMessage(MSGID_MODIFY_VIOLATES_SCHEMA,
+                String.valueOf(entryDN),
+                invalidReason.toString()));
+            break modifyProcessing;
+          }
+        }
+
+
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+        // If the operation is not a synchronization operation,
+        // Invoke the pre-operation modify plugins.
+        if (!localOp.isSynchronizationOperation())
+        {
+          PreOperationPluginResult preOpResult =
+            pluginConfigManager.invokePreOperationModifyPlugins(localOp);
+          if (preOpResult.connectionTerminated())
+          {
+            // There's no point in continuing with anything.  Log the result
+            // and return.
+            localOp.setResultCode(ResultCode.CANCELED);
+
+            int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+            localOp.appendErrorMessage(getMessage(msgID));
+
+            localOp.setProcessingStopTime();
+
+            return;
+          }
+          else if (preOpResult.sendResponseImmediately())
+          {
+            skipPostOperation = true;
+            break modifyProcessing;
+          }
+          else if (preOpResult.skipCoreProcessing())
+          {
+            skipPostOperation = false;
+            break modifyProcessing;
+          }
+        }
+
+
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+
+        // Actually perform the modify operation.  This should also include
+        // taking care of any synchronization that might be needed.
+        if (backend == null)
+        {
+          localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+          localOp.appendErrorMessage(
+              getMessage(MSGID_MODIFY_NO_BACKEND_FOR_ENTRY,
+              String.valueOf(entryDN)));
+          break modifyProcessing;
+        }
+
+        try
+        {
+          // If it is not a private backend, then check to see if the server or
+          // backend is operating in read-only mode.
+          if (! backend.isPrivateBackend())
+          {
+            switch (DirectoryServer.getWritabilityMode())
+            {
+            case DISABLED:
+              localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+              localOp.appendErrorMessage(
+                  getMessage(MSGID_MODIFY_SERVER_READONLY,
+                  String.valueOf(entryDN)));
+              break modifyProcessing;
+
+            case INTERNAL_ONLY:
+              if (! (localOp.isInternalOperation() ||
+                  localOp.isSynchronizationOperation()))
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                localOp.appendErrorMessage(
+                    getMessage(MSGID_MODIFY_SERVER_READONLY,
+                    String.valueOf(entryDN)));
+                break modifyProcessing;
+              }
+            }
+
+            switch (backend.getWritabilityMode())
+            {
+            case DISABLED:
+              localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+              localOp.appendErrorMessage(
+                  getMessage(MSGID_MODIFY_BACKEND_READONLY,
+                  String.valueOf(entryDN)));
+              break modifyProcessing;
+
+            case INTERNAL_ONLY:
+              if (! localOp.isInternalOperation() ||
+                  localOp.isSynchronizationOperation())
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                localOp.appendErrorMessage(
+                    getMessage(MSGID_MODIFY_BACKEND_READONLY,
+                    String.valueOf(entryDN)));
+                break modifyProcessing;
+              }
+            }
+          }
+
+
+          if (noOp)
+          {
+            localOp.appendErrorMessage(getMessage(MSGID_MODIFY_NOOP));
+
+            // FIXME -- We must set a result code other than SUCCESS.
+          }
+          else
+          {
+            for (SynchronizationProvider provider :
+              DirectoryServer.getSynchronizationProviders())
+            {
+              try
+              {
+                SynchronizationProviderResult result =
+                  provider.doPreOperation(localOp);
+                if (! result.continueOperationProcessing())
+                {
+                  break modifyProcessing;
+                }
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                logError(ErrorLogCategory.SYNCHRONIZATION,
+                    ErrorLogSeverity.SEVERE_ERROR,
+                    MSGID_MODIFY_SYNCH_PREOP_FAILED,
+                    localOp.getConnectionID(),
+                    localOp.getOperationID(),
+                    getExceptionMessage(de));
+
+                localOp.setResponseData(de);
+                break modifyProcessing;
+              }
+            }
+
+            backend.replaceEntry(modifiedEntry, localOp);
+
+
+            // If the modification was successful, then see if there's any other
+            // work that we need to do here before handing off to postop
+            // plugins.
+            if (passwordChanged)
+            {
+              if (selfChange)
+              {
+                AuthenticationInfo authInfo =
+                  clientConnection.getAuthenticationInfo();
+                if (authInfo.getAuthenticationDN().equals(entryDN))
+                {
+                  clientConnection.setMustChangePassword(false);
+                }
+
+                int    msgID   = MSGID_MODIFY_PASSWORD_CHANGED;
+                String message = getMessage(msgID);
+                pwPolicyState.generateAccountStatusNotification(
+                    AccountStatusNotificationType.PASSWORD_CHANGED, entryDN,
+                    msgID, message);
+              }
+              else
+              {
+                int    msgID   = MSGID_MODIFY_PASSWORD_RESET;
+                String message = getMessage(msgID);
+                pwPolicyState.generateAccountStatusNotification(
+                    AccountStatusNotificationType.PASSWORD_RESET, entryDN,
+                    msgID, message);
+              }
+            }
+
+            if (enabledStateChanged)
+            {
+              if (isEnabled)
+              {
+                int    msgID   = MSGID_MODIFY_ACCOUNT_ENABLED;
+                String message = getMessage(msgID);
+                pwPolicyState.generateAccountStatusNotification(
+                    AccountStatusNotificationType.ACCOUNT_ENABLED, entryDN,
+                    msgID, message);
+              }
+              else
+              {
+                int    msgID   = MSGID_MODIFY_ACCOUNT_DISABLED;
+                String message = getMessage(msgID);
+                pwPolicyState.generateAccountStatusNotification(
+                    AccountStatusNotificationType.ACCOUNT_DISABLED, entryDN,
+                    msgID, message);
+              }
+            }
+
+            if (wasLocked)
+            {
+              int    msgID   = MSGID_MODIFY_ACCOUNT_UNLOCKED;
+              String message = getMessage(msgID);
+              pwPolicyState.generateAccountStatusNotification(
+                  AccountStatusNotificationType.ACCOUNT_UNLOCKED, entryDN,
+                  msgID, message);
+            }
+          }
+
+          if (preReadRequest != null)
+          {
+            Entry entry = currentEntry.duplicate(true);
+
+            if (! preReadRequest.allowsAttribute(
+                DirectoryServer.getObjectClassAttributeType()))
+            {
+              entry.removeAttribute(
+                  DirectoryServer.getObjectClassAttributeType());
+            }
+
+            if (! preReadRequest.returnAllUserAttributes())
+            {
+              Iterator<AttributeType> iterator =
+                entry.getUserAttributes().keySet().iterator();
+              while (iterator.hasNext())
+              {
+                AttributeType attrType = iterator.next();
+                if (! preReadRequest.allowsAttribute(attrType))
+                {
+                  iterator.remove();
+                }
+              }
+            }
+
+            if (! preReadRequest.returnAllOperationalAttributes())
+            {
+              Iterator<AttributeType> iterator =
+                entry.getOperationalAttributes().keySet().iterator();
+              while (iterator.hasNext())
+              {
+                AttributeType attrType = iterator.next();
+                if (! preReadRequest.allowsAttribute(attrType))
+                {
+                  iterator.remove();
+                }
+              }
+            }
+
+            // FIXME -- Check access controls on the entry to see if it should
+            //          be returned or if any attributes need to be stripped
+            //          out..
+            SearchResultEntry searchEntry = new SearchResultEntry(entry);
+            LDAPPreReadResponseControl responseControl =
+              new LDAPPreReadResponseControl(preReadRequest.getOID(),
+                  preReadRequest.isCritical(),
+                  searchEntry);
+
+            localOp.getResponseControls().add(responseControl);
+          }
+
+          if (postReadRequest != null)
+          {
+            Entry entry = modifiedEntry.duplicate(true);
+
+            if (! postReadRequest.allowsAttribute(
+                DirectoryServer.getObjectClassAttributeType()))
+            {
+              entry.removeAttribute(
+                  DirectoryServer.getObjectClassAttributeType());
+            }
+
+            if (! postReadRequest.returnAllUserAttributes())
+            {
+              Iterator<AttributeType> iterator =
+                entry.getUserAttributes().keySet().iterator();
+              while (iterator.hasNext())
+              {
+                AttributeType attrType = iterator.next();
+                if (! postReadRequest.allowsAttribute(attrType))
+                {
+                  iterator.remove();
+                }
+              }
+            }
+
+            if (! postReadRequest.returnAllOperationalAttributes())
+            {
+              Iterator<AttributeType> iterator =
+                entry.getOperationalAttributes().keySet().iterator();
+              while (iterator.hasNext())
+              {
+                AttributeType attrType = iterator.next();
+                if (! postReadRequest.allowsAttribute(attrType))
+                {
+                  iterator.remove();
+                }
+              }
+            }
+
+            // FIXME -- Check access controls on the entry to see if it should
+            //          be returned or if any attributes need to be stripped
+            //          out..
+            SearchResultEntry searchEntry = new SearchResultEntry(entry);
+            LDAPPostReadResponseControl responseControl =
+              new LDAPPostReadResponseControl(postReadRequest.getOID(),
+                  postReadRequest.isCritical(),
+                  searchEntry);
+
+            localOp.getResponseControls().add(responseControl);
+          }
+
+          localOp.setResultCode(ResultCode.SUCCESS);
+        }
+        catch (DirectoryException de)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, de);
+          }
+
+          localOp.setResultCode(de.getResultCode());
+          localOp.appendErrorMessage(de.getErrorMessage());
+          localOp.setMatchedDN(de.getMatchedDN());
+          localOp.setReferralURLs(de.getReferralURLs());
+
+          break modifyProcessing;
+        }
+        catch (CancelledOperationException coe)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, coe);
+          }
+
+          CancelResult cancelResult = coe.getCancelResult();
+
+          localOp.setCancelResult(cancelResult);
+          localOp.setResultCode(cancelResult.getResultCode());
+
+          String message = coe.getMessage();
+          if ((message != null) && (message.length() > 0))
+          {
+            localOp.appendErrorMessage(message);
+          }
+
+          break modifyProcessing;
+        }
+      }
+      finally
+      {
+        LockManager.unlock(entryDN, entryLock);
+
+        for (SynchronizationProvider provider :
+          DirectoryServer.getSynchronizationProviders())
+        {
+          try
+          {
+            provider.doPostOperation(localOp);
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            logError(ErrorLogCategory.SYNCHRONIZATION,
+                ErrorLogSeverity.SEVERE_ERROR,
+                MSGID_MODIFY_SYNCH_POSTOP_FAILED, localOp.getConnectionID(),
+                localOp.getOperationID(), getExceptionMessage(de));
+
+            localOp.setResponseData(de);
+            break;
+          }
+        }
+      }
+    }
+
+
+    // Indicate that it is now too late to attempt to cancel the operation.
+    localOp.setCancelResult(CancelResult.TOO_LATE);
+
+    // Invoke the post-operation modify plugins.
+    if (! skipPostOperation)
+    {
+      // FIXME -- Should this also be done while holding the locks?
+      PostOperationPluginResult postOpResult =
+        pluginConfigManager.invokePostOperationModifyPlugins(localOp);
+      if (postOpResult.connectionTerminated())
+      {
+        // There's no point in continuing with anything.  Log the result and
+        // return.
+        localOp.setResultCode(ResultCode.CANCELED);
+
+        int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+        localOp.appendErrorMessage(getMessage(msgID));
+
+        localOp.setProcessingStopTime();
+
+        return;
+      }
+    }
+
+    // Notify any change notification listeners that might be registered with
+    // the server.
+    if (localOp.getResultCode() == ResultCode.SUCCESS)
+    {
+      for (ChangeNotificationListener changeListener :
+           DirectoryServer.getChangeNotificationListeners())
+      {
+        try
+        {
+          changeListener.handleModifyOperation(localOp,
+              localOp.getCurrentEntry(),
+              localOp.getModifiedEntry());
+        }
+        catch (Exception e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+
+          int    msgID   = MSGID_MODIFY_ERROR_NOTIFYING_CHANGE_LISTENER;
+          String message = getMessage(msgID, getExceptionMessage(e));
+          logError(ErrorLogCategory.CORE_SERVER, ErrorLogSeverity.SEVERE_ERROR,
+                   message, msgID);
+        }
+      }
+    }
+
+
+
+    // Stop the processing timer.
+    localOp.setProcessingStopTime();
+  }
+
+  /**
+   * Perform a search operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  public void processSearch(SearchOperation operation)
+  {
+
+    LocalBackendSearchOperation localOp =
+      new LocalBackendSearchOperation(operation);
+
+    PersistentSearch persistentSearch = null;
+
+    ClientConnection clientConnection = localOp.getClientConnection();
+
+    // Get the plugin config manager that will be used for invoking plugins.
+    PluginConfigManager pluginConfigManager =
+      DirectoryServer.getPluginConfigManager();
+    boolean skipPostOperation = false;
+
+    // Create a labeled block of code that we can break out of if a problem is
+    // detected.
+    searchProcessing:
+    {
+      // Process the search base and filter to convert them from their raw forms
+      // as provided by the client to the forms required for the rest of the
+      // search processing.
+      DN baseDN = localOp.getBaseDN();
+      SearchFilter filter = localOp.getFilter();
+
+      if ((baseDN == null) || (filter == null)){
+        break searchProcessing;
+      }
+
+      // Check to see if there are any controls in the request.  If so, then
+      // see if there is any special processing required.
+      boolean       processSearch    = true;
+      List<Control> requestControls  = localOp.getRequestControls();
+      if ((requestControls != null) && (! requestControls.isEmpty()))
+      {
+        for (int i=0; i < requestControls.size(); i++)
+        {
+          Control c   = requestControls.get(i);
+          String  oid = c.getOID();
+
+          if (oid.equals(OID_LDAP_ASSERTION))
+          {
+            LDAPAssertionRequestControl assertControl;
+            if (c instanceof LDAPAssertionRequestControl)
+            {
+              assertControl = (LDAPAssertionRequestControl) c;
+            }
+            else
+            {
+              try
+              {
+                assertControl = LDAPAssertionRequestControl.decodeControl(c);
+                requestControls.set(i, assertControl);
+              }
+              catch (LDAPException le)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                }
+
+                localOp.setResultCode(
+                    ResultCode.valueOf(le.getResultCode()));
+                localOp.appendErrorMessage(le.getMessage());
+
+                break searchProcessing;
+              }
+            }
+
+            try
+            {
+              // FIXME -- We need to determine whether the current user has
+              //          permission to make this determination.
+              SearchFilter assertionFilter = assertControl.getSearchFilter();
+              Entry entry;
+              try
+              {
+                entry = DirectoryServer.getEntry(baseDN);
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(de.getResultCode());
+
+                int msgID = MSGID_SEARCH_CANNOT_GET_ENTRY_FOR_ASSERTION;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, de.getErrorMessage()));
+
+                break searchProcessing;
+              }
+
+              if (entry == null)
+              {
+                localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+
+                int msgID = MSGID_SEARCH_NO_SUCH_ENTRY_FOR_ASSERTION;
+                localOp.appendErrorMessage(getMessage(msgID));
+
+                break searchProcessing;
+              }
+
+
+              if (! assertionFilter.matchesEntry(entry))
+              {
+                localOp.setResultCode(ResultCode.ASSERTION_FAILED);
+
+                localOp.appendErrorMessage(
+                    getMessage(MSGID_SEARCH_ASSERTION_FAILED));
+
+                break searchProcessing;
+              }
+            }
+            catch (DirectoryException de)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+
+              localOp.setResultCode(ResultCode.PROTOCOL_ERROR);
+
+              int msgID = MSGID_SEARCH_CANNOT_PROCESS_ASSERTION_FILTER;
+              localOp.appendErrorMessage(
+                  getMessage(msgID, de.getErrorMessage()));
+
+              break searchProcessing;
+            }
+          }
+          else if (oid.equals(OID_PROXIED_AUTH_V1))
+          {
+            // The requester must have the PROXIED_AUTH privilige in order to be
+            // able to use this control.
+            if (! clientConnection.hasPrivilege(
+                Privilege.PROXIED_AUTH, localOp))
+            {
+              int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+              localOp.appendErrorMessage(getMessage(msgID));
+              localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+              break searchProcessing;
+            }
+
+
+            ProxiedAuthV1Control proxyControl;
+            if (c instanceof ProxiedAuthV1Control)
+            {
+              proxyControl = (ProxiedAuthV1Control) c;
+            }
+            else
+            {
+              try
+              {
+                proxyControl = ProxiedAuthV1Control.decodeControl(c);
+              }
+              catch (LDAPException le)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                }
+
+                localOp.setResultCode(
+                    ResultCode.valueOf(le.getResultCode()));
+                localOp.appendErrorMessage(le.getMessage());
+
+                break searchProcessing;
+              }
+            }
+
+
+            Entry authorizationEntry;
+            try
+            {
+              authorizationEntry = proxyControl.getAuthorizationEntry();
+            }
+            catch (DirectoryException de)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+
+              localOp.setResultCode(de.getResultCode());
+              localOp.appendErrorMessage(de.getErrorMessage());
+
+              break searchProcessing;
+            }
+
+            if (AccessControlConfigManager.getInstance().
+                getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                    authorizationEntry) == false) {
+              localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+              int msgID = MSGID_SEARCH_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+              localOp.appendErrorMessage(getMessage(msgID,
+                  String.valueOf(baseDN)));
+
+              skipPostOperation = true;
+              break searchProcessing;
+            }
+            localOp.setAuthorizationEntry(authorizationEntry);
+            if (authorizationEntry == null)
+            {
+              localOp.setProxiedAuthorizationDN(DN.nullDN());
+            }
+            else
+            {
+              localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+            }
+          }
+          else if (oid.equals(OID_PROXIED_AUTH_V2))
+          {
+            // The requester must have the PROXIED_AUTH privilige in order to be
+            // able to use this control.
+            if (! clientConnection.hasPrivilege(
+                Privilege.PROXIED_AUTH, localOp))
+            {
+              int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+              localOp.appendErrorMessage(getMessage(msgID));
+              localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+              break searchProcessing;
+            }
+
+
+            ProxiedAuthV2Control proxyControl;
+            if (c instanceof ProxiedAuthV2Control)
+            {
+              proxyControl = (ProxiedAuthV2Control) c;
+            }
+            else
+            {
+              try
+              {
+                proxyControl = ProxiedAuthV2Control.decodeControl(c);
+              }
+              catch (LDAPException le)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                }
+
+                localOp.setResultCode(
+                    ResultCode.valueOf(le.getResultCode()));
+                localOp.appendErrorMessage(le.getMessage());
+
+                break searchProcessing;
+              }
+            }
+
+
+            Entry authorizationEntry;
+            try
+            {
+              authorizationEntry = proxyControl.getAuthorizationEntry();
+            }
+            catch (DirectoryException de)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+
+              localOp.setResultCode(de.getResultCode());
+              localOp.appendErrorMessage(de.getErrorMessage());
+
+              break searchProcessing;
+            }
+
+            if (AccessControlConfigManager.getInstance().
+                getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                    authorizationEntry) == false) {
+              localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+              int msgID = MSGID_SEARCH_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+              localOp.appendErrorMessage(getMessage(msgID,
+                  String.valueOf(baseDN)));
+
+              skipPostOperation = true;
+              break searchProcessing;
+            }
+            localOp.setAuthorizationEntry(authorizationEntry);
+            if (authorizationEntry == null)
+            {
+              localOp.setProxiedAuthorizationDN(DN.nullDN());
+            }
+            else
+            {
+              localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+            }
+          }
+          else if (oid.equals(OID_PERSISTENT_SEARCH))
+          {
+            PersistentSearchControl psearchControl;
+            if (c instanceof PersistentSearchControl)
+            {
+              psearchControl = (PersistentSearchControl) c;
+            }
+            else
+            {
+              try
+              {
+                psearchControl = PersistentSearchControl.decodeControl(c);
+              }
+              catch (LDAPException le)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                }
+
+                localOp.setResultCode(
+                    ResultCode.valueOf(le.getResultCode()));
+                localOp.appendErrorMessage(le.getMessage());
+
+                break searchProcessing;
+              }
+            }
+
+            persistentSearch =
+              new PersistentSearch(operation, psearchControl.getChangeTypes(),
+                  psearchControl.getReturnECs());
+            localOp.setPersistentSearch(persistentSearch);
+
+            // If we're only interested in changes, then we don't actually want
+            // to process the search now.
+            if (psearchControl.getChangesOnly())
+            {
+              processSearch = false;
+            }
+          }
+          else if (oid.equals(OID_LDAP_SUBENTRIES))
+          {
+            localOp.setReturnLDAPSubentries(true);
+          }
+          else if (oid.equals(OID_MATCHED_VALUES))
+          {
+            if (c instanceof MatchedValuesControl)
+            {
+              localOp.setMatchedValuesControl((MatchedValuesControl) c);
+            }
+            else
+            {
+              try
+              {
+                MatchedValuesControl matchedValuesControl =
+                  MatchedValuesControl.decodeControl(c);
+                localOp.setMatchedValuesControl(matchedValuesControl);
+              }
+              catch (LDAPException le)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                }
+
+                localOp.setResultCode(
+                    ResultCode.valueOf(le.getResultCode()));
+                localOp.appendErrorMessage(le.getMessage());
+
+                break searchProcessing;
+              }
+            }
+          }
+          else if (oid.equals(OID_ACCOUNT_USABLE_CONTROL))
+          {
+            localOp.setIncludeUsableControl(true);
+          }
+          else if (oid.equals(OID_REAL_ATTRS_ONLY))
+          {
+            localOp.setRealAttributesOnly(true);
+          }
+          else if (oid.equals(OID_VIRTUAL_ATTRS_ONLY))
+          {
+            localOp.setVirtualAttributesOnly(true);
+          }
+          else if(oid.equals(OID_GET_EFFECTIVE_RIGHTS))
+          {
+            GetEffectiveRights effectiveRightsControl;
+            if (c instanceof GetEffectiveRights)
+            {
+              effectiveRightsControl = (GetEffectiveRights) c;
+            }
+            else
+            {
+              try
+              {
+                effectiveRightsControl = GetEffectiveRights.decodeControl(c);
+              }
+              catch (LDAPException le)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                }
+
+                localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                localOp.appendErrorMessage(le.getMessage());
+
+                break searchProcessing;
+              }
+            }
+
+              if (!AccessControlConfigManager.getInstance()
+                   .getAccessControlHandler().
+                    isGetEffectiveRightsAllowed(localOp,
+                        effectiveRightsControl)) {
+                localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+                 int msgID =
+                        MSGID_SEARCH_EFFECTIVERIGHTS_INSUFFICIENT_ACCESS_RIGHTS;
+                 localOp.appendErrorMessage(getMessage(msgID,
+                     String.valueOf(baseDN)));
+
+                 skipPostOperation = true;
+                 break searchProcessing;
+               }
+          }
+
+          // NYI -- Add support for additional controls.
+          else if (c.isCritical())
+          {
+            if ((backend == null) || (! backend.supportsControl(oid)))
+            {
+              localOp.setResultCode(
+                  ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+
+              int msgID = MSGID_SEARCH_UNSUPPORTED_CRITICAL_CONTROL;
+              localOp.appendErrorMessage(getMessage(msgID, oid));
+
+              break searchProcessing;
+            }
+          }
+        }
+      }
+
+
+      // Check to see if the client has permission to perform the
+      // search.
+
+      // FIXME: for now assume that this will check all permission
+      // pertinent to the operation. This includes proxy authorization
+      // and any other controls specified.
+      if (AccessControlConfigManager.getInstance()
+          .getAccessControlHandler().isAllowed(localOp) == false) {
+        localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+        int msgID = MSGID_SEARCH_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+        localOp.appendErrorMessage(
+            getMessage(msgID, String.valueOf(baseDN)));
+
+        skipPostOperation = true;
+        break searchProcessing;
+      }
+
+      // Check for and handle a request to cancel this operation.
+      if (localOp.getCancelRequest() != null)
+      {
+        localOp.indicateCancelled(localOp.getCancelRequest());
+        localOp.setProcessingStopTime();
+        return;
+      }
+
+
+      // Invoke the pre-operation search plugins.
+      PreOperationPluginResult preOpResult =
+        pluginConfigManager.invokePreOperationSearchPlugins(localOp);
+      if (preOpResult.connectionTerminated())
+      {
+        // There's no point in continuing with anything.  Log the request and
+        // result and return.
+        localOp.setResultCode(ResultCode.CANCELED);
+
+        int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+        localOp.appendErrorMessage(getMessage(msgID));
+
+        localOp.setProcessingStopTime();
+        return;
+      }
+      else if (preOpResult.sendResponseImmediately())
+      {
+        skipPostOperation = true;
+        break searchProcessing;
+      }
+      else if (preOpResult.skipCoreProcessing())
+      {
+        skipPostOperation = false;
+        break searchProcessing;
+      }
+
+
+      // Check for and handle a request to cancel this operation.
+      if (localOp.getCancelRequest() != null)
+      {
+        localOp.indicateCancelled(localOp.getCancelRequest());
+        localOp.setProcessingStopTime();
+        return;
+      }
+
+
+      // Get the backend that should hold the search base.  If there is none,
+      // then fail.
+      if (backend == null)
+      {
+        localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+        localOp.appendErrorMessage(
+            getMessage(MSGID_SEARCH_BASE_DOESNT_EXIST,
+            String.valueOf(baseDN)));
+        break searchProcessing;
+      }
+
+
+      // We'll set the result code to "success".  If a problem occurs, then it
+      // will be overwritten.
+      localOp.setResultCode(ResultCode.SUCCESS);
+
+
+      // If there's a persistent search, then register it with the server.
+      if (persistentSearch != null)
+      {
+        DirectoryServer.registerPersistentSearch(persistentSearch);
+        localOp.setSendResponse(false);
+      }
+
+
+      // Process the search in the backend and all its subordinates.
+      try
+      {
+        if (processSearch)
+        {
+          localOp.searchBackend(backend);
+        }
+      }
+      catch (DirectoryException de)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, de);
+        }
+
+        localOp.setResultCode(de.getResultCode());
+        localOp.appendErrorMessage(de.getErrorMessage());
+        localOp.setMatchedDN(de.getMatchedDN());
+        localOp.setReferralURLs(de.getReferralURLs());
+
+        if (persistentSearch != null)
+        {
+          DirectoryServer.deregisterPersistentSearch(persistentSearch);
+          localOp.setSendResponse(true);
+        }
+
+        break searchProcessing;
+      }
+      catch (CancelledOperationException coe)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, coe);
+        }
+
+        CancelResult cancelResult = coe.getCancelResult();
+
+        localOp.setCancelResult(cancelResult);
+        localOp.setResultCode(cancelResult.getResultCode());
+
+        String message = coe.getMessage();
+        if ((message != null) && (message.length() > 0))
+        {
+          localOp.appendErrorMessage(message);
+        }
+
+        if (persistentSearch != null)
+        {
+          DirectoryServer.deregisterPersistentSearch(persistentSearch);
+          localOp.setSendResponse(true);
+        }
+
+        skipPostOperation = true;
+        break searchProcessing;
+      }
+      catch (Exception e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+
+        localOp.setResultCode(
+            DirectoryServer.getServerErrorResultCode());
+
+        int msgID = MSGID_SEARCH_BACKEND_EXCEPTION;
+        localOp.appendErrorMessage(
+            getMessage(msgID, getExceptionMessage(e)));
+
+        if (persistentSearch != null)
+        {
+          DirectoryServer.deregisterPersistentSearch(persistentSearch);
+          localOp.setSendResponse(true);
+        }
+
+        skipPostOperation = true;
+        break searchProcessing;
+      }
+    }
+
+
+    // Check for and handle a request to cancel this operation.
+    if (localOp.getCancelRequest() != null)
+    {
+      localOp.indicateCancelled(localOp.getCancelRequest());
+      localOp.setProcessingStopTime();
+      return;
+    }
+
+
+    // Invoke the post-operation search plugins.
+    if (! skipPostOperation)
+    {
+      PostOperationPluginResult postOperationResult =
+        pluginConfigManager.invokePostOperationSearchPlugins(localOp);
+      if (postOperationResult.connectionTerminated())
+      {
+        localOp.setResultCode(ResultCode.CANCELED);
+
+        int msgID = MSGID_CANCELED_BY_POSTOP_DISCONNECT;
+        localOp.appendErrorMessage(getMessage(msgID));
+
+        localOp.setProcessingStopTime();
+        return;
+      }
+    }
+
+  }
+
+  /**
+   * Perform a bind operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  public void processBind(BindOperation operation)
+  {
+    LocalBackendBindOperation localOperation =
+        new LocalBackendBindOperation(operation);
+
+    processLocalBind(localOperation);
+  }
+
+  /**
+   * Perform a local bind operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  private void processLocalBind(LocalBackendBindOperation localOp)
+  {
+    ClientConnection clientConnection = localOp.getClientConnection();
+
+    boolean returnAuthzID    = false;
+    int     sizeLimit        = DirectoryServer.getSizeLimit();
+    int     timeLimit        = DirectoryServer.getTimeLimit();
+    int     lookthroughLimit = DirectoryServer.getLookthroughLimit();
+    boolean skipPostOperation = false;
+
+    // The password policy state information for this bind operation.
+    PasswordPolicyState pwPolicyState = null;
+
+    // The password policy error type that should be included in the response
+    // control
+    PasswordPolicyErrorType pwPolicyErrorType = null;
+
+    // Indicates whether the client included the password policy control in the
+    // bind request.
+    boolean pwPolicyControlRequested = false;
+
+    // Indicates whether the authentication should use a grace login if it is
+    // successful.
+    boolean isGraceLogin = false;
+
+    // Indicates whether the user's password must be changed before any other
+    // operations will be allowed.
+    boolean mustChangePassword = false;
+
+    // The password policy warning type that should be included in the response
+    // control
+    PasswordPolicyWarningType pwPolicyWarningType = null;
+
+    // The password policy warning value that should be included in the response
+    // control.
+    int pwPolicyWarningValue = -1 ;
+
+    String saslMechanism = localOp.getSASLMechanism();
+
+    // Indicates whether the warning notification that should be sent to
+    // the user would be the first warning.
+    boolean isFirstWarning = false;
+
+    // The entry of the user that successfully authenticated during processing
+    // of this bind operation.
+    Entry authenticatedUserEntry = null;
+
+    // The password policy state information for this bind operation.
+    pwPolicyState = null;
+
+    // Get the plugin config manager that will be used for invoking plugins.
+    PluginConfigManager pluginConfigManager =
+         DirectoryServer.getPluginConfigManager();
+
+
+    // Create a labeled block of code that we can break out of if a problem is
+    // detected.
+bindProcessing:
+    {
+      DN bindDN = localOp.getBindDN();
+      // Check to see if the client has permission to perform the
+      // bind.
+
+      // FIXME: for now assume that this will check all permission
+      // pertinent to the operation. This includes any controls
+      // specified.
+      if (AccessControlConfigManager.getInstance()
+          .getAccessControlHandler().isAllowed(localOp) == false) {
+        localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+        int    msgID   = MSGID_BIND_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+        String message = getMessage(msgID, String.valueOf(bindDN));
+        localOp.setAuthFailureReason(msgID, message);
+
+        skipPostOperation = true;
+        break bindProcessing;
+      }
+
+      // Check to see if there are any controls in the request.  If so, then see
+      // if there is any special processing required.
+      List<Control> requestControls = localOp.getRequestControls();
+      if ((requestControls != null) && (! requestControls.isEmpty()))
+      {
+        for (int i=0; i < requestControls.size(); i++)
+        {
+          Control c   = requestControls.get(i);
+          String  oid = c.getOID();
+
+          if (oid.equals(OID_AUTHZID_REQUEST))
+          {
+            returnAuthzID = true;
+          }
+          else if (oid.equals(OID_PASSWORD_POLICY_CONTROL))
+          {
+            pwPolicyControlRequested = true;
+          }
+
+          // NYI -- Add support for additional controls.
+          else if (c.isCritical())
+          {
+            localOp.setResultCode(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+
+            int msgID = MSGID_BIND_UNSUPPORTED_CRITICAL_CONTROL;
+            localOp.appendErrorMessage(getMessage(msgID, String.valueOf(oid)));
+
+            break bindProcessing;
+          }
+        }
+      }
+
+
+      // Check to see if this is a simple bind or a SASL bind and process
+      // accordingly.
+      switch (localOp.getAuthenticationType())
+      {
+        case SIMPLE:
+          // See if this is an anonymous bind.  If so, then determine whether
+          // to allow it.
+
+          ByteString simplePassword = localOp.getSimplePassword();
+          if ((simplePassword == null) || (simplePassword.value().length == 0))
+          {
+            // If the server is in lockdown mode, then fail.
+            if (DirectoryServer.lockdownMode())
+            {
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              int msgID = MSGID_BIND_REJECTED_LOCKDOWN_MODE;
+              localOp.setAuthFailureReason(msgID, getMessage(msgID));
+
+              localOp.setProcessingStopTime();
+              logBindResponse(localOp);
+              break bindProcessing;
+            }
+
+            // If there is a bind DN, then see whether that is acceptable.
+            if (DirectoryServer.bindWithDNRequiresPassword() &&
+                ((bindDN != null) && (! bindDN.isNullDN())))
+            {
+              localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+              int    msgID   = MSGID_BIND_DN_BUT_NO_PASSWORD;
+              String message = getMessage(msgID);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+
+
+            // Invoke the pre-operation bind plugins.
+            PreOperationPluginResult preOpResult =
+                 pluginConfigManager.invokePreOperationBindPlugins(localOp);
+            if (preOpResult.connectionTerminated())
+            {
+              // There's no point in continuing with anything.  Log the result
+              // and return.
+              localOp.setResultCode(ResultCode.CANCELED);
+
+              int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+              localOp.appendErrorMessage(getMessage(msgID));
+
+              localOp.setProcessingStopTime();
+              return;
+            }
+            else if (preOpResult.sendResponseImmediately())
+            {
+              skipPostOperation = true;
+              break bindProcessing;
+            }
+            else if (preOpResult.skipCoreProcessing())
+            {
+              skipPostOperation = false;
+              break bindProcessing;
+            }
+
+            localOp.setResultCode(ResultCode.SUCCESS);
+            localOp.setAuthenticationInfo(new AuthenticationInfo());
+            break bindProcessing;
+          }
+
+          // See if the bind DN is actually one of the alternate root DNs
+          // defined in the server.  If so, then replace it with the actual DN
+          // for that user.
+          DN actualRootDN = DirectoryServer.getActualRootBindDN(bindDN);
+          if (actualRootDN != null)
+          {
+            bindDN = actualRootDN;
+          }
+
+          // Get the user entry based on the bind DN.  If it does not exist,
+          // then fail.
+          Lock userLock = null;
+          for (int i=0; i < 3; i++)
+          {
+            userLock = LockManager.lockRead(bindDN);
+            if (userLock != null)
+            {
+              break;
+            }
+          }
+
+          if (userLock == null)
+          {
+            int    msgID   = MSGID_BIND_OPERATION_CANNOT_LOCK_USER;
+            String message = getMessage(msgID, String.valueOf(bindDN));
+
+            localOp.setResultCode(DirectoryServer.getServerErrorResultCode());
+            localOp.setAuthFailureReason(msgID, message);
+            break bindProcessing;
+          }
+
+          try
+          {
+            Entry userEntry;
+            try
+            {
+              userEntry = backend.getEntry(bindDN);
+            }
+            catch (DirectoryException de)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(de.getMessageID(),
+                                   de.getErrorMessage());
+
+              userEntry = null;
+              break bindProcessing;
+            }
+
+            if (userEntry == null)
+            {
+
+              int    msgID   = MSGID_BIND_OPERATION_UNKNOWN_USER;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+            else
+            {
+              localOp.setUserEntryDN(userEntry.getDN());
+            }
+
+
+            // Check to see if the user has a password.  If not, then fail.
+            // FIXME -- We need to have a way to enable/disable debugging.
+            pwPolicyState = new PasswordPolicyState(userEntry, false, false);
+            AttributeType pwType
+                 = pwPolicyState.getPolicy().getPasswordAttribute();
+
+            List<Attribute> pwAttr = userEntry.getAttribute(pwType);
+            if ((pwAttr == null) || (pwAttr.isEmpty()))
+            {
+              int    msgID   = MSGID_BIND_OPERATION_NO_PASSWORD;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+
+
+            // Check to see if the authentication must be done in a secure
+            // manner.  If so, then the client connection must be secure.
+            if (pwPolicyState.getPolicy().requireSecureAuthentication() &&
+                (! clientConnection.isSecure()))
+            {
+              int    msgID   = MSGID_BIND_OPERATION_INSECURE_SIMPLE_BIND;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+
+
+            // Check to see if the user is administratively disabled or locked.
+            if (pwPolicyState.isDisabled())
+            {
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_DISABLED;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+            else if (pwPolicyState.isAccountExpired())
+            {
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_EXPIRED;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+
+              pwPolicyState.generateAccountStatusNotification(
+                   AccountStatusNotificationType.ACCOUNT_EXPIRED, bindDN, msgID,
+                   message);
+
+              break bindProcessing;
+            }
+            else if (pwPolicyState.lockedDueToFailures())
+            {
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_FAILURE_LOCKED;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              if (pwPolicyErrorType == null)
+              {
+                pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
+              }
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+            else if (pwPolicyState.lockedDueToMaximumResetAge())
+            {
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_RESET_LOCKED;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              if (pwPolicyErrorType == null)
+              {
+                pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
+              }
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+
+              pwPolicyState.generateAccountStatusNotification(
+                   AccountStatusNotificationType.ACCOUNT_RESET_LOCKED, bindDN,
+                   msgID, message);
+
+              break bindProcessing;
+            }
+            else if (pwPolicyState.lockedDueToIdleInterval())
+            {
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_IDLE_LOCKED;
+              String message = getMessage(msgID, String.valueOf(bindDN));
+
+              if (pwPolicyErrorType == null)
+              {
+                pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
+              }
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+
+              pwPolicyState.generateAccountStatusNotification(
+                   AccountStatusNotificationType.ACCOUNT_IDLE_LOCKED, bindDN,
+                   msgID, message);
+
+              break bindProcessing;
+            }
+
+
+            // Determine whether the password is expired, or whether the user
+            // should be warned about an upcoming expiration.
+            if (pwPolicyState.isPasswordExpired())
+            {
+              if (pwPolicyErrorType == null)
+              {
+                pwPolicyErrorType = PasswordPolicyErrorType.PASSWORD_EXPIRED;
+              }
+
+              int maxGraceLogins
+                   = pwPolicyState.getPolicy().getGraceLoginCount();
+              if ((maxGraceLogins > 0) && pwPolicyState.mayUseGraceLogin())
+              {
+                List<Long> graceLoginTimes = pwPolicyState.getGraceLoginTimes();
+                if ((graceLoginTimes == null) ||
+                    (graceLoginTimes.size() < maxGraceLogins))
+                {
+                  isGraceLogin       = true;
+                  mustChangePassword = true;
+
+                  if (pwPolicyWarningType == null)
+                  {
+                    pwPolicyWarningType =
+                         PasswordPolicyWarningType.GRACE_LOGINS_REMAINING;
+                    pwPolicyWarningValue = maxGraceLogins -
+                                           (graceLoginTimes.size() + 1);
+                  }
+                }
+                else
+                {
+                  int    msgID   = MSGID_BIND_OPERATION_PASSWORD_EXPIRED;
+                  String message = getMessage(msgID, String.valueOf(bindDN));
+
+                  localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+                  localOp.setAuthFailureReason(msgID, message);
+
+                  pwPolicyState.generateAccountStatusNotification(
+                       AccountStatusNotificationType.PASSWORD_EXPIRED, bindDN,
+                       msgID, message);
+
+                  break bindProcessing;
+                }
+              }
+              else
+              {
+                int    msgID   = MSGID_BIND_OPERATION_PASSWORD_EXPIRED;
+                String message = getMessage(msgID, String.valueOf(bindDN));
+
+                localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+                localOp.setAuthFailureReason(msgID, message);
+
+                pwPolicyState.generateAccountStatusNotification(
+                     AccountStatusNotificationType.PASSWORD_EXPIRED, bindDN,
+                     msgID, message);
+
+                break bindProcessing;
+              }
+            }
+            else if (pwPolicyState.shouldWarn())
+            {
+              int numSeconds = pwPolicyState.getSecondsUntilExpiration();
+              String timeToExpiration = secondsToTimeString(numSeconds);
+
+              int msgID = MSGID_BIND_PASSWORD_EXPIRING;
+              String message = getMessage(msgID, timeToExpiration);
+              localOp.appendErrorMessage(message);
+
+              if (pwPolicyWarningType == null)
+              {
+                pwPolicyWarningType =
+                     PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION;
+                pwPolicyWarningValue = numSeconds;
+              }
+
+              isFirstWarning = pwPolicyState.isFirstWarning();
+            }
+
+
+            // Check to see if the user's password has been reset.
+            if (pwPolicyState.mustChangePassword())
+            {
+              mustChangePassword = true;
+
+              if (pwPolicyErrorType == null)
+              {
+                pwPolicyErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
+              }
+            }
+
+
+            // Invoke the pre-operation bind plugins.
+            PreOperationPluginResult preOpResult =
+                 pluginConfigManager.invokePreOperationBindPlugins(localOp);
+            if (preOpResult.connectionTerminated())
+            {
+              // There's no point in continuing with anything.  Log the result
+              // and return.
+              localOp.setResultCode(ResultCode.CANCELED);
+
+              int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+              localOp.appendErrorMessage(getMessage(msgID));
+
+              localOp.setProcessingStopTime();
+              return;
+            }
+            else if (preOpResult.sendResponseImmediately())
+            {
+              skipPostOperation = true;
+              break bindProcessing;
+            }
+            else if (preOpResult.skipCoreProcessing())
+            {
+              skipPostOperation = false;
+              break bindProcessing;
+            }
+
+
+            // Determine whether the provided password matches any of the stored
+            // passwords for the user.
+            if (pwPolicyState.passwordMatches(simplePassword))
+            {
+              localOp.setResultCode(ResultCode.SUCCESS);
+
+              boolean isRoot = DirectoryServer.isRootDN(userEntry.getDN());
+              if (DirectoryServer.lockdownMode() && (! isRoot))
+              {
+                localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+                int msgID = MSGID_BIND_REJECTED_LOCKDOWN_MODE;
+                localOp.setAuthFailureReason(msgID, getMessage(msgID));
+
+                break bindProcessing;
+              }
+              localOp.setAuthenticationInfo(new AuthenticationInfo(
+                                                userEntry,
+                                                simplePassword,
+                                                isRoot));
+
+
+              // See if the user's entry contains a custom size limit.
+              AttributeType attrType =
+                   DirectoryServer.getAttributeType(OP_ATTR_USER_SIZE_LIMIT,
+                                                 true);
+              List<Attribute> attrList = userEntry.getAttribute(attrType);
+              if ((attrList != null) && (attrList.size() == 1))
+              {
+                Attribute a = attrList.get(0);
+                LinkedHashSet<AttributeValue>  values = a.getValues();
+                Iterator<AttributeValue> iterator = values.iterator();
+                if (iterator.hasNext())
+                {
+                  AttributeValue v = iterator.next();
+                  if (iterator.hasNext())
+                  {
+                    int msgID = MSGID_BIND_MULTIPLE_USER_SIZE_LIMITS;
+                    String message =
+                         getMessage(msgID, String.valueOf(userEntry.getDN()));
+                    logError(ErrorLogCategory.CORE_SERVER,
+                             ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                  }
+                  else
+                  {
+                    try
+                    {
+                      sizeLimit = Integer.parseInt(v.getStringValue());
+                    }
+                    catch (Exception e)
+                    {
+                      if (debugEnabled())
+                      {
+                        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                      }
+
+                      int msgID = MSGID_BIND_CANNOT_PROCESS_USER_SIZE_LIMIT;
+                      String message =
+                           getMessage(msgID, v.getStringValue(),
+                                      String.valueOf(userEntry.getDN()));
+                      logError(ErrorLogCategory.CORE_SERVER,
+                               ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                    }
+                  }
+                }
+              }
+
+
+              // See if the user's entry contains a custom time limit.
+              attrType =
+                   DirectoryServer.getAttributeType(OP_ATTR_USER_TIME_LIMIT,
+                                                 true);
+              attrList = userEntry.getAttribute(attrType);
+              if ((attrList != null) && (attrList.size() == 1))
+              {
+                Attribute a = attrList.get(0);
+                LinkedHashSet<AttributeValue>  values = a.getValues();
+                Iterator<AttributeValue> iterator = values.iterator();
+                if (iterator.hasNext())
+                {
+                  AttributeValue v = iterator.next();
+                  if (iterator.hasNext())
+                  {
+                    int msgID = MSGID_BIND_MULTIPLE_USER_TIME_LIMITS;
+                    String message =
+                         getMessage(msgID, String.valueOf(userEntry.getDN()));
+                    logError(ErrorLogCategory.CORE_SERVER,
+                             ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                  }
+                  else
+                  {
+                    try
+                    {
+                      timeLimit = Integer.parseInt(v.getStringValue());
+                    }
+                    catch (Exception e)
+                    {
+                      if (debugEnabled())
+                      {
+                        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                      }
+
+                      int msgID = MSGID_BIND_CANNOT_PROCESS_USER_TIME_LIMIT;
+                      String message =
+                           getMessage(msgID, v.getStringValue(),
+                                      String.valueOf(userEntry.getDN()));
+                      logError(ErrorLogCategory.CORE_SERVER,
+                               ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                    }
+                  }
+                }
+              }
+
+
+              // See if the user's entry contains a custom lookthrough limit.
+              attrType =
+                   DirectoryServer.getAttributeType(
+                       OP_ATTR_USER_LOOKTHROUGH_LIMIT, true);
+              attrList = userEntry.getAttribute(attrType);
+              if ((attrList != null) && (attrList.size() == 1))
+              {
+                Attribute a = attrList.get(0);
+                LinkedHashSet<AttributeValue>  values = a.getValues();
+                Iterator<AttributeValue> iterator = values.iterator();
+                if (iterator.hasNext())
+                {
+                  AttributeValue v = iterator.next();
+                  if (iterator.hasNext())
+                  {
+                    int msgID = MSGID_BIND_MULTIPLE_USER_LOOKTHROUGH_LIMITS;
+                    String message =
+                         getMessage(msgID, String.valueOf(userEntry.getDN()));
+                    logError(ErrorLogCategory.CORE_SERVER,
+                             ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                  }
+                  else
+                  {
+                    try
+                    {
+                      lookthroughLimit = Integer.parseInt(v.getStringValue());
+                    }
+                    catch (Exception e)
+                    {
+                      if (debugEnabled())
+                      {
+                        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                      }
+
+                      int msgID =
+                          MSGID_BIND_CANNOT_PROCESS_USER_LOOKTHROUGH_LIMIT;
+                      String message =
+                           getMessage(msgID, v.getStringValue(),
+                                      String.valueOf(userEntry.getDN()));
+                      logError(ErrorLogCategory.CORE_SERVER,
+                               ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                    }
+                  }
+                }
+              }
+
+
+              pwPolicyState.handleDeprecatedStorageSchemes(simplePassword);
+              pwPolicyState.clearFailureLockout();
+
+              if (isFirstWarning)
+              {
+                pwPolicyState.setWarnedTime();
+
+                int numSeconds = pwPolicyState.getSecondsUntilExpiration();
+                String timeToExpiration = secondsToTimeString(numSeconds);
+
+                int msgID = MSGID_BIND_PASSWORD_EXPIRING;
+                String message = getMessage(msgID, timeToExpiration);
+
+                pwPolicyState.generateAccountStatusNotification(
+                     AccountStatusNotificationType.PASSWORD_EXPIRING, bindDN,
+                     msgID, message);
+              }
+
+              if (isGraceLogin)
+              {
+                pwPolicyState.updateGraceLoginTimes();
+              }
+
+              pwPolicyState.setLastLoginTime();
+            }
+            else
+            {
+              int    msgID   = MSGID_BIND_OPERATION_WRONG_PASSWORD;
+              String message = getMessage(msgID);
+
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+              localOp.setAuthFailureReason(msgID, message);
+
+              if (pwPolicyState.getPolicy().getLockoutFailureCount() > 0)
+              {
+                pwPolicyState.updateAuthFailureTimes();
+                if (pwPolicyState.lockedDueToFailures())
+                {
+                  AccountStatusNotificationType notificationType;
+
+                  int lockoutDuration = pwPolicyState.getSecondsUntilUnlock();
+                  if (lockoutDuration > -1)
+                  {
+                    notificationType = AccountStatusNotificationType.
+                                            ACCOUNT_TEMPORARILY_LOCKED;
+                    msgID   = MSGID_BIND_ACCOUNT_TEMPORARILY_LOCKED;
+                    message = getMessage(msgID,
+                                         secondsToTimeString(lockoutDuration));
+                  }
+                  else
+                  {
+                    notificationType = AccountStatusNotificationType.
+                                            ACCOUNT_PERMANENTLY_LOCKED;
+                    msgID   = MSGID_BIND_ACCOUNT_PERMANENTLY_LOCKED;
+                    message = getMessage(msgID);
+                  }
+
+                  pwPolicyState.generateAccountStatusNotification(
+                       notificationType, localOp.getUserEntryDN(),
+                       msgID, message);
+                }
+              }
+            }
+          }
+          catch (Exception e)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, e);
+            }
+
+            int    msgID   = MSGID_BIND_OPERATION_PASSWORD_VALIDATION_EXCEPTION;
+            String message = getMessage(msgID, getExceptionMessage(e));
+
+            localOp.setResultCode(DirectoryServer.getServerErrorResultCode());
+            localOp.setAuthFailureReason(msgID, message);
+            break bindProcessing;
+          }
+          finally
+          {
+            // No matter what, make sure to unlock the user's entry.
+            LockManager.unlock(bindDN, userLock);
+          }
+
+          break;
+
+
+        case SASL:
+          // Get the appropriate authentication handler for this request based
+          // on the SASL mechanism.  If there is none, then fail.
+          SASLMechanismHandler saslHandler =
+               DirectoryServer.getSASLMechanismHandler(saslMechanism);
+          if (saslHandler == null)
+          {
+            localOp.setResultCode(ResultCode.AUTH_METHOD_NOT_SUPPORTED);
+
+            int    msgID   = MSGID_BIND_OPERATION_UNKNOWN_SASL_MECHANISM;
+            String message = getMessage(msgID, saslMechanism);
+
+            localOp.appendErrorMessage(message);
+            localOp.setAuthFailureReason(msgID, message);
+            break bindProcessing;
+          }
+
+
+          // Check to see if the client has sufficient permission to perform the
+          // bind.
+          // NYI
+
+
+          // Invoke the pre-operation bind plugins.
+          PreOperationPluginResult preOpResult =
+               pluginConfigManager.invokePreOperationBindPlugins(localOp);
+          if (preOpResult.connectionTerminated())
+          {
+            // There's no point in continuing with anything.  Log the result
+            // and return.
+            localOp.setResultCode(ResultCode.CANCELED);
+
+            int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+            localOp.appendErrorMessage(getMessage(msgID));
+
+            localOp.setProcessingStopTime();
+            return;
+          }
+          else if (preOpResult.sendResponseImmediately())
+          {
+            skipPostOperation = true;
+            break bindProcessing;
+          }
+          else if (preOpResult.skipCoreProcessing())
+          {
+            skipPostOperation = false;
+            break bindProcessing;
+          }
+
+          // Actually process the SASL bind.
+          saslHandler.processSASLBind(localOp);
+
+          // If the server is operating in lockdown mode, then we will need to
+          // ensure that the authentication was successful and performed as a
+          // root user to continue.
+          if (DirectoryServer.lockdownMode())
+          {
+            ResultCode resultCode = localOp.getResultCode();
+            if (resultCode != ResultCode.SASL_BIND_IN_PROGRESS)
+            {
+              if ((resultCode != ResultCode.SUCCESS) ||
+                  (localOp.getSASLAuthUserEntry() == null) ||
+                  (! DirectoryServer.isRootDN(
+                      localOp.getSASLAuthUserEntry().getDN())))
+              {
+                localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+                int msgID = MSGID_BIND_REJECTED_LOCKDOWN_MODE;
+                localOp.setAuthFailureReason(msgID, getMessage(msgID));
+
+                break bindProcessing;
+              }
+            }
+          }
+
+          // Create the password policy state object.
+          String userDNString;
+          Entry saslAuthUserEntry = localOp.getSASLAuthUserEntry();
+          if (saslAuthUserEntry == null)
+          {
+            pwPolicyState = null;
+            userDNString  = null;
+          }
+          else
+          {
+            try
+            {
+              // FIXME -- Need to have a way to enable debugging.
+              pwPolicyState = new PasswordPolicyState(saslAuthUserEntry, false,
+                                                      false);
+              localOp.setUserEntryDN(saslAuthUserEntry.getDN());
+              userDNString = String.valueOf(localOp.getUserEntryDN());
+            }
+            catch (DirectoryException de)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+
+              localOp.setResponseData(de);
+              break bindProcessing;
+            }
+          }
+
+
+          // Perform password policy checks that will need to be completed
+          // regardless of whether the authentication was successful.
+          if (pwPolicyState != null)
+          {
+            if (pwPolicyState.isDisabled())
+            {
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_DISABLED;
+              String message = getMessage(msgID, userDNString);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+            else if (pwPolicyState.isAccountExpired())
+            {
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_EXPIRED;
+              String message = getMessage(msgID, userDNString);
+              localOp.setAuthFailureReason(msgID, message);
+
+              pwPolicyState.generateAccountStatusNotification(
+                   AccountStatusNotificationType.ACCOUNT_EXPIRED, bindDN, msgID,
+                   message);
+
+              break bindProcessing;
+            }
+
+            if (pwPolicyState.getPolicy().requireSecureAuthentication() &&
+                (! clientConnection.isSecure()) &&
+                (! saslHandler.isSecure(saslMechanism)))
+            {
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              int    msgID   = MSGID_BIND_OPERATION_INSECURE_SASL_BIND;
+              String message = getMessage(msgID, saslMechanism, userDNString);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+
+            if (pwPolicyState.lockedDueToFailures())
+            {
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              if (pwPolicyErrorType == null)
+              {
+                pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
+              }
+
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_FAILURE_LOCKED;
+              String message = getMessage(msgID, userDNString);
+              localOp.setAuthFailureReason(msgID, message);
+              break bindProcessing;
+            }
+
+            if (pwPolicyState.lockedDueToIdleInterval())
+            {
+              localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              if (pwPolicyErrorType == null)
+              {
+                pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
+              }
+
+              int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_IDLE_LOCKED;
+              String message = getMessage(msgID, userDNString);
+              localOp.setAuthFailureReason(msgID, message);
+
+              pwPolicyState.generateAccountStatusNotification(
+                   AccountStatusNotificationType.ACCOUNT_IDLE_LOCKED, bindDN,
+                   msgID, message);
+
+              break bindProcessing;
+            }
+
+
+            if (saslHandler.isPasswordBased(saslMechanism))
+            {
+              if (pwPolicyState.lockedDueToMaximumResetAge())
+              {
+                localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+                if (pwPolicyErrorType == null)
+                {
+                  pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
+                }
+
+                int    msgID   = MSGID_BIND_OPERATION_ACCOUNT_RESET_LOCKED;
+                String message = getMessage(msgID, userDNString);
+                localOp.setAuthFailureReason(msgID, message);
+
+                pwPolicyState.generateAccountStatusNotification(
+                     AccountStatusNotificationType.ACCOUNT_RESET_LOCKED, bindDN,
+                     msgID, message);
+
+                break bindProcessing;
+              }
+
+              if (pwPolicyState.isPasswordExpired())
+              {
+                if (pwPolicyErrorType == null)
+                {
+                  pwPolicyErrorType = PasswordPolicyErrorType.PASSWORD_EXPIRED;
+                }
+
+                int maxGraceLogins
+                     = pwPolicyState.getPolicy().getGraceLoginCount();
+                if ((maxGraceLogins > 0) && pwPolicyState.mayUseGraceLogin())
+                {
+                  List<Long> graceLoginTimes =
+                       pwPolicyState.getGraceLoginTimes();
+                  if ((graceLoginTimes == null) ||
+                      (graceLoginTimes.size() < maxGraceLogins))
+                  {
+                    isGraceLogin       = true;
+                    mustChangePassword = true;
+
+                    if (pwPolicyWarningType == null)
+                    {
+                      pwPolicyWarningType =
+                           PasswordPolicyWarningType.GRACE_LOGINS_REMAINING;
+                      pwPolicyWarningValue =
+                           maxGraceLogins - (graceLoginTimes.size() + 1);
+                    }
+                  }
+                  else
+                  {
+                    int    msgID   = MSGID_BIND_OPERATION_PASSWORD_EXPIRED;
+                    String message = getMessage(msgID, String.valueOf(bindDN));
+
+                    localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+                    localOp.setAuthFailureReason(msgID, message);
+
+                    pwPolicyState.generateAccountStatusNotification(
+                         AccountStatusNotificationType.PASSWORD_EXPIRED, bindDN,
+                         msgID, message);
+
+                    break bindProcessing;
+                  }
+                }
+                else
+                {
+                  int    msgID   = MSGID_BIND_OPERATION_PASSWORD_EXPIRED;
+                  String message = getMessage(msgID, String.valueOf(bindDN));
+
+                  localOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
+                  localOp.setAuthFailureReason(msgID, message);
+
+                  pwPolicyState.generateAccountStatusNotification(
+                       AccountStatusNotificationType.PASSWORD_EXPIRED, bindDN,
+                       msgID, message);
+
+                  break bindProcessing;
+                }
+              }
+              else if (pwPolicyState.shouldWarn())
+              {
+                int numSeconds = pwPolicyState.getSecondsUntilExpiration();
+                String timeToExpiration = secondsToTimeString(numSeconds);
+
+                int    msgID   = MSGID_BIND_PASSWORD_EXPIRING;
+                String message = getMessage(msgID, timeToExpiration);
+                localOp.appendErrorMessage(message);
+
+                if (pwPolicyWarningType == null)
+                {
+                  pwPolicyWarningType =
+                       PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION;
+                  pwPolicyWarningValue = numSeconds;
+                }
+
+                isFirstWarning = pwPolicyState.isFirstWarning();
+              }
+            }
+          }
+
+
+          // Determine whether the authentication was successful and perform
+          // any remaining password policy processing accordingly.  Also check
+          // for a custom size/time limit.
+          ResultCode resultCode = localOp.getResultCode();
+          if (resultCode == ResultCode.SUCCESS)
+          {
+            if (pwPolicyState != null)
+            {
+              if (saslHandler.isPasswordBased(saslMechanism) &&
+                  pwPolicyState.mustChangePassword())
+              {
+                mustChangePassword = true;
+              }
+
+              if (isFirstWarning)
+              {
+                pwPolicyState.setWarnedTime();
+
+                int numSeconds = pwPolicyState.getSecondsUntilExpiration();
+                String timeToExpiration = secondsToTimeString(numSeconds);
+
+                int msgID = MSGID_BIND_PASSWORD_EXPIRING;
+                String message = getMessage(msgID, timeToExpiration);
+
+                pwPolicyState.generateAccountStatusNotification(
+                     AccountStatusNotificationType.PASSWORD_EXPIRING, bindDN,
+                     msgID, message);
+              }
+
+              if (isGraceLogin)
+              {
+                pwPolicyState.updateGraceLoginTimes();
+              }
+
+              pwPolicyState.setLastLoginTime();
+
+
+              // See if the user's entry contains a custom size limit.
+              AttributeType attrType =
+                   DirectoryServer.getAttributeType(OP_ATTR_USER_SIZE_LIMIT,
+                                                 true);
+              List<Attribute> attrList =
+                   saslAuthUserEntry.getAttribute(attrType);
+              if ((attrList != null) && (attrList.size() == 1))
+              {
+                Attribute a = attrList.get(0);
+                LinkedHashSet<AttributeValue>  values = a.getValues();
+                Iterator<AttributeValue> iterator = values.iterator();
+                if (iterator.hasNext())
+                {
+                  AttributeValue v = iterator.next();
+                  if (iterator.hasNext())
+                  {
+                    int msgID = MSGID_BIND_MULTIPLE_USER_SIZE_LIMITS;
+                    String message = getMessage(msgID, userDNString);
+                    logError(ErrorLogCategory.CORE_SERVER,
+                             ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                  }
+                  else
+                  {
+                    try
+                    {
+                      sizeLimit = Integer.parseInt(v.getStringValue());
+                    }
+                    catch (Exception e)
+                    {
+                      if (debugEnabled())
+                      {
+                        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                      }
+
+                      int msgID = MSGID_BIND_CANNOT_PROCESS_USER_SIZE_LIMIT;
+                      String message =
+                           getMessage(msgID, v.getStringValue(), userDNString);
+                      logError(ErrorLogCategory.CORE_SERVER,
+                               ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                    }
+                  }
+                }
+              }
+
+
+              // See if the user's entry contains a custom time limit.
+              attrType =
+                   DirectoryServer.getAttributeType(OP_ATTR_USER_TIME_LIMIT,
+                                                    true);
+              attrList = saslAuthUserEntry.getAttribute(attrType);
+              if ((attrList != null) && (attrList.size() == 1))
+              {
+                Attribute a = attrList.get(0);
+                LinkedHashSet<AttributeValue>  values = a.getValues();
+                Iterator<AttributeValue> iterator = values.iterator();
+                if (iterator.hasNext())
+                {
+                  AttributeValue v = iterator.next();
+                  if (iterator.hasNext())
+                  {
+                    int msgID = MSGID_BIND_MULTIPLE_USER_TIME_LIMITS;
+                    String message = getMessage(msgID, userDNString);
+                    logError(ErrorLogCategory.CORE_SERVER,
+                             ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                  }
+                  else
+                  {
+                    try
+                    {
+                      timeLimit = Integer.parseInt(v.getStringValue());
+                    }
+                    catch (Exception e)
+                    {
+                      if (debugEnabled())
+                      {
+                        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                      }
+
+                      int msgID = MSGID_BIND_CANNOT_PROCESS_USER_TIME_LIMIT;
+                      String message =
+                           getMessage(msgID, v.getStringValue(), userDNString);
+                      logError(ErrorLogCategory.CORE_SERVER,
+                               ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                    }
+                  }
+                }
+              }
+
+
+              // See if the user's entry contains a custom lookthrough limit.
+              attrType =
+                   DirectoryServer.getAttributeType(
+                       OP_ATTR_USER_LOOKTHROUGH_LIMIT, true);
+              attrList = saslAuthUserEntry.getAttribute(attrType);
+              if ((attrList != null) && (attrList.size() == 1))
+              {
+                Attribute a = attrList.get(0);
+                LinkedHashSet<AttributeValue>  values = a.getValues();
+                Iterator<AttributeValue> iterator = values.iterator();
+                if (iterator.hasNext())
+                {
+                  AttributeValue v = iterator.next();
+                  if (iterator.hasNext())
+                  {
+                    int msgID = MSGID_BIND_MULTIPLE_USER_LOOKTHROUGH_LIMITS;
+                    String message = getMessage(msgID, userDNString);
+                    logError(ErrorLogCategory.CORE_SERVER,
+                             ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                  }
+                  else
+                  {
+                    try
+                    {
+                      lookthroughLimit = Integer.parseInt(v.getStringValue());
+                    }
+                    catch (Exception e)
+                    {
+                      if (debugEnabled())
+                      {
+                        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                      }
+
+                      int msgID =
+                          MSGID_BIND_CANNOT_PROCESS_USER_LOOKTHROUGH_LIMIT;
+                      String message =
+                           getMessage(msgID, v.getStringValue(), userDNString);
+                      logError(ErrorLogCategory.CORE_SERVER,
+                               ErrorLogSeverity.SEVERE_WARNING, message, msgID);
+                    }
+                  }
+                }
+              }
+            }
+          }
+          else if (resultCode == ResultCode.SASL_BIND_IN_PROGRESS)
+          {
+            // FIXME -- Is any special processing needed here?
+          }
+          else
+          {
+            if (pwPolicyState != null)
+            {
+              if (saslHandler.isPasswordBased(saslMechanism))
+              {
+
+                if (pwPolicyState.getPolicy().getLockoutFailureCount() > 0)
+                {
+                  pwPolicyState.updateAuthFailureTimes();
+                  if (pwPolicyState.lockedDueToFailures())
+                  {
+                    AccountStatusNotificationType notificationType;
+                    int msgID;
+                    String message;
+
+                    int lockoutDuration = pwPolicyState.getSecondsUntilUnlock();
+                    if (lockoutDuration > -1)
+                    {
+                      notificationType = AccountStatusNotificationType.
+                                              ACCOUNT_TEMPORARILY_LOCKED;
+                      msgID   = MSGID_BIND_ACCOUNT_TEMPORARILY_LOCKED;
+                      message = getMessage(msgID,
+                                     secondsToTimeString(lockoutDuration));
+                    }
+                    else
+                    {
+                      notificationType = AccountStatusNotificationType.
+                                              ACCOUNT_PERMANENTLY_LOCKED;
+                      msgID   = MSGID_BIND_ACCOUNT_PERMANENTLY_LOCKED;
+                      message = getMessage(msgID);
+                    }
+
+                    pwPolicyState.generateAccountStatusNotification(
+                         notificationType, localOp.getUserEntryDN(),
+                         msgID, message);
+                  }
+                }
+              }
+            }
+          }
+
+          break;
+
+
+        default:
+          // Send a protocol error response to the client and disconnect.
+          // NYI
+          return;
+      }
+    }
+
+
+    // Update the user's account with any password policy changes that may be
+    // required.
+    try
+    {
+      if (pwPolicyState != null)
+      {
+        pwPolicyState.updateUserEntry();
+      }
+    }
+    catch (DirectoryException de)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugCaught(DebugLogLevel.ERROR, de);
+      }
+
+      localOp.setResponseData(de);
+    }
+
+
+    // Invoke the post-operation bind plugins.
+    if (! skipPostOperation)
+    {
+      PostOperationPluginResult postOpResult =
+           pluginConfigManager.invokePostOperationBindPlugins(localOp);
+      if (postOpResult.connectionTerminated())
+      {
+        // There's no point in continuing with anything.  Log the result
+        // and return.
+        localOp.setResultCode(ResultCode.CANCELED);
+
+        int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+        localOp.appendErrorMessage(getMessage(msgID));
+
+        localOp.setProcessingStopTime();
+        return;
+      }
+    }
+
+
+    // Update the authentication information for the user.
+    AuthenticationInfo authInfo = localOp.getAuthenticationInfo();
+    if ((localOp.getResultCode() == ResultCode.SUCCESS) &&
+        (authInfo != null))
+    {
+      authenticatedUserEntry = authInfo.getAuthenticationEntry();
+      clientConnection.setAuthenticationInfo(authInfo);
+      clientConnection.setSizeLimit(sizeLimit);
+      clientConnection.setTimeLimit(timeLimit);
+      clientConnection.setLookthroughLimit(lookthroughLimit);
+      clientConnection.setMustChangePassword(mustChangePassword);
+
+      if (returnAuthzID)
+      {
+        localOp.addResponseControl(
+            new AuthorizationIdentityResponseControl(
+                                      authInfo.getAuthorizationDN()));
+      }
+    }
+
+
+    // See if we need to send a password policy control to the client.  If so,
+    // then add it to the response.
+    if (localOp.getResultCode() == ResultCode.SUCCESS)
+    {
+      if (pwPolicyControlRequested)
+      {
+        PasswordPolicyResponseControl pwpControl =
+             new PasswordPolicyResponseControl(pwPolicyWarningType,
+                                               pwPolicyWarningValue,
+                                               pwPolicyErrorType);
+        localOp.addResponseControl(pwpControl);
+      }
+      else
+      {
+        if (pwPolicyErrorType == PasswordPolicyErrorType.PASSWORD_EXPIRED)
+        {
+          localOp.addResponseControl(new PasswordExpiredControl());
+        }
+        else if (pwPolicyWarningType ==
+                 PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION)
+        {
+          localOp.addResponseControl(new PasswordExpiringControl(
+                                        pwPolicyWarningValue));
+        }
+      }
+    }
+    else
+    {
+      if (pwPolicyControlRequested)
+      {
+        PasswordPolicyResponseControl pwpControl =
+             new PasswordPolicyResponseControl(pwPolicyWarningType,
+                                               pwPolicyWarningValue,
+                                               pwPolicyErrorType);
+        localOp.addResponseControl(pwpControl);
+      }
+      else
+      {
+        if (pwPolicyErrorType == PasswordPolicyErrorType.PASSWORD_EXPIRED)
+        {
+          localOp.addResponseControl(new PasswordExpiredControl());
+        }
+      }
+    }
+
+    // Stop the processing timer.
+    localOp.setProcessingStopTime();
+
+  }
+
+  /**
+   * Perform an add operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  public void processAdd(AddOperation operation)
+  {
+    LocalBackendAddOperation localOperation =
+      new LocalBackendAddOperation(operation);
+
+    processLocalAdd(localOperation);
+  }
+
+  /**
+   * Perform a local add operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  private void processLocalAdd(LocalBackendAddOperation localOp)
+  {
+    ClientConnection clientConnection = localOp.getClientConnection();
+
+    // Get the plugin config manager that will be used for invoking plugins.
+    PluginConfigManager pluginConfigManager =
+         DirectoryServer.getPluginConfigManager();
+    boolean skipPostOperation = false;
+
+    // Check for and handle a request to cancel this operation.
+    if (localOp.getCancelRequest() != null)
+    {
+      localOp.indicateCancelled(localOp.getCancelRequest());
+      localOp.setProcessingStopTime();
+      return;
+    }
+
+    // Create a labeled block of code that we can break out of if a problem is
+    // detected.
+addProcessing:
+    {
+      // Process the entry DN and set of attributes to convert them from their
+      // raw forms as provided by the client to the forms required for the rest
+      // of the add processing.
+      DN entryDN = localOp.getEntryDN();
+      if (entryDN == null){
+        break addProcessing;
+      }
+
+      Map<ObjectClass, String> objectClasses =
+        localOp.getObjectClasses();
+      Map<AttributeType, List<Attribute>> userAttributes =
+        localOp.getUserAttributes();
+      Map<AttributeType, List<Attribute>> operationalAttributes =
+        localOp.getOperationalAttributes();
+
+      if ((objectClasses == null ) ||
+          (userAttributes == null) ||
+          (operationalAttributes == null)){
+        break addProcessing;
+      }
+
+      // Check for and handle a request to cancel this operation.
+      if (localOp.getCancelRequest() != null)
+      {
+        localOp.indicateCancelled(localOp.getCancelRequest());
+        localOp.setProcessingStopTime();
+        return;
+      }
+
+
+      // Grab a read lock on the parent entry, if there is one.  We need to do
+      // this to ensure that the parent is not deleted or renamed while this add
+      // is in progress, and we could also need it to check the entry against
+      // a DIT structure rule.
+      Lock parentLock = null;
+      Lock entryLock  = null;
+
+      DN parentDN = entryDN.getParentDNInSuffix();
+      if (parentDN == null)
+      {
+        // Either this entry is a suffix or doesn't belong in the directory.
+        if (DirectoryServer.isNamingContext(entryDN))
+        {
+          // This is fine.  This entry is one of the configured suffixes.
+          parentLock = null;
+        }
+        else if (entryDN.isNullDN())
+        {
+          // This is not fine.  The root DSE cannot be added.
+          localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+          localOp.appendErrorMessage(getMessage(MSGID_ADD_CANNOT_ADD_ROOT_DSE));
+          break addProcessing;
+        }
+        else
+        {
+          // The entry doesn't have a parent but isn't a suffix.  This is not
+          // allowed.
+          localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+          localOp.appendErrorMessage(getMessage(MSGID_ADD_ENTRY_NOT_SUFFIX,
+                                        String.valueOf(entryDN)));
+          break addProcessing;
+        }
+      }
+      else
+      {
+        for (int i=0; i < 3; i++)
+        {
+          parentLock = LockManager.lockRead(parentDN);
+          if (parentLock != null)
+          {
+            break;
+          }
+        }
+
+        if (parentLock == null)
+        {
+          localOp.setResultCode(DirectoryServer.getServerErrorResultCode());
+          localOp.appendErrorMessage(getMessage(MSGID_ADD_CANNOT_LOCK_PARENT,
+                                        String.valueOf(entryDN),
+                                        String.valueOf(parentDN)));
+
+          skipPostOperation = true;
+          break addProcessing;
+        }
+      }
+
+
+      try
+      {
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+
+        // Grab a write lock on the target entry.  We'll need to do this
+        // eventually anyway, and we want to make sure that the two locks are
+        // always released when exiting this method, no matter what.  Since
+        // the entry shouldn't exist yet, locking earlier than necessary
+        // shouldn't cause a problem.
+        for (int i=0; i < 3; i++)
+        {
+          entryLock = LockManager.lockWrite(entryDN);
+          if (entryLock != null)
+          {
+            break;
+          }
+        }
+
+        if (entryLock == null)
+        {
+          localOp.setResultCode(DirectoryServer.getServerErrorResultCode());
+          localOp.appendErrorMessage(getMessage(MSGID_ADD_CANNOT_LOCK_ENTRY,
+                                        String.valueOf(entryDN)));
+
+          skipPostOperation = true;
+          break addProcessing;
+        }
+
+
+        // Invoke any conflict resolution processing that might be needed by the
+        // synchronization provider.
+        for (SynchronizationProvider provider :
+             DirectoryServer.getSynchronizationProviders())
+        {
+          try
+          {
+            SynchronizationProviderResult result =
+                 provider.handleConflictResolution(localOp);
+            if (! result.continueOperationProcessing())
+            {
+              break addProcessing;
+            }
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            logError(ErrorLogCategory.SYNCHRONIZATION,
+                     ErrorLogSeverity.SEVERE_ERROR,
+                     MSGID_ADD_SYNCH_CONFLICT_RESOLUTION_FAILED,
+                     localOp.getConnectionID(),
+                     localOp.getOperationID(),
+                     getExceptionMessage(de));
+
+            localOp.setResponseData(de);
+            break addProcessing;
+          }
+        }
+
+
+        // Check to see if the entry already exists.  We do this before
+        // checking whether the parent exists to ensure a referral entry
+        // above the parent results in a correct referral.
+        try
+        {
+          if (DirectoryServer.entryExists(entryDN))
+          {
+            localOp.setResultCode(ResultCode.ENTRY_ALREADY_EXISTS);
+              localOp.appendErrorMessage(getMessage(
+                                MSGID_ADD_ENTRY_ALREADY_EXISTS,
+                                String.valueOf(entryDN)));
+            break addProcessing;
+          }
+        }
+        catch (DirectoryException de)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, de);
+          }
+
+          localOp.setResultCode(de.getResultCode());
+          localOp.appendErrorMessage(de.getErrorMessage());
+          localOp.setMatchedDN(de.getMatchedDN());
+          localOp.setReferralURLs(de.getReferralURLs());
+          break addProcessing;
+        }
+
+
+        // Get the parent entry, if it exists.
+        Entry parentEntry = null;
+        if (parentDN != null)
+        {
+          try
+          {
+            parentEntry = DirectoryServer.getEntry(parentDN);
+
+            if (parentEntry == null)
+            {
+              DN matchedDN = parentDN.getParentDNInSuffix();
+              while (matchedDN != null)
+              {
+                try
+                {
+                  if (DirectoryServer.entryExists(matchedDN))
+                  {
+                    localOp.setMatchedDN(matchedDN);
+                    break;
+                  }
+                }
+                catch (Exception e)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, e);
+                  }
+                  break;
+                }
+
+                matchedDN = matchedDN.getParentDNInSuffix();
+              }
+
+
+              // The parent doesn't exist, so this add can't be successful.
+              localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+              localOp.appendErrorMessage(getMessage(MSGID_ADD_NO_PARENT,
+                                            String.valueOf(entryDN),
+                                            String.valueOf(parentDN)));
+              break addProcessing;
+            }
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            localOp.setResultCode(de.getResultCode());
+            localOp.appendErrorMessage(de.getErrorMessage());
+            localOp.setMatchedDN(de.getMatchedDN());
+            localOp.setReferralURLs(de.getReferralURLs());
+            break addProcessing;
+          }
+        }
+
+
+        // Check to make sure that all of the RDN attributes are included as
+        // attribute values.  If not, then either add them or report an error.
+        RDN rdn = entryDN.getRDN();
+        int numAVAs = rdn.getNumValues();
+        for (int i=0; i < numAVAs; i++)
+        {
+          AttributeType  t = rdn.getAttributeType(i);
+          AttributeValue v = rdn.getAttributeValue(i);
+          String         n = rdn.getAttributeName(i);
+          if (t.isOperational())
+          {
+            List<Attribute> attrList = operationalAttributes.get(t);
+            if (attrList == null)
+            {
+              if (localOp.isSynchronizationOperation() ||
+                  DirectoryServer.addMissingRDNAttributes())
+              {
+                LinkedHashSet<AttributeValue> valueList =
+                     new LinkedHashSet<AttributeValue>(1);
+                valueList.add(v);
+
+                attrList = new ArrayList<Attribute>();
+                attrList.add(new Attribute(t, n, valueList));
+
+                operationalAttributes.put(t, attrList);
+              }
+              else
+              {
+                localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+
+                int msgID = MSGID_ADD_MISSING_RDN_ATTRIBUTE;
+                localOp.appendErrorMessage(getMessage(msgID,
+                                                      String.valueOf(entryDN),
+                                                      n));
+
+                break addProcessing;
+              }
+            }
+            else
+            {
+              boolean found = false;
+              for (Attribute a : attrList)
+              {
+                if (a.hasOptions())
+                {
+                  continue;
+                }
+                else
+                {
+                  if (! a.hasValue(v))
+                  {
+                    a.getValues().add(v);
+                  }
+
+                  found = true;
+                  break;
+                }
+              }
+
+              if (! found)
+              {
+                if (localOp.isSynchronizationOperation() ||
+                    DirectoryServer.addMissingRDNAttributes())
+                {
+                  LinkedHashSet<AttributeValue> valueList =
+                       new LinkedHashSet<AttributeValue>(1);
+                  valueList.add(v);
+                  attrList.add(new Attribute(t, n, valueList));
+                }
+                else
+                {
+                  localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+
+                  int msgID = MSGID_ADD_MISSING_RDN_ATTRIBUTE;
+                  localOp.appendErrorMessage(
+                      getMessage(msgID, String.valueOf(entryDN), n));
+
+                  break addProcessing;
+                }
+              }
+            }
+          }
+          else
+          {
+            List<Attribute> attrList = userAttributes.get(t);
+            if (attrList == null)
+            {
+              if (localOp.isSynchronizationOperation() ||
+                  DirectoryServer.addMissingRDNAttributes())
+              {
+                LinkedHashSet<AttributeValue> valueList =
+                     new LinkedHashSet<AttributeValue>(1);
+                valueList.add(v);
+
+                attrList = new ArrayList<Attribute>();
+                attrList.add(new Attribute(t, n, valueList));
+
+                userAttributes.put(t, attrList);
+              }
+              else
+              {
+                localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+
+                int msgID = MSGID_ADD_MISSING_RDN_ATTRIBUTE;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),n));
+
+                break addProcessing;
+              }
+            }
+            else
+            {
+              boolean found = false;
+              for (Attribute a : attrList)
+              {
+                if (a.hasOptions())
+                {
+                  continue;
+                }
+                else
+                {
+                  if (! a.hasValue(v))
+                  {
+                    a.getValues().add(v);
+                  }
+
+                  found = true;
+                  break;
+                }
+              }
+
+              if (! found)
+              {
+                if (localOp.isSynchronizationOperation() ||
+                    DirectoryServer.addMissingRDNAttributes())
+                {
+                  LinkedHashSet<AttributeValue> valueList =
+                       new LinkedHashSet<AttributeValue>(1);
+                  valueList.add(v);
+                  attrList.add(new Attribute(t, n, valueList));
+                }
+                else
+                {
+                  localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+
+                  int msgID = MSGID_ADD_MISSING_RDN_ATTRIBUTE;
+                  localOp.appendErrorMessage(
+                      getMessage(msgID, String.valueOf(entryDN),n));
+
+                  break addProcessing;
+                }
+              }
+            }
+          }
+        }
+
+
+        // Check to make sure that all objectclasses have their superior classes
+        // listed in the entry.  If not, then add them.
+        HashSet<ObjectClass> additionalClasses = null;
+        for (ObjectClass oc : objectClasses.keySet())
+        {
+          ObjectClass superiorClass = oc.getSuperiorClass();
+          if ((superiorClass != null) &&
+              (! objectClasses.containsKey(superiorClass)))
+          {
+            if (additionalClasses == null)
+            {
+              additionalClasses = new HashSet<ObjectClass>();
+            }
+
+            additionalClasses.add(superiorClass);
+          }
+        }
+
+        if (additionalClasses != null)
+        {
+          for (ObjectClass oc : additionalClasses)
+          {
+            localOp.addObjectClassChain(oc);
+          }
+        }
+
+
+        // Create an entry object to encapsulate the set of attributes and
+        // objectclasses.
+        Entry entry = new Entry(entryDN, objectClasses, userAttributes,
+                          operationalAttributes);
+        localOp.setEntryToAdd(entry);
+
+        // Check to see if the entry includes a privilege specification.  If so,
+        // then the requester must have the PRIVILEGE_CHANGE privilege.
+        AttributeType privType =
+             DirectoryServer.getAttributeType(OP_ATTR_PRIVILEGE_NAME, true);
+        if (entry.hasAttribute(privType) &&
+            (! clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE,
+                localOp)))
+        {
+          int msgID = MSGID_ADD_CHANGE_PRIVILEGE_INSUFFICIENT_PRIVILEGES;
+          localOp.appendErrorMessage(getMessage(msgID));
+          localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+          break addProcessing;
+        }
+
+
+        // If it's not a synchronization operation, then check
+        // to see if the entry contains one or more passwords and if they
+        // are valid in accordance with the password policies associated with
+        // the user.  Also perform any encoding that might be required by
+        // password storage schemes.
+        if (! localOp.isSynchronizationOperation())
+        {
+          // FIXME -- We need to check to see if the password policy subentry
+          //          might be specified virtually rather than as a real
+          //          attribute.
+          PasswordPolicy pwPolicy = null;
+          List<Attribute> pwAttrList =
+               entry.getAttribute(OP_ATTR_PWPOLICY_POLICY_DN);
+          if ((pwAttrList != null) && (! pwAttrList.isEmpty()))
+          {
+            Attribute a = pwAttrList.get(0);
+            LinkedHashSet<AttributeValue> valueSet = a.getValues();
+            Iterator<AttributeValue> iterator = valueSet.iterator();
+            if (iterator.hasNext())
+            {
+              DN policyDN;
+              try
+              {
+                policyDN = DN.decode(iterator.next().getValue());
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                int msgID = MSGID_ADD_INVALID_PWPOLICY_DN_SYNTAX;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                        de.getErrorMessage()));
+
+                localOp.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+                break addProcessing;
+              }
+
+              pwPolicy = DirectoryServer.getPasswordPolicy(policyDN);
+              if (pwPolicy == null)
+              {
+                int msgID = MSGID_ADD_NO_SUCH_PWPOLICY;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN),
+                        String.valueOf(policyDN)));
+
+                localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+                break addProcessing;
+              }
+            }
+          }
+
+          if (pwPolicy == null)
+          {
+            pwPolicy = DirectoryServer.getDefaultPasswordPolicy();
+          }
+
+          try
+          {
+            localOp.handlePasswordPolicy(pwPolicy, entry);
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            localOp.setResponseData(de);
+            break addProcessing;
+          }
+        }
+
+
+        // Check to see if the entry is valid according to the server schema,
+        // and also whether its attributes are valid according to their syntax.
+        if (DirectoryServer.checkSchema())
+        {
+          StringBuilder invalidReason = new StringBuilder();
+          if (! entry.conformsToSchema(parentEntry, true, true, true,
+                                       invalidReason))
+          {
+            localOp.setResultCode(ResultCode.OBJECTCLASS_VIOLATION);
+            localOp.setErrorMessage(invalidReason);
+            break addProcessing;
+          }
+          else
+          {
+            switch (DirectoryServer.getSyntaxEnforcementPolicy())
+            {
+              case REJECT:
+                invalidReason = new StringBuilder();
+                for (List<Attribute> attrList : userAttributes.values())
+                {
+                  for (Attribute a : attrList)
+                  {
+                    AttributeSyntax syntax = a.getAttributeType().getSyntax();
+                    if (syntax != null)
+                    {
+                      for (AttributeValue v : a.getValues())
+                      {
+                        if (! syntax.valueIsAcceptable(v.getValue(),
+                                                       invalidReason))
+                        {
+                          String message =
+                               getMessage(MSGID_ADD_OP_INVALID_SYNTAX,
+                                          String.valueOf(entryDN),
+                                          String.valueOf(v.getStringValue()),
+                                          String.valueOf(a.getName()),
+                                          String.valueOf(invalidReason));
+                          invalidReason = new StringBuilder(message);
+
+                          localOp.setResultCode(
+                              ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+                          localOp.setErrorMessage(invalidReason);
+                          break addProcessing;
+                        }
+                      }
+                    }
+                  }
+                }
+
+                for (List<Attribute> attrList :
+                     operationalAttributes.values())
+                {
+                  for (Attribute a : attrList)
+                  {
+                    AttributeSyntax syntax = a.getAttributeType().getSyntax();
+                    if (syntax != null)
+                    {
+                      for (AttributeValue v : a.getValues())
+                      {
+                        if (! syntax.valueIsAcceptable(v.getValue(),
+                                                       invalidReason))
+                        {
+                          String message =
+                               getMessage(MSGID_ADD_OP_INVALID_SYNTAX,
+                                          String.valueOf(entryDN),
+                                          String.valueOf(v.getStringValue()),
+                                          String.valueOf(a.getName()),
+                                          String.valueOf(invalidReason));
+                          invalidReason = new StringBuilder(message);
+
+                          localOp.setResultCode(
+                              ResultCode.INVALID_ATTRIBUTE_SYNTAX);
+                          localOp.setErrorMessage(invalidReason);
+                          break addProcessing;
+                        }
+                      }
+                    }
+                  }
+                }
+
+                break;
+
+
+              case WARN:
+                invalidReason = new StringBuilder();
+                for (List<Attribute> attrList : userAttributes.values())
+                {
+                  for (Attribute a : attrList)
+                  {
+                    AttributeSyntax syntax = a.getAttributeType().getSyntax();
+                    if (syntax != null)
+                    {
+                      for (AttributeValue v : a.getValues())
+                      {
+                        if (! syntax.valueIsAcceptable(v.getValue(),
+                                                       invalidReason))
+                        {
+                          logError(ErrorLogCategory.SCHEMA,
+                                   ErrorLogSeverity.SEVERE_WARNING,
+                                   MSGID_ADD_OP_INVALID_SYNTAX,
+                                   String.valueOf(entryDN),
+                                   String.valueOf(v.getStringValue()),
+                                   String.valueOf(a.getName()),
+                                   String.valueOf(invalidReason));
+                        }
+                      }
+                    }
+                  }
+                }
+
+                for (List<Attribute> attrList : operationalAttributes.values())
+                {
+                  for (Attribute a : attrList)
+                  {
+                    AttributeSyntax syntax = a.getAttributeType().getSyntax();
+                    if (syntax != null)
+                    {
+                      for (AttributeValue v : a.getValues())
+                      {
+                        if (! syntax.valueIsAcceptable(v.getValue(),
+                                                       invalidReason))
+                        {
+                          logError(ErrorLogCategory.SCHEMA,
+                                   ErrorLogSeverity.SEVERE_WARNING,
+                                   MSGID_ADD_OP_INVALID_SYNTAX,
+                                   String.valueOf(entryDN),
+                                   String.valueOf(v.getStringValue()),
+                                   String.valueOf(a.getName()),
+                                   String.valueOf(invalidReason));
+                        }
+                      }
+                    }
+                  }
+                }
+
+                break;
+            }
+          }
+
+
+          // See if the entry contains any attributes or object classes marked
+          // OBSOLETE.  If so, then reject the entry.
+          for (AttributeType at : userAttributes.keySet())
+          {
+            if (at.isObsolete())
+            {
+              int    msgID   = MSGID_ADD_ATTR_IS_OBSOLETE;
+              String message = getMessage(msgID, String.valueOf(entryDN),
+                                          at.getNameOrOID());
+              localOp.appendErrorMessage(message);
+              localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+              break addProcessing;
+            }
+          }
+
+          for (AttributeType at : operationalAttributes.keySet())
+          {
+            if (at.isObsolete())
+            {
+              int    msgID   = MSGID_ADD_ATTR_IS_OBSOLETE;
+              String message = getMessage(msgID, String.valueOf(entryDN),
+                                          at.getNameOrOID());
+              localOp.appendErrorMessage(message);
+              localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+              break addProcessing;
+            }
+          }
+
+          for (ObjectClass oc : objectClasses.keySet())
+          {
+            if (oc.isObsolete())
+            {
+              int    msgID   = MSGID_ADD_OC_IS_OBSOLETE;
+              String message = getMessage(msgID, String.valueOf(entryDN),
+                                          oc.getNameOrOID());
+              localOp.appendErrorMessage(message);
+              localOp.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
+              break addProcessing;
+            }
+          }
+        }
+
+        // Check to see if there are any controls in the request. If so,
+        // then
+        // see if there is any special processing required.
+        boolean                    noOp            = false;
+        LDAPPostReadRequestControl postReadRequest = null;
+        List<Control> requestControls = localOp.getRequestControls();
+        if ((requestControls != null) && (! requestControls.isEmpty()))
+        {
+          for (int i=0; i < requestControls.size(); i++)
+          {
+            Control c   = requestControls.get(i);
+            String  oid = c.getOID();
+
+            if (oid.equals(OID_LDAP_ASSERTION))
+            {
+              LDAPAssertionRequestControl assertControl;
+              if (c instanceof LDAPAssertionRequestControl)
+              {
+                assertControl = (LDAPAssertionRequestControl) c;
+              }
+              else
+              {
+                try
+                {
+                  assertControl = LDAPAssertionRequestControl.decodeControl(c);
+                  requestControls.set(i, assertControl);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break addProcessing;
+                }
+              }
+
+              try
+              {
+                // FIXME -- We need to determine whether the current user has
+                //          permission to make this determination.
+                SearchFilter filter = assertControl.getSearchFilter();
+                if (! filter.matchesEntry(entry))
+                {
+                  localOp.setResultCode(ResultCode.ASSERTION_FAILED);
+
+                  localOp.appendErrorMessage(getMessage(
+                                                MSGID_ADD_ASSERTION_FAILED,
+                                                String.valueOf(entryDN)));
+
+                  break addProcessing;
+                }
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(ResultCode.PROTOCOL_ERROR);
+
+                int msgID = MSGID_ADD_CANNOT_PROCESS_ASSERTION_FILTER;
+                localOp.appendErrorMessage(getMessage(msgID,
+                                                      String.valueOf(entryDN),
+                                                      de.getErrorMessage()));
+
+                break addProcessing;
+              }
+            }
+            else if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED))
+            {
+              noOp = true;
+            }
+            else if (oid.equals(OID_LDAP_READENTRY_POSTREAD))
+            {
+              if (c instanceof LDAPAssertionRequestControl)
+              {
+                postReadRequest = (LDAPPostReadRequestControl) c;
+              }
+              else
+              {
+                try
+                {
+                  postReadRequest = LDAPPostReadRequestControl.decodeControl(c);
+                  requestControls.set(i, postReadRequest);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break addProcessing;
+                }
+              }
+            }
+            else if (oid.equals(OID_PROXIED_AUTH_V1))
+            {
+              // The requester must have the PROXIED_AUTH privilige in order to
+              // be able to use this control.
+              if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH,
+                  localOp))
+              {
+                int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+                break addProcessing;
+              }
+
+
+              ProxiedAuthV1Control proxyControl;
+              if (c instanceof ProxiedAuthV1Control)
+              {
+                proxyControl = (ProxiedAuthV1Control) c;
+              }
+              else
+              {
+                try
+                {
+                  proxyControl = ProxiedAuthV1Control.decodeControl(c);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break addProcessing;
+                }
+              }
+
+
+              Entry authorizationEntry;
+              try
+              {
+                authorizationEntry = proxyControl.getAuthorizationEntry();
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(de.getResultCode());
+                localOp.appendErrorMessage(de.getErrorMessage());
+
+                break addProcessing;
+              }
+
+              if (AccessControlConfigManager.getInstance()
+                  .getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                      authorizationEntry) == false) {
+                localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+                int msgID = MSGID_ADD_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+                localOp.appendErrorMessage(getMessage(msgID,
+                    String.valueOf(entryDN)));
+
+                skipPostOperation = true;
+                break addProcessing;
+              }
+              localOp.setAuthorizationEntry(authorizationEntry);
+              if (authorizationEntry == null)
+              {
+                localOp.setProxiedAuthorizationDN(DN.nullDN());
+              }
+              else
+              {
+                localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+              }
+            }
+            else if (oid.equals(OID_PROXIED_AUTH_V2))
+            {
+              // The requester must have the PROXIED_AUTH privilige in order to
+              // be able to use this control.
+              if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH,
+                  localOp))
+              {
+                int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+                break addProcessing;
+              }
+
+
+              ProxiedAuthV2Control proxyControl;
+              if (c instanceof ProxiedAuthV2Control)
+              {
+                proxyControl = (ProxiedAuthV2Control) c;
+              }
+              else
+              {
+                try
+                {
+                  proxyControl = ProxiedAuthV2Control.decodeControl(c);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break addProcessing;
+                }
+              }
+
+
+              Entry authorizationEntry;
+              try
+              {
+                authorizationEntry = proxyControl.getAuthorizationEntry();
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(de.getResultCode());
+                localOp.appendErrorMessage(de.getErrorMessage());
+
+                break addProcessing;
+              }
+
+              if (AccessControlConfigManager.getInstance()
+                      .getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                      authorizationEntry) == false) {
+                localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+                int msgID = MSGID_ADD_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+                localOp.appendErrorMessage(getMessage(msgID,
+                    String.valueOf(entryDN)));
+
+                skipPostOperation = true;
+                break addProcessing;
+              }
+              localOp.setAuthorizationEntry(authorizationEntry);
+              if (authorizationEntry == null)
+              {
+                localOp.setProxiedAuthorizationDN(DN.nullDN());
+              }
+              else
+              {
+                localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+              }
+            }
+
+            // NYI -- Add support for additional controls.
+            else if (c.isCritical())
+            {
+              if ((backend == null) || (! backend.supportsControl(oid)))
+              {
+                localOp.setResultCode(
+                    ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+
+                int msgID = MSGID_ADD_UNSUPPORTED_CRITICAL_CONTROL;
+                localOp.appendErrorMessage(getMessage(msgID,
+                                              String.valueOf(entryDN),
+                                              oid));
+
+                break addProcessing;
+              }
+            }
+          }
+        }
+
+
+        // Check to see if the client has permission to perform the add.
+
+        // FIXME: for now assume that this will check all permission
+        // pertinent to the operation. This includes proxy authorization
+        // and any other controls specified.
+
+        // FIXME: earlier checks to see if the entry already exists or
+        // if the parent entry does not exist may have already exposed
+        // sensitive information to the client.
+        if (AccessControlConfigManager.getInstance()
+            .getAccessControlHandler().isAllowed(localOp) == false) {
+          localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+          int msgID = MSGID_ADD_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+          localOp.appendErrorMessage(getMessage(msgID,
+                                                String.valueOf(entryDN)));
+
+          skipPostOperation = true;
+          break addProcessing;
+        }
+
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+
+        // If the operation is not a synchronization operation,
+        // Invoke the pre-operation modify plugins.
+        if (!localOp.isSynchronizationOperation())
+        {
+          PreOperationPluginResult preOpResult =
+            pluginConfigManager.invokePreOperationAddPlugins(localOp);
+          if (preOpResult.connectionTerminated())
+          {
+            // There's no point in continuing with anything.  Log the result
+            // and return.
+            localOp.setResultCode(ResultCode.CANCELED);
+
+            int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+            localOp.appendErrorMessage(getMessage(msgID));
+
+            localOp.setProcessingStopTime();
+            return;
+          }
+          else if (preOpResult.sendResponseImmediately())
+          {
+            skipPostOperation = true;
+            break addProcessing;
+          }
+          else if (preOpResult.skipCoreProcessing())
+          {
+            skipPostOperation = false;
+            break addProcessing;
+          }
+        }
+
+
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+
+        // Actually perform the add operation.  This should also include taking
+        // care of any synchronization that might be needed.
+        Backend backend = DirectoryServer.getBackend(entryDN);
+        if (backend == null)
+        {
+          localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+          localOp.appendErrorMessage("No backend for entry " +
+              entryDN.toString());
+        }
+        else
+        {
+          // If it is not a private backend, then check to see if the server or
+          // backend is operating in read-only mode.
+          if (! backend.isPrivateBackend())
+          {
+            switch (DirectoryServer.getWritabilityMode())
+            {
+              case DISABLED:
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                localOp.appendErrorMessage(getMessage(MSGID_ADD_SERVER_READONLY,
+                                              String.valueOf(entryDN)));
+                break addProcessing;
+
+              case INTERNAL_ONLY:
+                if (! (localOp.isInternalOperation() ||
+                    localOp.isSynchronizationOperation()))
+                {
+                  localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                  localOp.appendErrorMessage(getMessage(
+                                                MSGID_ADD_SERVER_READONLY,
+                                                String.valueOf(entryDN)));
+                  break addProcessing;
+                }
+            }
+
+            switch (backend.getWritabilityMode())
+            {
+              case DISABLED:
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                localOp.appendErrorMessage(getMessage(
+                                              MSGID_ADD_BACKEND_READONLY,
+                                              String.valueOf(entryDN)));
+                break addProcessing;
+
+              case INTERNAL_ONLY:
+                if (! (localOp.isInternalOperation() ||
+                    localOp.isSynchronizationOperation()))
+                {
+                  localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                  localOp.appendErrorMessage(getMessage(
+                                                MSGID_ADD_BACKEND_READONLY,
+                                                String.valueOf(entryDN)));
+                  break addProcessing;
+                }
+            }
+          }
+
+
+          try
+          {
+            if (noOp)
+            {
+              localOp.appendErrorMessage(getMessage(MSGID_ADD_NOOP));
+
+              // FIXME -- We must set a result code other than SUCCESS.
+            }
+            else
+            {
+              for (SynchronizationProvider provider :
+                   DirectoryServer.getSynchronizationProviders())
+              {
+                try
+                {
+                  SynchronizationProviderResult result =
+                       provider.doPreOperation(localOp);
+                  if (! result.continueOperationProcessing())
+                  {
+                    break addProcessing;
+                  }
+                }
+                catch (DirectoryException de)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                  }
+
+                  logError(ErrorLogCategory.SYNCHRONIZATION,
+                           ErrorLogSeverity.SEVERE_ERROR,
+                           MSGID_ADD_SYNCH_PREOP_FAILED,
+                           localOp.getConnectionID(),
+                           localOp.getOperationID(),
+                           getExceptionMessage(de));
+
+                  localOp.setResponseData(de);
+                  break addProcessing;
+                }
+              }
+
+              backend.addEntry(entry, localOp);
+            }
+
+            if (postReadRequest != null)
+            {
+              Entry addedEntry = entry.duplicate(true);
+
+              if (! postReadRequest.allowsAttribute(
+                         DirectoryServer.getObjectClassAttributeType()))
+              {
+                addedEntry.removeAttribute(
+                     DirectoryServer.getObjectClassAttributeType());
+              }
+
+              if (! postReadRequest.returnAllUserAttributes())
+              {
+                Iterator<AttributeType> iterator =
+                     addedEntry.getUserAttributes().keySet().iterator();
+                while (iterator.hasNext())
+                {
+                  AttributeType attrType = iterator.next();
+                  if (! postReadRequest.allowsAttribute(attrType))
+                  {
+                    iterator.remove();
+                  }
+                }
+              }
+
+              if (! postReadRequest.returnAllOperationalAttributes())
+              {
+                Iterator<AttributeType> iterator =
+                     addedEntry.getOperationalAttributes().keySet().iterator();
+                while (iterator.hasNext())
+                {
+                  AttributeType attrType = iterator.next();
+                  if (! postReadRequest.allowsAttribute(attrType))
+                  {
+                    iterator.remove();
+                  }
+                }
+              }
+
+              // FIXME -- Check access controls on the entry to see if it should
+              //          be returned or if any attributes need to be stripped
+              //          out..
+              SearchResultEntry searchEntry = new SearchResultEntry(addedEntry);
+              LDAPPostReadResponseControl responseControl =
+                   new LDAPPostReadResponseControl(postReadRequest.getOID(),
+                                                   postReadRequest.isCritical(),
+                                                   searchEntry);
+
+              localOp.addResponseControl(responseControl);
+            }
+
+            localOp.setResultCode(ResultCode.SUCCESS);
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            localOp.setResultCode(de.getResultCode());
+            localOp.appendErrorMessage(de.getErrorMessage());
+            localOp.setMatchedDN(de.getMatchedDN());
+            localOp.setReferralURLs(de.getReferralURLs());
+
+            break addProcessing;
+          }
+          catch (CancelledOperationException coe)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, coe);
+            }
+
+            CancelResult cancelResult = coe.getCancelResult();
+
+            localOp.setCancelResult(cancelResult);
+            localOp.setResultCode(cancelResult.getResultCode());
+
+            String message = coe.getMessage();
+            if ((message != null) && (message.length() > 0))
+            {
+              localOp.appendErrorMessage(message);
+            }
+
+            break addProcessing;
+          }
+        }
+      }
+      finally
+      {
+        if (entryLock != null)
+        {
+          LockManager.unlock(entryDN, entryLock);
+        }
+
+        if (parentLock != null)
+        {
+          LockManager.unlock(parentDN, parentLock);
+        }
+
+
+        for (SynchronizationProvider provider :
+             DirectoryServer.getSynchronizationProviders())
+        {
+          try
+          {
+            provider.doPostOperation(localOp);
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            logError(ErrorLogCategory.SYNCHRONIZATION,
+                     ErrorLogSeverity.SEVERE_ERROR,
+                     MSGID_ADD_SYNCH_POSTOP_FAILED,
+                     localOp.getConnectionID(),
+                     localOp.getOperationID(),
+                     getExceptionMessage(de));
+
+            localOp.setResponseData(de);
+            break;
+          }
+        }
+      }
+    }
+
+
+    // Indicate that it is now too late to attempt to cancel the operation.
+    localOp.setCancelResult(CancelResult.TOO_LATE);
+
+
+    // Invoke the post-operation add plugins.
+    if (! skipPostOperation)
+    {
+      // FIXME -- Should this also be done while holding the locks?
+      PostOperationPluginResult postOpResult =
+           pluginConfigManager.invokePostOperationAddPlugins(localOp);
+      if (postOpResult.connectionTerminated())
+      {
+        // There's no point in continuing with anything.  Log the result and
+        // return.
+        localOp.setResultCode(ResultCode.CANCELED);
+
+        int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+        localOp.appendErrorMessage(getMessage(msgID));
+
+        localOp.setProcessingStopTime();
+        return;
+      }
+    }
+
+
+    // Notify any change notification listeners that might be registered with
+    // the server.
+    if ((localOp.getResultCode() == ResultCode.SUCCESS) &&
+        (localOp.getEntryToAdd() != null))
+    {
+      for (ChangeNotificationListener changeListener :
+           DirectoryServer.getChangeNotificationListeners())
+      {
+        try
+        {
+          changeListener.handleAddOperation(localOp, localOp.getEntryToAdd());
+        }
+        catch (Exception e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+
+          int    msgID   = MSGID_ADD_ERROR_NOTIFYING_CHANGE_LISTENER;
+          String message = getMessage(msgID, getExceptionMessage(e));
+          logError(ErrorLogCategory.CORE_SERVER, ErrorLogSeverity.SEVERE_ERROR,
+                   message, msgID);
+        }
+      }
+    }
+
+
+    // Stop the processing timer.
+    localOp.setProcessingStopTime();
+
+  }
+
+
+  /**
+   * Perform a delete operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  public void processDelete(DeleteOperation operation){
+    LocalBackendDeleteOperation localOperation =
+      new LocalBackendDeleteOperation(operation);
+    processLocalDelete(localOperation);
+  }
+
+  /**
+   * Perform a local delete operation against a local backend.
+   *
+   * @param operation - The operation to perform
+   */
+  private void processLocalDelete(LocalBackendDeleteOperation localOp)
+  {
+    ClientConnection clientConnection = localOp.getClientConnection();
+
+    // Get the plugin config manager that will be used for invoking plugins.
+    PluginConfigManager pluginConfigManager =
+         DirectoryServer.getPluginConfigManager();
+    boolean skipPostOperation = false;
+
+    // Check for and handle a request to cancel this operation.
+    if (localOp.getCancelRequest() != null)
+    {
+      localOp.indicateCancelled(localOp.getCancelRequest());
+      localOp.setProcessingStopTime();
+      return;
+    }
+
+    // Create a labeled block of code that we can break out of if a problem is
+    // detected.
+deleteProcessing:
+    {
+      // Process the entry DN to convert it from its raw form as provided by the
+      // client to the form required for the rest of the delete processing.
+      DN entryDN = localOp.getEntryDN();
+      if (entryDN == null){
+        break deleteProcessing;
+      }
+
+      // Grab a write lock on the entry.
+      Lock entryLock = null;
+      for (int i=0; i < 3; i++)
+      {
+        entryLock = LockManager.lockWrite(entryDN);
+        if (entryLock != null)
+        {
+          break;
+        }
+      }
+
+      if (entryLock == null)
+      {
+        localOp.setResultCode(DirectoryServer.getServerErrorResultCode());
+        localOp.appendErrorMessage(getMessage(MSGID_DELETE_CANNOT_LOCK_ENTRY,
+                                      String.valueOf(entryDN)));
+        break deleteProcessing;
+      }
+
+      Entry entry = null;
+      try
+      {
+        // Get the entry to delete.  If it doesn't exist, then fail.
+        try
+        {
+          entry = backend.getEntry(entryDN);
+          localOp.setEntryToDelete(entry);
+          if (entry == null)
+          {
+            localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+            localOp.appendErrorMessage(getMessage(MSGID_DELETE_NO_SUCH_ENTRY,
+                                          String.valueOf(entryDN)));
+
+            try
+            {
+              DN parentDN = entryDN.getParentDNInSuffix();
+              while (parentDN != null)
+              {
+                if (DirectoryServer.entryExists(parentDN))
+                {
+                  localOp.setMatchedDN(parentDN);
+                  break;
+                }
+
+                parentDN = parentDN.getParentDNInSuffix();
+              }
+            }
+            catch (Exception e)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, e);
+              }
+            }
+
+            break deleteProcessing;
+          }
+        }
+        catch (DirectoryException de)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, de);
+          }
+
+          localOp.setResultCode(de.getResultCode());
+          localOp.appendErrorMessage(de.getErrorMessage());
+          localOp.setMatchedDN(de.getMatchedDN());
+          localOp.setReferralURLs(de.getReferralURLs());
+          break deleteProcessing;
+        }
+
+
+        // Invoke any conflict resolution processing that might be needed by the
+        // synchronization provider.
+        for (SynchronizationProvider provider :
+             DirectoryServer.getSynchronizationProviders())
+        {
+          try
+          {
+            SynchronizationProviderResult result =
+                 provider.handleConflictResolution(localOp);
+            if (! result.continueOperationProcessing())
+            {
+              break deleteProcessing;
+            }
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            logError(ErrorLogCategory.SYNCHRONIZATION,
+                     ErrorLogSeverity.SEVERE_ERROR,
+                     MSGID_DELETE_SYNCH_CONFLICT_RESOLUTION_FAILED,
+                     localOp.getConnectionID(),
+                     localOp.getOperationID(),
+                     getExceptionMessage(de));
+
+            localOp.setResponseData(de);
+            break deleteProcessing;
+          }
+        }
+
+        // Check to see if the client has permission to perform the
+        // delete.
+
+        // Check to see if there are any controls in the request.  If so, then
+        // see if there is any special processing required.
+        boolean                   noOp           = false;
+        LDAPPreReadRequestControl preReadRequest = null;
+        List<Control> requestControls =
+          localOp.getRequestControls();
+        if ((requestControls != null) && (! requestControls.isEmpty()))
+        {
+          for (int i=0; i < requestControls.size(); i++)
+          {
+            Control c   = requestControls.get(i);
+            String  oid = c.getOID();
+
+            if (oid.equals(OID_LDAP_ASSERTION))
+            {
+              LDAPAssertionRequestControl assertControl;
+              if (c instanceof LDAPAssertionRequestControl)
+              {
+                assertControl = (LDAPAssertionRequestControl) c;
+              }
+              else
+              {
+                try
+                {
+                  assertControl = LDAPAssertionRequestControl.decodeControl(c);
+                  requestControls.set(i, assertControl);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break deleteProcessing;
+                }
+              }
+
+              try
+              {
+                // FIXME -- We need to determine whether the current user has
+                //          permission to make this determination.
+                SearchFilter filter = assertControl.getSearchFilter();
+                if (! filter.matchesEntry(entry))
+                {
+                  localOp.setResultCode(ResultCode.ASSERTION_FAILED);
+
+                  localOp.appendErrorMessage(getMessage(
+                                                MSGID_DELETE_ASSERTION_FAILED,
+                                                String.valueOf(entryDN)));
+
+                  break deleteProcessing;
+                }
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(ResultCode.PROTOCOL_ERROR);
+
+                int msgID = MSGID_DELETE_CANNOT_PROCESS_ASSERTION_FILTER;
+                localOp.appendErrorMessage(getMessage(msgID,
+                                              String.valueOf(entryDN),
+                                              de.getErrorMessage()));
+
+                break deleteProcessing;
+              }
+            }
+            else if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED))
+            {
+              noOp = true;
+            }
+            else if (oid.equals(OID_LDAP_READENTRY_PREREAD))
+            {
+              if (c instanceof LDAPAssertionRequestControl)
+              {
+                preReadRequest = (LDAPPreReadRequestControl) c;
+              }
+              else
+              {
+                try
+                {
+                  preReadRequest = LDAPPreReadRequestControl.decodeControl(c);
+                  requestControls.set(i, preReadRequest);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break deleteProcessing;
+                }
+              }
+            }
+            else if (oid.equals(OID_PROXIED_AUTH_V1))
+            {
+              // The requester must have the PROXIED_AUTH privilige in order to
+              // be able to use this control.
+              if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH,
+                  localOp))
+              {
+                int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+                break deleteProcessing;
+              }
+
+
+              ProxiedAuthV1Control proxyControl;
+              if (c instanceof ProxiedAuthV1Control)
+              {
+                proxyControl = (ProxiedAuthV1Control) c;
+              }
+              else
+              {
+                try
+                {
+                  proxyControl = ProxiedAuthV1Control.decodeControl(c);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break deleteProcessing;
+                }
+              }
+
+
+              Entry authorizationEntry;
+              try
+              {
+                authorizationEntry = proxyControl.getAuthorizationEntry();
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(de.getResultCode());
+                localOp.appendErrorMessage(de.getErrorMessage());
+
+                break deleteProcessing;
+              }
+
+              if (AccessControlConfigManager.getInstance()
+                  .getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                      authorizationEntry) == false) {
+                localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+                int msgID = MSGID_DELETE_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN)));
+
+                skipPostOperation = true;
+                break deleteProcessing;
+              }
+              localOp.setAuthorizationEntry(authorizationEntry);
+              if (authorizationEntry == null)
+              {
+                localOp.setProxiedAuthorizationDN(DN.nullDN());
+              }
+              else
+              {
+                localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+              }
+            }
+            else if (oid.equals(OID_PROXIED_AUTH_V2))
+            {
+              // The requester must have the PROXIED_AUTH privilige in order to
+              // be able to use this control.
+              if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH,
+                  localOp))
+              {
+                int msgID = MSGID_PROXYAUTH_INSUFFICIENT_PRIVILEGES;
+                localOp.appendErrorMessage(getMessage(msgID));
+                localOp.setResultCode(ResultCode.AUTHORIZATION_DENIED);
+                break deleteProcessing;
+              }
+
+
+              ProxiedAuthV2Control proxyControl;
+              if (c instanceof ProxiedAuthV2Control)
+              {
+                proxyControl = (ProxiedAuthV2Control) c;
+              }
+              else
+              {
+                try
+                {
+                  proxyControl = ProxiedAuthV2Control.decodeControl(c);
+                }
+                catch (LDAPException le)
+                {
+                  if (debugEnabled())
+                  {
+                    TRACER.debugCaught(DebugLogLevel.ERROR, le);
+                  }
+
+                  localOp.setResultCode(ResultCode.valueOf(le.getResultCode()));
+                  localOp.appendErrorMessage(le.getMessage());
+
+                  break deleteProcessing;
+                }
+              }
+
+
+              Entry authorizationEntry;
+              try
+              {
+                authorizationEntry = proxyControl.getAuthorizationEntry();
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                localOp.setResultCode(de.getResultCode());
+                localOp.appendErrorMessage(de.getErrorMessage());
+
+                break deleteProcessing;
+              }
+
+              if (AccessControlConfigManager.getInstance()
+                  .getAccessControlHandler().isProxiedAuthAllowed(localOp,
+                      authorizationEntry) == false) {
+                localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+                int msgID = MSGID_DELETE_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+                localOp.appendErrorMessage(
+                    getMessage(msgID, String.valueOf(entryDN)));
+
+                skipPostOperation = true;
+                break deleteProcessing;
+              }
+              localOp.setAuthorizationEntry(authorizationEntry);
+              if (authorizationEntry == null)
+              {
+                localOp.setProxiedAuthorizationDN(DN.nullDN());
+              }
+              else
+              {
+                localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
+              }
+            }
+
+            // NYI -- Add support for additional controls.
+            else if (c.isCritical())
+            {
+              Backend backend = DirectoryServer.getBackend(entryDN);
+              if ((backend == null) || (! backend.supportsControl(oid)))
+              {
+                localOp.setResultCode(
+                    ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+
+                int msgID = MSGID_DELETE_UNSUPPORTED_CRITICAL_CONTROL;
+                localOp.appendErrorMessage(getMessage(msgID,
+                                              String.valueOf(entryDN),
+                                              oid));
+
+                break deleteProcessing;
+              }
+            }
+          }
+        }
+
+
+        // FIXME: for now assume that this will check all permission
+        // pertinent to the operation. This includes proxy authorization
+        // and any other controls specified.
+
+        // FIXME: earlier checks to see if the entry already exists may
+        // have already exposed sensitive information to the client.
+        if (AccessControlConfigManager.getInstance()
+            .getAccessControlHandler().isAllowed(localOp) == false) {
+          localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+
+          int msgID = MSGID_DELETE_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS;
+          localOp.appendErrorMessage(getMessage(msgID,
+              String.valueOf(entryDN)));
+
+          skipPostOperation = true;
+          break deleteProcessing;
+        }
+
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+
+        // If the operation is not a synchronization operation,
+        // invoke the pre-delete plugins.
+        if (!localOp.isSynchronizationOperation())
+        {
+          PreOperationPluginResult preOpResult =
+            pluginConfigManager.invokePreOperationDeletePlugins(localOp);
+          if (preOpResult.connectionTerminated())
+          {
+            // There's no point in continuing with anything.  Log the request
+            // and result and return.
+            localOp.setResultCode(ResultCode.CANCELED);
+
+            int msgID = MSGID_CANCELED_BY_PREOP_DISCONNECT;
+            localOp.appendErrorMessage(getMessage(msgID));
+
+            localOp.setProcessingStopTime();
+            return;
+          }
+          else if (preOpResult.sendResponseImmediately())
+          {
+            skipPostOperation = true;
+            break deleteProcessing;
+          }
+          else if (preOpResult.skipCoreProcessing())
+          {
+            skipPostOperation = false;
+            break deleteProcessing;
+          }
+        }
+
+
+        // Check for and handle a request to cancel this operation.
+        if (localOp.getCancelRequest() != null)
+        {
+          localOp.indicateCancelled(localOp.getCancelRequest());
+          localOp.setProcessingStopTime();
+          return;
+        }
+
+
+        // Get the backend to use for the delete.  If there is none, then fail.
+        if (backend == null)
+        {
+          localOp.setResultCode(ResultCode.NO_SUCH_OBJECT);
+          localOp.appendErrorMessage(getMessage(MSGID_DELETE_NO_SUCH_ENTRY,
+                                        String.valueOf(entryDN)));
+          break deleteProcessing;
+        }
+
+
+        // If it is not a private backend, then check to see if the server or
+        // backend is operating in read-only mode.
+        if (! backend.isPrivateBackend())
+        {
+          switch (DirectoryServer.getWritabilityMode())
+          {
+            case DISABLED:
+              localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+              localOp.appendErrorMessage(getMessage(
+                                            MSGID_DELETE_SERVER_READONLY,
+                                            String.valueOf(entryDN)));
+              break deleteProcessing;
+
+            case INTERNAL_ONLY:
+              if (! (localOp.isInternalOperation() ||
+                  localOp.isSynchronizationOperation()))
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                localOp.appendErrorMessage(getMessage(
+                                              MSGID_DELETE_SERVER_READONLY,
+                                              String.valueOf(entryDN)));
+                break deleteProcessing;
+              }
+          }
+
+          switch (backend.getWritabilityMode())
+          {
+            case DISABLED:
+              localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+              localOp.appendErrorMessage(getMessage(
+                                            MSGID_DELETE_BACKEND_READONLY,
+                                            String.valueOf(entryDN)));
+              break deleteProcessing;
+
+            case INTERNAL_ONLY:
+              if (! (localOp.isInternalOperation() ||
+                  localOp.isSynchronizationOperation()))
+              {
+                localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+                localOp.appendErrorMessage(getMessage(
+                                              MSGID_DELETE_BACKEND_READONLY,
+                                              String.valueOf(entryDN)));
+                break deleteProcessing;
+              }
+          }
+        }
+
+
+        // The selected backend will have the responsibility of making sure that
+        // the entry actually exists and does not have any children (or possibly
+        // handling a subtree delete).  But we will need to check if there are
+        // any subordinate backends that should stop us from attempting the
+        // delete.
+        Backend[] subBackends = backend.getSubordinateBackends();
+        for (Backend b : subBackends)
+        {
+          DN[] baseDNs = b.getBaseDNs();
+          for (DN dn : baseDNs)
+          {
+            if (dn.isDescendantOf(entryDN))
+            {
+              localOp.setResultCode(ResultCode.NOT_ALLOWED_ON_NONLEAF);
+              localOp.appendErrorMessage(getMessage(
+                                            MSGID_DELETE_HAS_SUB_BACKEND,
+                                            String.valueOf(entryDN),
+                                            String.valueOf(dn)));
+              break deleteProcessing;
+            }
+          }
+        }
+
+
+        // Actually perform the delete.
+        try
+        {
+          if (noOp)
+          {
+            localOp.appendErrorMessage(getMessage(MSGID_DELETE_NOOP));
+
+            // FIXME -- We must set a result code other than SUCCESS.
+          }
+          else
+          {
+            for (SynchronizationProvider provider :
+                 DirectoryServer.getSynchronizationProviders())
+            {
+              try
+              {
+                SynchronizationProviderResult result =
+                     provider.doPreOperation(localOp);
+                if (! result.continueOperationProcessing())
+                {
+                  break deleteProcessing;
+                }
+              }
+              catch (DirectoryException de)
+              {
+                if (debugEnabled())
+                {
+                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
+                }
+
+                logError(ErrorLogCategory.SYNCHRONIZATION,
+                         ErrorLogSeverity.SEVERE_ERROR,
+                         MSGID_DELETE_SYNCH_PREOP_FAILED,
+                         localOp.getConnectionID(),
+                         localOp.getOperationID(),
+                         getExceptionMessage(de));
+
+                localOp.setResponseData(de);
+                break deleteProcessing;
+              }
+            }
+
+            backend.deleteEntry(entryDN, localOp);
+          }
+
+          if (preReadRequest != null)
+          {
+            Entry entryCopy = entry.duplicate(true);
+
+            if (! preReadRequest.allowsAttribute(
+                       DirectoryServer.getObjectClassAttributeType()))
+            {
+              entryCopy.removeAttribute(
+                   DirectoryServer.getObjectClassAttributeType());
+            }
+
+            if (! preReadRequest.returnAllUserAttributes())
+            {
+              Iterator<AttributeType> iterator =
+                   entryCopy.getUserAttributes().keySet().iterator();
+              while (iterator.hasNext())
+              {
+                AttributeType attrType = iterator.next();
+                if (! preReadRequest.allowsAttribute(attrType))
+                {
+                  iterator.remove();
+                }
+              }
+            }
+
+            if (! preReadRequest.returnAllOperationalAttributes())
+            {
+              Iterator<AttributeType> iterator =
+                   entryCopy.getOperationalAttributes().keySet().iterator();
+              while (iterator.hasNext())
+              {
+                AttributeType attrType = iterator.next();
+                if (! preReadRequest.allowsAttribute(attrType))
+                {
+                  iterator.remove();
+                }
+              }
+            }
+
+            // FIXME -- Check access controls on the entry to see if it should
+            //          be returned or if any attributes need to be stripped
+            //          out..
+            SearchResultEntry searchEntry = new SearchResultEntry(entryCopy);
+            LDAPPreReadResponseControl responseControl =
+                 new LDAPPreReadResponseControl(preReadRequest.getOID(),
+                                                preReadRequest.isCritical(),
+                                                searchEntry);
+
+            localOp.addResponseControl(responseControl);
+          }
+
+          localOp.setResultCode(ResultCode.SUCCESS);
+        }
+        catch (DirectoryException de)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, de);
+          }
+
+          localOp.setResultCode(de.getResultCode());
+          localOp.appendErrorMessage(de.getErrorMessage());
+          localOp.setMatchedDN(de.getMatchedDN());
+          localOp.setReferralURLs(de.getReferralURLs());
+
+          break deleteProcessing;
+        }
+        catch (CancelledOperationException coe)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, coe);
+          }
+
+          CancelResult cancelResult = coe.getCancelResult();
+
+          localOp.setCancelResult(cancelResult);
+          localOp.setResultCode(cancelResult.getResultCode());
+
+          String message = coe.getMessage();
+          if ((message != null) && (message.length() > 0))
+          {
+            localOp.appendErrorMessage(message);
+          }
+
+          break deleteProcessing;
+        }
+      }
+      finally
+      {
+        LockManager.unlock(entryDN, entryLock);
+
+        for (SynchronizationProvider provider :
+             DirectoryServer.getSynchronizationProviders())
+        {
+          try
+          {
+            provider.doPostOperation(localOp);
+          }
+          catch (DirectoryException de)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, de);
+            }
+
+            logError(ErrorLogCategory.SYNCHRONIZATION,
+                     ErrorLogSeverity.SEVERE_ERROR,
+                     MSGID_DELETE_SYNCH_POSTOP_FAILED,
+                     localOp.getConnectionID(),
+                     localOp.getOperationID(),
+                     getExceptionMessage(de));
+
+            localOp.setResponseData(de);
+            break;
+          }
+        }
+      }
+    }
+
+
+    // Indicate that it is now too late to attempt to cancel the operation.
+    localOp.setCancelResult(CancelResult.TOO_LATE);
+
+
+    // Invoke the post-operation delete plugins.
+    if (! skipPostOperation)
+    {
+      PostOperationPluginResult postOperationResult =
+           pluginConfigManager.invokePostOperationDeletePlugins(localOp);
+      if (postOperationResult.connectionTerminated())
+      {
+        localOp.setResultCode(ResultCode.CANCELED);
+
+        int msgID = MSGID_CANCELED_BY_POSTOP_DISCONNECT;
+        localOp.appendErrorMessage(getMessage(msgID));
+
+        localOp.setProcessingStopTime();
+        return;
+      }
+    }
+
+
+    // Notify any change notification listeners that might be registered with
+    // the server.
+    if (localOp.getResultCode() == ResultCode.SUCCESS)
+    {
+      for (ChangeNotificationListener changeListener :
+           DirectoryServer.getChangeNotificationListeners())
+      {
+        try
+        {
+          changeListener.handleDeleteOperation(localOp,
+              localOp.getEntryToDelete());
+        }
+        catch (Exception e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+
+          int    msgID   = MSGID_DELETE_ERROR_NOTIFYING_CHANGE_LISTENER;
+          String message = getMessage(msgID, getExceptionMessage(e));
+          logError(ErrorLogCategory.CORE_SERVER, ErrorLogSeverity.SEVERE_ERROR,
+                   message, msgID);
+        }
+      }
+    }
+
+    // Stop the processing timer.
+    localOp.setProcessingStopTime();
+  }
+
+
+  /**
+   * Attaches the current local operation to the global operation so that
+   * operation runner can execute local operation post response later on.
+   *
+   * @param <O>              subtype of Operation
+   * @param <L>              subtype of LocalBackendOperation
+   * @param globalOperation  the global operation to which local operation
+   *                         should be attached to
+   * @param currentLocalOperation  the local operation to attach to the global
+   *                               operation
+   */
+  @SuppressWarnings("unchecked")
+  public static final <O extends Operation, L>
+  void attachLocalOperation (
+      O globalOperation,
+      L currentLocalOperation
+      )
+  {
+    List<?> existingAttachment =
+      (List<?>) globalOperation.getAttachment(Operation.LOCALBACKENDOPERATIONS);
+
+    List<L> newAttachment = new ArrayList<L>();
+
+    if (existingAttachment != null)
+    {
+      // This line raises an unchecked conversion warning.
+      // There is nothing we can do to prevent this warning
+      // so let's get rid of it since we know the cast is safe.
+      newAttachment.addAll ((List<L>) existingAttachment);
+    }
+    newAttachment.add (currentLocalOperation);
+    globalOperation.setAttachment(
+        Operation.LOCALBACKENDOPERATIONS, newAttachment);
+  }
+
+}

--
Gitblit v1.10.0