/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 */
package org.opends.server.backends.ndb;

import com.mysql.cluster.ndbj.Ndb;
import com.mysql.cluster.ndbj.NdbApiException;
import com.mysql.cluster.ndbj.NdbBlob;
import com.mysql.cluster.ndbj.NdbIndexScanOperation;
import com.mysql.cluster.ndbj.NdbOperation;
import com.mysql.cluster.ndbj.NdbOperation.AbortOption;
import com.mysql.cluster.ndbj.NdbResultSet;
import com.mysql.cluster.ndbj.NdbScanOperation;
import com.mysql.cluster.ndbj.NdbTransaction;
import com.mysql.cluster.ndbj.NdbTransaction.ExecType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.DN;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.AttributeValues;
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.ObjectClassType;

import static org.opends.server.util.ServerConstants.ATTR_REFERRAL_URL;
import static org.opends.server.loggers.debug.DebugLogger.*;

/**
 * This class represents the DN database, which has one record for each entry.
 */
public class OperationContainer extends DatabaseContainer
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();

  /**
   * The default name of the ordered index for the primary key.
   */
  private static final String PRIMARY_INDEX_NAME = "PRIMARY";

  /**
   * The name of extensible object objectclass.
   */
  private static final String OC_EXTENSIBLEOBJECT = "extensibleObject";

  /**
   * Lowest multivalued attribute id.
   */
  private static final int MIN_MID = 1;

  /**
   * Create a OperationContainer instance for the database in a
   * given entryContainer.
   *
   * @param name The name of the database.
   * @param entryContainer The entryContainer of the database.
   * @throws NdbApiException If an error occurs.
   */
  OperationContainer(String name, EntryContainer entryContainer)
      throws NdbApiException
  {
    super(name, entryContainer);
  }

  /**
   * Insert a new entry into the database.
   * @param txn Abstract transaction to be used for the database operation.
   * @param dn The entry DN.
   * @param id The entry ID.
   * @param entry The entry.
   * @return true if the entry was inserted, false if a entry already exists.
   * @throws NdbApiException If an error occurred while attempting to insert
   * the new entry.
   */
  public boolean insert(AbstractTransaction txn, DN dn, long id, Entry entry)
       throws NdbApiException
  {
    return writeNDBEntry(txn, dn, id, entry, false);
  }

  /**
   * Put an entry to the database.  If an entry already exists, the entry
   * will be replaced, otherwise a new entry will be inserted.
   * @param txn Abstract transaction to be used for the database operation.
   * @param dn The entry DN.
   * @param id The entry ID.
   * @param entry The new entry.
   * @param originalEntry The old entry.
   * @return true if the entry was written, false if it was not written.
   * @throws NdbApiException If an error occurred while attempting to write
   * the entry.
   */
  public boolean put(AbstractTransaction txn, DN dn, long id, Entry entry,
                     Entry originalEntry)
       throws NdbApiException
  {
    // Delete first.
    deleteNDBEntry(txn, originalEntry, id);

    return writeNDBEntry(txn, dn, id, entry, true);
  }

  /**
   * Write an entry to the database.
   * @param txn Abstract transaction to be used for the database operation.
   * @param dn The entry DN.
   * @param id The entry ID.
   * @param entry The entry.
   * @param overwrite Whether or not the entry should be overwritten.
   * @return true if the entry was written, false if it was not written.
   * @throws com.mysql.cluster.ndbj.NdbApiException If an error occurred
   * while attempting to write the entry.
   */
  private boolean writeNDBEntry(AbstractTransaction txn, DN dn,
    long id, Entry entry, boolean overwrite)
    throws NdbApiException
  {
    int nClasses = 0;
    NdbOperation op = null;
    NdbOperation tagOp = null;
    StringBuilder ocBuffer = new StringBuilder();
    StringBuilder xocBuffer = new StringBuilder();
    Map<ObjectClass, String> ocMap = entry.getObjectClasses();
    Map<AttributeType, List<Attribute>> userAttrMap =
      entry.getUserAttributes();
    Map<NdbBlob, byte[]> blobMap =
      new HashMap<NdbBlob, byte[]>(BackendImpl.blobAttributes.size());
    ArrayList<AttributeType> userAttributes =
      new ArrayList<AttributeType>();

    boolean extensibleObject = false;

    // Update ocs tables.
    NdbTransaction ndbDATxn = null;
    for (Map.Entry<ObjectClass, String> ocEntry : ocMap.entrySet()) {
      ObjectClass oc = ocEntry.getKey();
      String ocName = oc.getNameOrOID();

      Map<Integer, NdbOperation> mvOpMap =
        new HashMap<Integer, NdbOperation>();

      if (nClasses > 0) {
        ocBuffer.append(" ");
      }
      ocBuffer.append(ocName);
      nClasses++;

      if (oc.getObjectClassType() == ObjectClassType.ABSTRACT) {
        continue;
      }

      if (ocName.equalsIgnoreCase(OC_EXTENSIBLEOBJECT)) {
        extensibleObject = true;
      }

      if (ndbDATxn == null) {
        ndbDATxn = txn.getNdbDATransaction(ocName, id);
      }
      if (overwrite) {
        op = ndbDATxn.getWriteOperation(ocName);
      } else {
        op = ndbDATxn.getInsertOperation(ocName);
      }
      op.equalLong(BackendImpl.EID, id);
      op.equalInt(BackendImpl.MID, MIN_MID);
      mvOpMap.put(MIN_MID, op);

      for (AttributeType reqAttr : oc.getRequiredAttributes()) {
        if (userAttributes.contains(reqAttr)) {
          continue;
        }
        if (reqAttr.isOperational()) {
          userAttrMap.put(reqAttr, entry.getOperationalAttribute(reqAttr));
        }
        String attrName = reqAttr.getNameOrOID();
        if (entry.hasAttribute(reqAttr)) {
          boolean indexed = BackendImpl.indexes.contains(attrName);
          List<Attribute> attrList = userAttrMap.get(reqAttr);
          int mid = MIN_MID;
          for (Attribute attr : attrList) {
            if (attr.isVirtual() || attr.isEmpty()) {
              continue;
            }
            // Attribute options.
            Set<String> attrOptionsSet = attr.getOptions();
            if (!attrOptionsSet.isEmpty()) {
              if (overwrite) {
                tagOp =
                  ndbDATxn.getWriteOperation(BackendImpl.TAGS_TABLE);
              } else {
                tagOp =
                  ndbDATxn.getInsertOperation(BackendImpl.TAGS_TABLE);
              }
              tagOp.equalLong(BackendImpl.EID, id);
              tagOp.equalString(BackendImpl.TAG_ATTR, attrName);
              tagOp.equalInt(BackendImpl.MID, mid);
              StringBuilder buffer = new StringBuilder();
              for (String option : attrOptionsSet) {
                buffer.append(';');
                buffer.append(option);
              }
              tagOp.setString(BackendImpl.TAG_TAGS, buffer.toString());
            }
            for (AttributeValue attrVal : attr) {
              String attrStringVal = attrVal.toString();
              NdbOperation attrOp = mvOpMap.get(mid);
              if (attrOp == null) {
                if (overwrite) {
                  attrOp = ndbDATxn.getWriteOperation(ocName);
                } else {
                  attrOp = ndbDATxn.getInsertOperation(ocName);
                }
                attrOp.equalLong(BackendImpl.EID, id);
                attrOp.equalInt(BackendImpl.MID, mid);
                mvOpMap.put(mid, attrOp);
              }
              if (BackendImpl.blobAttributes.contains(attrName)) {
                NdbBlob blob = attrOp.getBlobHandle(attrName);
                blob.setValue(new byte[0]);
                byte[] blobBytes = attrVal.getValue().toByteArray();
                blobMap.put(blob, blobBytes);
              } else {
                attrOp.setString(attrName, attrStringVal);
              }
              // Update Indexes.
              if (indexed) {
                NdbOperation idxOp = null;
                if (overwrite) {
                  idxOp = ndbDATxn.getWriteOperation(
                    BackendImpl.IDX_TABLE_PREFIX + attrName);
                } else {
                  idxOp = ndbDATxn.getInsertOperation(
                    BackendImpl.IDX_TABLE_PREFIX + attrName);
                }
                idxOp.equalLong(BackendImpl.EID, id);
                idxOp.equalInt(BackendImpl.MID, mid);
                idxOp.setString(BackendImpl.IDX_VAL, attrStringVal);
              }
              mid++;
            }
          }
          userAttributes.add(reqAttr);
        }
      }

      for (AttributeType optAttr : oc.getOptionalAttributes()) {
        if (userAttributes.contains(optAttr)) {
          continue;
        }
        if (optAttr.isOperational()) {
          userAttrMap.put(optAttr, entry.getOperationalAttribute(optAttr));
        }
        String attrName = optAttr.getNameOrOID();
        if (entry.hasAttribute(optAttr)) {
          boolean indexed = BackendImpl.indexes.contains(attrName);
          List<Attribute> attrList = userAttrMap.get(optAttr);
          int mid = MIN_MID;
          for (Attribute attr : attrList) {
            if (attr.isVirtual() || attr.isEmpty()) {
              continue;
            }
            // Attribute options.
            Set<String> attrOptionsSet = attr.getOptions();
            if (!attrOptionsSet.isEmpty()) {
              if (overwrite) {
                tagOp =
                  ndbDATxn.getWriteOperation(BackendImpl.TAGS_TABLE);
              } else {
                tagOp =
                  ndbDATxn.getInsertOperation(BackendImpl.TAGS_TABLE);
              }
              tagOp.equalLong(BackendImpl.EID, id);
              tagOp.equalString(BackendImpl.TAG_ATTR, attrName);
              tagOp.equalInt(BackendImpl.MID, mid);
              StringBuilder buffer = new StringBuilder();
              for (String option : attrOptionsSet) {
                buffer.append(';');
                buffer.append(option);
              }
              tagOp.setString(BackendImpl.TAG_TAGS, buffer.toString());
            }
            for (AttributeValue attrVal : attr) {
              String attrStringVal = attrVal.toString();
              NdbOperation attrOp = mvOpMap.get(mid);
              if (attrOp == null) {
                if (overwrite) {
                  attrOp = ndbDATxn.getWriteOperation(ocName);
                } else {
                  attrOp = ndbDATxn.getInsertOperation(ocName);
                }
                attrOp.equalLong(BackendImpl.EID, id);
                attrOp.equalInt(BackendImpl.MID, mid);
                mvOpMap.put(mid, attrOp);
              }
              if (BackendImpl.blobAttributes.contains(attrName)) {
                NdbBlob blob = attrOp.getBlobHandle(attrName);
                blob.setValue(new byte[0]);
                byte[] blobBytes = attrVal.getValue().toByteArray();
                blobMap.put(blob, blobBytes);
              } else {
                attrOp.setString(attrName, attrStringVal);
              }
              // Update Indexes.
              if (indexed) {
                NdbOperation idxOp = null;
                if (overwrite) {
                  idxOp = ndbDATxn.getWriteOperation(
                    BackendImpl.IDX_TABLE_PREFIX + attrName);
                } else {
                  idxOp = ndbDATxn.getInsertOperation(
                    BackendImpl.IDX_TABLE_PREFIX + attrName);
                }
                idxOp.equalLong(BackendImpl.EID, id);
                idxOp.equalInt(BackendImpl.MID, mid);
                idxOp.setString(BackendImpl.IDX_VAL, attrStringVal);
              }
              mid++;
            }
          }
          userAttributes.add(optAttr);
        }
      }
    }

    // Extensible object.
    if (extensibleObject) {
      int xnClasses = 0;
      for (Map.Entry<AttributeType, List<Attribute>> attrEntry :
           userAttrMap.entrySet())
      {
        AttributeType attrType = attrEntry.getKey();
        if (!userAttributes.contains(attrType)) {
          String attrName = attrType.getNameOrOID();
          String ocName = BackendImpl.attr2Oc.get(attrName);
          Map<Integer, NdbOperation> mvOpMap =
            new HashMap<Integer, NdbOperation>();
          boolean indexed = BackendImpl.indexes.contains(attrName);

          if (ndbDATxn == null) {
            ndbDATxn = txn.getNdbDATransaction(ocName, id);
          }
          if (overwrite) {
            op = ndbDATxn.getWriteOperation(ocName);
          } else {
            op = ndbDATxn.getInsertOperation(ocName);
          }
          op.equalLong(BackendImpl.EID, id);
          op.equalInt(BackendImpl.MID, MIN_MID);
          mvOpMap.put(MIN_MID, op);

          List<Attribute> attrList = userAttrMap.get(attrType);
          int mid = MIN_MID;
          for (Attribute attr : attrList) {
            if (attr.isVirtual() || attr.isEmpty()) {
              continue;
            }
            // Attribute options.
            Set<String> attrOptionsSet = attr.getOptions();
            if (!attrOptionsSet.isEmpty()) {
              if (overwrite) {
                tagOp =
                  ndbDATxn.getWriteOperation(BackendImpl.TAGS_TABLE);
              } else {
                tagOp =
                  ndbDATxn.getInsertOperation(BackendImpl.TAGS_TABLE);
              }
              tagOp.equalLong(BackendImpl.EID, id);
              tagOp.equalString(BackendImpl.TAG_ATTR, attrName);
              tagOp.equalInt(BackendImpl.MID, mid);
              StringBuilder buffer = new StringBuilder();
              for (String option : attrOptionsSet) {
                buffer.append(';');
                buffer.append(option);
              }
              tagOp.setString(BackendImpl.TAG_TAGS, buffer.toString());
            }
            for (AttributeValue attrVal : attr) {
              String attrStringVal = attrVal.toString();
              NdbOperation attrOp = mvOpMap.get(mid);
              if (attrOp == null) {
                if (overwrite) {
                  attrOp = ndbDATxn.getWriteOperation(ocName);
                } else {
                  attrOp = ndbDATxn.getInsertOperation(ocName);
                }
                attrOp.equalLong(BackendImpl.EID, id);
                attrOp.equalInt(BackendImpl.MID, mid);
                mvOpMap.put(mid, attrOp);
              }
              if (BackendImpl.blobAttributes.contains(attrName)) {
                NdbBlob blob = attrOp.getBlobHandle(attrName);
                blob.setValue(new byte[0]);
                byte[] blobBytes = attrVal.getValue().toByteArray();
                blobMap.put(blob, blobBytes);
              } else {
                attrOp.setString(attrName, attrStringVal);
              }
              // Update Indexes.
              if (indexed) {
                NdbOperation idxOp = null;
                if (overwrite) {
                  idxOp = ndbDATxn.getWriteOperation(
                    BackendImpl.IDX_TABLE_PREFIX + attrName);
                } else {
                  idxOp = ndbDATxn.getInsertOperation(
                    BackendImpl.IDX_TABLE_PREFIX + attrName);
                }
                idxOp.equalLong(BackendImpl.EID, id);
                idxOp.equalInt(BackendImpl.MID, mid);
                idxOp.setString(BackendImpl.IDX_VAL, attrStringVal);
              }
              mid++;
            }
          }
          userAttributes.add(attrType);

          if (xnClasses > 0) {
            xocBuffer.append(" ");
          }
          xocBuffer.append(ocName);
          xnClasses++;
        }
      }
    }

    // Update operational attributes table.
    if (overwrite) {
      op = ndbDATxn.getWriteOperation(BackendImpl.OPATTRS_TABLE);
    } else {
      op = ndbDATxn.getInsertOperation(BackendImpl.OPATTRS_TABLE);
    }
    op.equalLong(BackendImpl.EID, id);
    for (List<Attribute> attrList :
         entry.getOperationalAttributes().values())
    {
      for (Attribute attr : attrList) {
        if (attr.isVirtual() || attr.isEmpty()) {
          continue;
        }
        if (userAttrMap.containsKey(attr.getAttributeType())) {
          continue;
        }
        String attrName = attr.getAttributeType().getNameOrOID();
        for (AttributeValue attrVal : attr) {
          op.setString(attrName, attrVal.toString());
        }
      }
    }

    // Move this txn into the active state and write blob data if any.
    if (!blobMap.isEmpty()) {
      ndbDATxn.execute(ExecType.NoCommit, AbortOption.AbortOnError, true);
      Set<Map.Entry<NdbBlob, byte[]>> blobEntrySet = blobMap.entrySet();
      for (Map.Entry blobEntry : blobEntrySet) {
        NdbBlob blob = (NdbBlob) blobEntry.getKey();
        byte[] blobBytes = (byte[]) blobEntry.getValue();
        blob.writeData(blobBytes);
      }
    }

    // Update dn2id table.
    NdbTransaction ndbTxn = txn.getNdbTransaction();
    if (overwrite) {
      op = ndbTxn.getWriteOperation(name);
    } else {
      op = ndbTxn.getInsertOperation(name);
    }

    int componentIndex = dn.getNumComponents() - 1;
    for (int i=0; i < BackendImpl.DN2ID_DN_NC; i++) {
      while (componentIndex >= 0) {
        op.equalString(BackendImpl.DN2ID_DN + Integer.toString(i),
          dn.getRDN(componentIndex).toString());
        componentIndex--;
        i++;
      }
      op.equalString(BackendImpl.DN2ID_DN +
        Integer.toString(i), "");
    }

    op.setLong(BackendImpl.EID, id);

    op.setString(BackendImpl.DN2ID_OC, ocBuffer.toString());

    op.setString(BackendImpl.DN2ID_XOC, xocBuffer.toString());

    return true;
  }

  /**
   * Delete an entry from the database.
   * @param txn Abstract transaction to be used for the database operation.
   * @param originalEntry The original entry.
   * @param id The entry ID.
   * @throws com.mysql.cluster.ndbj.NdbApiException If an error occurred
   * while attempting to write the entry.
   */
  private void deleteNDBEntry(AbstractTransaction txn,
    Entry originalEntry, long id) throws NdbApiException
  {
    NdbOperation op = null;
    NdbOperation tagOp = null;
    NdbTransaction ndbDATxn = null;
    boolean extensibleObject = false;

    // Delete attributes.
    Map<ObjectClass, String> originalOcMap =
      originalEntry.getObjectClasses();
    ArrayList<AttributeType> originalUserAttributes =
      new ArrayList<AttributeType>();
    Map<AttributeType, List<Attribute>> originalUserAttrMap =
      originalEntry.getUserAttributes();

    for (Map.Entry<ObjectClass, String> ocEntry : originalOcMap.entrySet()) {
      ObjectClass oc = ocEntry.getKey();
      String ocName = oc.getNameOrOID();
      Map<Integer, NdbOperation> mvOpMap =
        new HashMap<Integer, NdbOperation>();

      if (oc.getObjectClassType() == ObjectClassType.ABSTRACT) {
        continue;
      }

      if (ocName.equalsIgnoreCase(OC_EXTENSIBLEOBJECT)) {
        extensibleObject = true;
      }

      if (ndbDATxn == null) {
        ndbDATxn = txn.getNdbDATransaction(ocName, id);
      }
      op = ndbDATxn.getDeleteOperation(ocName);
      op.equalLong(BackendImpl.EID, id);
      op.equalInt(BackendImpl.MID, MIN_MID);
      mvOpMap.put(MIN_MID, op);

      for (AttributeType reqAttr : oc.getRequiredAttributes()) {
        String attrName = reqAttr.getNameOrOID();
        if (originalUserAttributes.contains(reqAttr)) {
          continue;
        }
        if (originalEntry.hasUserAttribute(reqAttr)) {
          boolean indexed = BackendImpl.indexes.contains(attrName);
          List<Attribute> attrList = originalUserAttrMap.get(reqAttr);
          int mid = MIN_MID;
          for (Attribute attr : attrList) {
            if (attr.isVirtual() || attr.isEmpty()) {
              continue;
            }
            // Attribute options.
            Set<String> attrOptionsSet = attr.getOptions();
            if (!attrOptionsSet.isEmpty()) {
              tagOp =
                ndbDATxn.getDeleteOperation(BackendImpl.TAGS_TABLE);
              tagOp.equalLong(BackendImpl.EID, id);
              tagOp.equalString(BackendImpl.TAG_ATTR, attrName);
              tagOp.equalInt(BackendImpl.MID, mid);
            }
            for (AttributeValue attrVal : attr) {
              NdbOperation attrOp = mvOpMap.get(mid);
              if (attrOp == null) {
                attrOp = ndbDATxn.getDeleteOperation(ocName);
                attrOp.equalLong(BackendImpl.EID, id);
                attrOp.equalInt(BackendImpl.MID, mid);
                mvOpMap.put(mid, attrOp);
              }
              // Update Indexes.
              if (indexed) {
                NdbOperation idxOp = ndbDATxn.getDeleteOperation(
                  BackendImpl.IDX_TABLE_PREFIX + attrName);
                idxOp.equalLong(BackendImpl.EID, id);
                idxOp.equalInt(BackendImpl.MID, mid);
              }
              mid++;
            }
          }
          originalUserAttributes.add(reqAttr);
        }
      }

      for (AttributeType optAttr : oc.getOptionalAttributes()) {
        String attrName = optAttr.getNameOrOID();
        if (originalUserAttributes.contains(optAttr)) {
          continue;
        }
        if (originalEntry.hasUserAttribute(optAttr)) {
          boolean indexed = BackendImpl.indexes.contains(attrName);
          List<Attribute> attrList = originalUserAttrMap.get(optAttr);
          int mid = MIN_MID;
          for (Attribute attr : attrList) {
            if (attr.isVirtual() || attr.isEmpty()) {
              continue;
            }
            // Attribute options.
            Set<String> attrOptionsSet = attr.getOptions();
            if (!attrOptionsSet.isEmpty()) {
              tagOp =
                ndbDATxn.getDeleteOperation(BackendImpl.TAGS_TABLE);
              tagOp.equalLong(BackendImpl.EID, id);
              tagOp.equalString(BackendImpl.TAG_ATTR, attrName);
              tagOp.equalInt(BackendImpl.MID, mid);
            }
            for (AttributeValue attrVal : attr) {
              NdbOperation attrOp = mvOpMap.get(mid);
              if (attrOp == null) {
                attrOp = ndbDATxn.getDeleteOperation(ocName);
                attrOp.equalLong(BackendImpl.EID, id);
                attrOp.equalInt(BackendImpl.MID, mid);
                mvOpMap.put(mid, attrOp);
              }
              // Update Indexes.
              if (indexed) {
                NdbOperation idxOp = ndbDATxn.getDeleteOperation(
                  BackendImpl.IDX_TABLE_PREFIX + attrName);
                idxOp.equalLong(BackendImpl.EID, id);
                idxOp.equalInt(BackendImpl.MID, mid);
              }
              mid++;
            }
          }
          originalUserAttributes.add(optAttr);
        }
      }
    }

    // Extensible object.
    if (extensibleObject) {
      for (Map.Entry<AttributeType, List<Attribute>> attrEntry :
           originalUserAttrMap.entrySet())
      {
        AttributeType attrType = attrEntry.getKey();
        if (!originalUserAttributes.contains(attrType)) {
          String attrName = attrType.getNameOrOID();
          String ocName = BackendImpl.attr2Oc.get(attrName);
          Map<Integer, NdbOperation> mvOpMap =
            new HashMap<Integer, NdbOperation>();
          boolean indexed = BackendImpl.indexes.contains(attrName);

          if (ndbDATxn == null) {
            ndbDATxn = txn.getNdbDATransaction(ocName, id);
          }
          op = ndbDATxn.getDeleteOperation(ocName);
          op.equalLong(BackendImpl.EID, id);
          op.equalInt(BackendImpl.MID, MIN_MID);
          mvOpMap.put(MIN_MID, op);

          List<Attribute> attrList = originalUserAttrMap.get(attrType);
          int mid = MIN_MID;
          for (Attribute attr : attrList) {
            if (attr.isVirtual() || attr.isEmpty()) {
              continue;
            }
            // Attribute options.
            Set<String> attrOptionsSet = attr.getOptions();
            if (!attrOptionsSet.isEmpty()) {
              tagOp =
                ndbDATxn.getDeleteOperation(BackendImpl.TAGS_TABLE);
              tagOp.equalLong(BackendImpl.EID, id);
              tagOp.equalString(BackendImpl.TAG_ATTR, attrName);
              tagOp.equalInt(BackendImpl.MID, mid);
            }
            for (AttributeValue attrVal : attr) {
              NdbOperation attrOp = mvOpMap.get(mid);
              if (attrOp == null) {
                attrOp = ndbDATxn.getDeleteOperation(ocName);
                attrOp.equalLong(BackendImpl.EID, id);
                attrOp.equalInt(BackendImpl.MID, mid);
                mvOpMap.put(mid, attrOp);
              }
              // Update Indexes.
              if (indexed) {
                NdbOperation idxOp = ndbDATxn.getDeleteOperation(
                  BackendImpl.IDX_TABLE_PREFIX + attrName);
                idxOp.equalLong(BackendImpl.EID, id);
                idxOp.equalInt(BackendImpl.MID, mid);
              }
              mid++;
            }
          }
          originalUserAttributes.add(attrType);
        }
      }
    }
  }

  /**
   * Remove an entry from the database.
   * @param txn Abstract transaction to be used for the database operation.
   * @param entry The entry.
   * @return true if the entry was removed, false if it was not removed.
   * @throws NdbApiException If an error occurred while attempting to remove
   * the entry.
   */
  public boolean remove(AbstractTransaction txn, Entry entry)
       throws NdbApiException
  {
    DN dn = entry.getDN();

    NdbResultSet rs = null;

    NdbTransaction ndbTxn = txn.getNdbTransaction();

    NdbOperation op = ndbTxn.getSelectOperation(name,
      NdbOperation.LockMode.LM_CommittedRead);

    boolean extensibleObject = false;

    int componentIndex = dn.getNumComponents() - 1;
    for (int i=0; i < BackendImpl.DN2ID_DN_NC; i++) {
      while (componentIndex >= 0) {
        op.equalString(BackendImpl.DN2ID_DN + Integer.toString(i),
          dn.getRDN(componentIndex).toNormalizedString());
        componentIndex--;
        i++;
      }
      op.equalString(BackendImpl.DN2ID_DN +
        Integer.toString(i), "");
    }
    op.getValue(BackendImpl.EID);
    op.getValue(BackendImpl.DN2ID_OC);
    op.getValue(BackendImpl.DN2ID_XOC);

    rs = op.resultData();
    ndbTxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    long eid = 0;
    NdbTransaction ndbDATxn = null;
    String[] ocsStringArray = null;
    String[] xocsStringArray = null;
    List<NdbResultSet> ocRsList = new ArrayList<NdbResultSet>();
    NdbIndexScanOperation indexScanOp = null;

    if (rs.next()) {
      eid = rs.getLong(BackendImpl.EID);
      String ocsString = rs.getString(BackendImpl.DN2ID_OC);
      ocsStringArray = ocsString.split(" ");

      String xocsString = rs.getString(BackendImpl.DN2ID_XOC);
      xocsStringArray = xocsString.split(" ");
      if (xocsString.length() > 0) {
        extensibleObject = true;
      }

      for (String ocName : ocsStringArray) {
        ObjectClass oc =
          DirectoryServer.getObjectClass(ocName, true);
        if (oc.getObjectClassType() == ObjectClassType.ABSTRACT) {
          continue;
        }
        if (ndbDATxn == null) {
          ndbDATxn = txn.getNdbDATransaction(ocName, eid);
        }
        indexScanOp =
          ndbDATxn.getSelectIndexScanOperation(PRIMARY_INDEX_NAME, ocName);
        indexScanOp.setBoundLong(BackendImpl.EID,
            NdbIndexScanOperation.BoundType.BoundEQ, eid);
        indexScanOp.getValue(BackendImpl.MID);
        ocRsList.add(indexScanOp.resultData());
      }

      // Extensible object.
      if (extensibleObject) {
        for (String xocName : xocsStringArray) {
          ObjectClass xoc =
            DirectoryServer.getObjectClass(xocName, true);
          if (xoc.getObjectClassType() == ObjectClassType.ABSTRACT) {
            continue;
          }
          if (ndbDATxn == null) {
            ndbDATxn = txn.getNdbDATransaction(xocName, eid);
          }
          indexScanOp =
            ndbDATxn.getSelectIndexScanOperation(PRIMARY_INDEX_NAME, xocName);
          indexScanOp.setBoundLong(BackendImpl.EID,
            NdbIndexScanOperation.BoundType.BoundEQ, eid);
          indexScanOp.getValue(BackendImpl.MID);
          ocRsList.add(indexScanOp.resultData());
        }
      }
    }

    // Attribute options.
    if (ndbDATxn == null) {
      ndbDATxn = txn.getNdbDATransaction(BackendImpl.TAGS_TABLE, eid);
    }
    indexScanOp = ndbDATxn.getSelectIndexScanOperation(PRIMARY_INDEX_NAME,
      BackendImpl.TAGS_TABLE);
    indexScanOp.setBoundLong(BackendImpl.EID,
      NdbIndexScanOperation.BoundType.BoundEQ, eid);
    indexScanOp.getValue(BackendImpl.TAG_ATTR);
    indexScanOp.getValue(BackendImpl.MID);
    NdbResultSet tagRs = indexScanOp.resultData();

    ndbDATxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    Iterator<NdbResultSet> rsIterator = ocRsList.iterator();
    for (String ocName : ocsStringArray) {
      ObjectClass oc =
        DirectoryServer.getObjectClass(ocName, true);
      if (oc.getObjectClassType() == ObjectClassType.ABSTRACT) {
        continue;
      }
      NdbResultSet ocRs = rsIterator.next();
      while (ocRs.next()) {
        int mid = ocRs.getInt(BackendImpl.MID);
        op = ndbDATxn.getDeleteOperation(ocName);
        op.equalLong(BackendImpl.EID, eid);
        op.equalInt(BackendImpl.MID, mid);
      }
    }

    // Extensible object.
    if (extensibleObject) {
      for (String xocName : xocsStringArray) {
        ObjectClass xoc =
          DirectoryServer.getObjectClass(xocName, true);
        if (xoc.getObjectClassType() == ObjectClassType.ABSTRACT) {
          continue;
        }
        NdbResultSet ocRs = rsIterator.next();
        while (ocRs.next()) {
          int mid = ocRs.getInt(BackendImpl.MID);
          op = ndbDATxn.getDeleteOperation(xocName);
          op.equalLong(BackendImpl.EID, eid);
          op.equalInt(BackendImpl.MID, mid);
        }
      }
    }

    // Operational attributes.
    op = ndbDATxn.getDeleteOperation(BackendImpl.OPATTRS_TABLE);
    op.equalLong(BackendImpl.EID, eid);

    // Attribute options.
    while (tagRs.next()) {
      String attrName = tagRs.getString(BackendImpl.TAG_ATTR);
      int mid = tagRs.getInt(BackendImpl.MID);
      op = ndbDATxn.getDeleteOperation(BackendImpl.TAGS_TABLE);
      op.equalLong(BackendImpl.EID, eid);
      op.equalString(BackendImpl.TAG_ATTR, attrName);
      op.equalInt(BackendImpl.MID, mid);
    }

    // Indexes.
    for (String attrName : BackendImpl.indexes) {
      AttributeType attributeType =
              DirectoryServer.getAttributeType(
              attrName.toLowerCase(), true);
      if (entry.hasAttribute(attributeType)) {
        List<Attribute> attrList =
          entry.getAttribute(attributeType);
        int mid = MIN_MID;
        for (Attribute attr : attrList) {
          for (AttributeValue attrVal : attr) {
            NdbOperation idxOp = ndbDATxn.getDeleteOperation(
              BackendImpl.IDX_TABLE_PREFIX + attrName);
            idxOp.equalLong(BackendImpl.EID, eid);
            idxOp.equalInt(BackendImpl.MID, mid);
            mid++;
          }
        }
      }
    }

    // dn2id.
    op = ndbTxn.getDeleteOperation(name);
    componentIndex = dn.getNumComponents() - 1;
    for (int i=0; i < BackendImpl.DN2ID_DN_NC; i++) {
      while (componentIndex >= 0) {
        op.equalString(BackendImpl.DN2ID_DN + Integer.toString(i),
          dn.getRDN(componentIndex).toNormalizedString());
        componentIndex--;
        i++;
      }
      op.equalString(BackendImpl.DN2ID_DN +
        Integer.toString(i), "");
    }

    return true;
  }

  /**
   * Fetch the entry for a given ID.
   * @param txn Abstract transaction to be used for the database read.
   * @param eid The ID for which the entry is desired.
   * @param lockMode NDB locking mode for this operation.
   * @return The entry, or null if the given ID is not in the database.
   * @throws NdbApiException If an error occurs in the database.
   * @throws DirectoryException If a problem occurs while trying to
   *         retrieve the entry.
   */
  public Entry get(AbstractTransaction txn, long eid,
    NdbOperation.LockMode lockMode)
       throws NdbApiException, DirectoryException
  {
    NdbIndexScanOperation indexScanOp = null;
    NdbResultSet rs = null;
    DN dn = null;

    NdbTransaction ndbTxn = txn.getNdbTransaction();

    indexScanOp = ndbTxn.getSelectIndexScanOperation(
      BackendImpl.EID, name, lockMode);
    indexScanOp.setBoundLong(BackendImpl.EID,
            NdbIndexScanOperation.BoundType.BoundEQ, eid);
    for (int i = 0; i < BackendImpl.DN2ID_DN_NC; i++) {
      indexScanOp.getValue(BackendImpl.DN2ID_DN +
        Integer.toString(i));
    }
    indexScanOp.getValue(BackendImpl.DN2ID_OC);
    indexScanOp.getValue(BackendImpl.DN2ID_XOC);

    rs = indexScanOp.resultData();
    ndbTxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    if (rs.next()) {
      StringBuilder dnBuffer = new StringBuilder();
      int dnColumnIndex = BackendImpl.DN2ID_DN_NC - 1;
      while (dnColumnIndex >= 0) {
        String rdnString = rs.getString(BackendImpl.DN2ID_DN +
          Integer.toString(dnColumnIndex));
        if (rdnString.length() > 0) {
          dnBuffer.append(rdnString);
          if (dnColumnIndex > 0) {
            dnBuffer.append(",");
          }
        }
        dnColumnIndex--;
      }
      String dnString = dnBuffer.toString();
      if (dnString.length() == 0) {
        return null;
      }
      dn = DN.decode(dnString);
      return getNDBEntry(txn, rs, dn, eid);
    } else {
      return null;
    }
  }


  /**
   * Fetch the entry for a given DN.
   * @param txn Abstract transaction to be used for the database read.
   * @param dn The DN for which the entry is desired.
   * @param lockMode NDB locking mode for this operation.
   * @return The entry, or null if the given DN is not in the database.
   * @throws NdbApiException If an error occurs in the database.
   */
  public Entry get(AbstractTransaction txn, DN dn,
    NdbOperation.LockMode lockMode) throws NdbApiException
  {
    NdbOperation op = null;
    NdbResultSet rs = null;

    NdbTransaction ndbTxn = txn.getNdbTransaction();

    op = ndbTxn.getSelectOperation(name, lockMode);

    int componentIndex = dn.getNumComponents() - 1;
    for (int i=0; i < BackendImpl.DN2ID_DN_NC; i++) {
      while (componentIndex >= 0) {
        op.equalString(BackendImpl.DN2ID_DN + Integer.toString(i),
          dn.getRDN(componentIndex).toNormalizedString());
        componentIndex--;
        i++;
      }
      op.equalString(BackendImpl.DN2ID_DN +
        Integer.toString(i), "");
    }
    op.getValue(BackendImpl.EID);
    op.getValue(BackendImpl.DN2ID_OC);
    op.getValue(BackendImpl.DN2ID_XOC);

    rs = op.resultData();
    ndbTxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    if (rs.next()) {
      long eid = rs.getLong(BackendImpl.EID);
      if (eid == 0) {
        return null;
      }
      Entry entry = getNDBEntry(txn, rs, dn, eid);
      if (entry != null) {
        entry.setAttachment(eid);
      }
      return entry;
    } else {
      return null;
    }
  }

  /**
   * Get the entry from the database.
   * @param txn Abstract transaction to be used for the database read.
   * @param rs NDB results set from the initial get entry operation.
   * @param dn The entry DN.
   * @param id The entry ID.
   * @return The entry.
   * @throws NdbApiException If an error occurs in the database.
   */
  private Entry getNDBEntry(
    AbstractTransaction txn,
    NdbResultSet rs,
    DN dn,
    long eid) throws NdbApiException
  {
    NdbOperation op = null;
    NdbIndexScanOperation indexScanOp = null;
    boolean extensibleObject = false;

    String ocsString = rs.getString(BackendImpl.DN2ID_OC);
    String[] ocsStringArray = ocsString.split(" ");

    String xocsString = rs.getString(BackendImpl.DN2ID_XOC);
    String[] xocsStringArray = xocsString.split(" ");
    if (xocsString.length() > 0) {
      extensibleObject = true;
    }
    LinkedHashMap<ObjectClass, String> xObjectClasses =
      new LinkedHashMap<ObjectClass, String>();

    List<NdbResultSet> ocRsList = new ArrayList<NdbResultSet>();
    Map<String, Map<String, NdbBlob>> blobMap =
      new HashMap<String, Map<String, NdbBlob>>();
    LinkedHashMap<ObjectClass, String> objectClasses =
      new LinkedHashMap<ObjectClass, String>(ocsStringArray.length);

    NdbTransaction ndbDATxn = null;
    NdbIndexScanOperation tagScanOp = null;

    for (String ocName : ocsStringArray) {
      ObjectClass oc =
        DirectoryServer.getObjectClass(ocName, true);
      objectClasses.put(oc, ocName);
      if (oc.getObjectClassType() == ObjectClassType.ABSTRACT) {
        continue;
      }

      if (ndbDATxn == null) {
        ndbDATxn = txn.getNdbDATransaction(ocName, eid);
      }

      indexScanOp =
        ndbDATxn.getSelectIndexScanOperation(
        PRIMARY_INDEX_NAME, ocName,
        NdbOperation.LockMode.LM_CommittedRead);
      indexScanOp.setBoundLong(BackendImpl.EID,
        NdbIndexScanOperation.BoundType.BoundEQ, eid);
      indexScanOp.getValue(BackendImpl.MID);

      for (AttributeType reqAttr : oc.getRequiredAttributes()) {
        String attrName = reqAttr.getNameOrOID();
        if (BackendImpl.blobAttributes.contains(attrName)) {
          NdbBlob blob = indexScanOp.getBlobHandle(attrName);
          Map<String, NdbBlob> attr2Blob = blobMap.get(ocName);
          if (attr2Blob == null) {
            attr2Blob = new HashMap<String, NdbBlob>();
          }
          attr2Blob.put(attrName, blob);
          blobMap.put(ocName, attr2Blob);
        } else {
          indexScanOp.getValue(attrName);
        }
      }
      for (AttributeType optAttr : oc.getOptionalAttributes()) {
        String attrName = optAttr.getNameOrOID();
        if (BackendImpl.blobAttributes.contains(attrName)) {
          NdbBlob blob = indexScanOp.getBlobHandle(attrName);
          Map<String, NdbBlob> attr2Blob = blobMap.get(ocName);
          if (attr2Blob == null) {
            attr2Blob = new HashMap<String, NdbBlob>();
          }
          attr2Blob.put(attrName, blob);
          blobMap.put(ocName, attr2Blob);
        } else {
          indexScanOp.getValue(attrName);
        }
      }
      ocRsList.add(indexScanOp.resultData());
    }

    // Extensible object.
    if (extensibleObject) {
      for (String xocName : xocsStringArray) {
        ObjectClass xoc =
          DirectoryServer.getObjectClass(xocName, true);
        objectClasses.put(xoc, xocName);
        xObjectClasses.put(xoc, xocName);
        if (xoc.getObjectClassType() == ObjectClassType.ABSTRACT) {
          continue;
        }

        if (ndbDATxn == null) {
          ndbDATxn = txn.getNdbDATransaction(xocName, eid);
        }

        indexScanOp =
          ndbDATxn.getSelectIndexScanOperation(
          PRIMARY_INDEX_NAME, xocName,
          NdbOperation.LockMode.LM_CommittedRead);
        indexScanOp.setBoundLong(BackendImpl.EID,
          NdbIndexScanOperation.BoundType.BoundEQ, eid);
        indexScanOp.getValue(BackendImpl.MID);

        for (AttributeType reqAttr : xoc.getRequiredAttributes()) {
          String attrName = reqAttr.getNameOrOID();
          if (BackendImpl.blobAttributes.contains(attrName)) {
            NdbBlob blob = indexScanOp.getBlobHandle(attrName);
            Map<String, NdbBlob> attr2Blob = blobMap.get(xocName);
            if (attr2Blob == null) {
              attr2Blob = new HashMap<String, NdbBlob>();
            }
            attr2Blob.put(attrName, blob);
            blobMap.put(xocName, attr2Blob);
          } else {
            indexScanOp.getValue(attrName);
          }
        }
        for (AttributeType optAttr : xoc.getOptionalAttributes()) {
          String attrName = optAttr.getNameOrOID();
          if (BackendImpl.blobAttributes.contains(attrName)) {
            NdbBlob blob = indexScanOp.getBlobHandle(attrName);
            Map<String, NdbBlob> attr2Blob = blobMap.get(xocName);
            if (attr2Blob == null) {
              attr2Blob = new HashMap<String, NdbBlob>();
            }
            attr2Blob.put(attrName, blob);
            blobMap.put(xocName, attr2Blob);
          } else {
            indexScanOp.getValue(attrName);
          }
        }
        ocRsList.add(indexScanOp.resultData());
      }
    }

    // Operational attributes.
    op = ndbDATxn.getSelectOperation(BackendImpl.OPATTRS_TABLE,
      NdbOperation.LockMode.LM_CommittedRead);
    op.equalLong(BackendImpl.EID, eid);

    for (String attrName : BackendImpl.operationalAttributes) {
      op.getValue(attrName);
    }
    ocRsList.add(op.resultData());

    // Attribute options.
    tagScanOp = ndbDATxn.getSelectIndexScanOperation(
      PRIMARY_INDEX_NAME,
      BackendImpl.TAGS_TABLE,
      NdbOperation.LockMode.LM_CommittedRead);
    tagScanOp.setBoundLong(BackendImpl.EID,
      NdbIndexScanOperation.BoundType.BoundEQ, eid);
    tagScanOp.getValue(BackendImpl.TAG_ATTR);
    tagScanOp.getValue(BackendImpl.MID);
    tagScanOp.getValue(BackendImpl.TAG_TAGS);
    NdbResultSet tagRs = tagScanOp.resultData();

    ndbDATxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    return decodeNDBEntry(dn, ocRsList, tagRs, objectClasses,
      xObjectClasses, blobMap, extensibleObject);
  }

  /**
   * Decode the entry from NDB results.
   * @param dn The entry DN.
   * @param ocRsList ObjectClass result sets list.
   * @param tagRs Attribute tags result set.
   * @param objectClasses ObjectClasses map.
   * @param xObjectClasses Extensible ObjectClasses map.
   * @param blobMap Blob attributes map.
   * @param extensibleObject true if the entry is Extensible Object,
   * false otherwise.
   * @return The entry.
   * @throws com.mysql.cluster.ndbj.NdbApiException
   */
  private Entry decodeNDBEntry(
    DN dn,
    List<NdbResultSet> ocRsList,
    NdbResultSet tagRs,
    Map<ObjectClass, String> objectClasses,
    Map<ObjectClass, String> xObjectClasses,
    Map<String, Map<String, NdbBlob>> blobMap,
    boolean extensibleObject) throws NdbApiException
  {
    LinkedHashMap<AttributeType, List<Attribute>> userAttributes =
      new LinkedHashMap<AttributeType, List<Attribute>>();
    LinkedHashMap<AttributeType, List<Attribute>> opAttributes =
      new LinkedHashMap<AttributeType, List<Attribute>>();

    // Attribute options.
    Map<String, Map<Integer, LinkedHashSet<String>>> attr2tagMap =
      new HashMap<String, Map<Integer, LinkedHashSet<String>>>();
    while (tagRs.next()) {
      String attrName = tagRs.getString(BackendImpl.TAG_ATTR);
      int mid = tagRs.getInt(BackendImpl.MID);
      String attrOptions = tagRs.getString(BackendImpl.TAG_TAGS);
      if (!tagRs.wasNull()) {
        int currentIndex = attrOptions.indexOf(';');
        int nextIndex = attrOptions.indexOf(';', currentIndex + 1);
        String option = null;
        Map<Integer, LinkedHashSet<String>> mid2tagMap =
          attr2tagMap.get(attrName);
        if (mid2tagMap == null) {
          mid2tagMap = new HashMap<Integer, LinkedHashSet<String>>();
        }
        LinkedHashSet<String> options = new LinkedHashSet<String>();
        while (nextIndex > 0) {
          option =
            attrOptions.substring(currentIndex + 1, nextIndex);
          if (option.length() > 0) {
            options.add(option);
          }
          currentIndex = nextIndex;
          nextIndex = attrOptions.indexOf(';', currentIndex + 1);
        }
        option = attrOptions.substring(currentIndex + 1);
        if (option.length() > 0) {
          options.add(option);
        }
        mid2tagMap.put(mid, options);
        attr2tagMap.put(attrName, mid2tagMap);
      }
    }

    // Object classes and user atributes.
    Iterator<NdbResultSet> ocRsIterator = ocRsList.iterator();
    NdbResultSet ocRs = ocRsIterator.next();
    AttributeBuilder attrBuilder = new AttributeBuilder();
    for (ObjectClass oc : objectClasses.keySet()) {
      if (oc.getObjectClassType() == ObjectClassType.ABSTRACT) {
        continue;
      }
      while (ocRs.next()) {
        int mid = ocRs.getInt(BackendImpl.MID);
        for (AttributeType reqAttr : oc.getRequiredAttributes()) {
          String attrName = reqAttr.getNameOrOID();
          byte[] attrValBytes = null;
          NdbBlob blob = null;
          if (BackendImpl.blobAttributes.contains(attrName)) {
            Map<String, NdbBlob> attr2Blob =
              blobMap.get(objectClasses.get(oc));
            blob = attr2Blob.get(attrName);
          } else {
            attrValBytes = ocRs.getStringBytes(attrName);
            if (ocRs.wasNull()) {
              continue;
            }
          }
          AttributeType attributeType =
            DirectoryServer.getAttributeType(
            BackendImpl.attrName2LC.get(attrName), true);
          List<Attribute> attrList = userAttributes.get(attributeType);
          if (attrList == null) {
            attrList = new ArrayList<Attribute>();
          }
          Attribute attr = null;
          LinkedHashSet<String> options = null;
          Map<Integer, LinkedHashSet<String>> mid2tagMap =
            attr2tagMap.get(attrName);
          if (mid2tagMap != null) {
            options = mid2tagMap.get(mid);
          }
          if ((options == null) && !attrList.isEmpty()) {
            attr = attrList.get(attrList.size() - 1);
          }
          if (attr == null) {
            attrBuilder.setAttributeType(attributeType, attrName);
          } else {
            attrBuilder = new AttributeBuilder(attr);
          }
          if (blob != null) {
            if (blob.getNull()) {
              continue;
            }
            int len = blob.getLength().intValue();
            byte[] buf = new byte[len];
            blob.readData(buf, len);
            attrBuilder.add(AttributeValues.create(attributeType,
              ByteString.wrap(buf)));
          } else {
            attrBuilder.add(AttributeValues.create(attributeType,
              ByteString.wrap(attrValBytes)));
          }

          // Create or update an attribute.
          if (options != null) {
            attrBuilder.setOptions(options);
          }
          attr = attrBuilder.toAttribute();
          if (attrList.isEmpty()) {
            attrList.add(attr);
          } else {
            attrList.set(attrList.size() - 1, attr);
          }

          userAttributes.put(attributeType, attrList);
        }
        for (AttributeType optAttr : oc.getOptionalAttributes()) {
          String attrName = optAttr.getNameOrOID();
          byte[] attrValBytes = null;
          NdbBlob blob = null;
          if (BackendImpl.blobAttributes.contains(attrName)) {
            Map<String, NdbBlob> attr2Blob =
              blobMap.get(objectClasses.get(oc));
            blob = attr2Blob.get(attrName);
          } else {
            attrValBytes = ocRs.getStringBytes(attrName);
            if (ocRs.wasNull()) {
              continue;
            }
          }
          AttributeType attributeType =
            DirectoryServer.getAttributeType(
            BackendImpl.attrName2LC.get(attrName), true);
          List<Attribute> attrList = userAttributes.get(attributeType);
          if (attrList == null) {
            attrList = new ArrayList<Attribute>();
          }
          Attribute attr = null;
          LinkedHashSet<String> options = null;
          Map<Integer, LinkedHashSet<String>> mid2tagMap =
            attr2tagMap.get(attrName);
          if (mid2tagMap != null) {
            options = mid2tagMap.get(mid);
          }
          if ((options == null) && !attrList.isEmpty()) {
            attr = attrList.get(attrList.size() - 1);
          }
          if (attr == null) {
            attrBuilder.setAttributeType(attributeType, attrName);
          } else {
            attrBuilder = new AttributeBuilder(attr);
          }
          if (blob != null) {
            if (blob.getNull()) {
              continue;
            }
            int len = blob.getLength().intValue();
            byte[] buf = new byte[len];
            blob.readData(buf, len);
            attrBuilder.add(AttributeValues.create(attributeType,
              ByteString.wrap(buf)));
          } else {
            attrBuilder.add(AttributeValues.create(attributeType,
              ByteString.wrap(attrValBytes)));
          }

          // Create or update an attribute.
          if (options != null) {
            attrBuilder.setOptions(options);
          }
          attr = attrBuilder.toAttribute();
          if (attrList.isEmpty()) {
            attrList.add(attr);
          } else {
            attrList.set(attrList.size() - 1, attr);
          }

          userAttributes.put(attributeType, attrList);
        }
      }
      if (ocRsIterator.hasNext()) {
        ocRs = ocRsIterator.next();
      }
    }

    // Operational attributes.
    if (ocRs.next()) {
      for (String attrName : BackendImpl.operationalAttributes) {
        byte[] attrValBytes = ocRs.getStringBytes(attrName);
        if (ocRs.wasNull()) {
          continue;
        }
        AttributeType attributeType =
          DirectoryServer.getAttributeType(
          BackendImpl.attrName2LC.get(attrName), true);
        attrBuilder.setAttributeType(attributeType, attrName);
        attrBuilder.add(AttributeValues.create(attributeType,
          ByteString.wrap(attrValBytes)));
        Attribute attr = attrBuilder.toAttribute();
        List<Attribute> attrList = opAttributes.get(attributeType);
        if (attrList == null) {
          attrList = new ArrayList<Attribute>();
          attrList.add(attr);
          opAttributes.put(attributeType, attrList);
        } else {
          attrList.add(attr);
        }
      }
    }

    // Extensible object.
    if (extensibleObject) {
      for (ObjectClass oc : xObjectClasses.keySet()) {
        objectClasses.remove(oc);
      }
    }

    Entry entry = new Entry(dn, objectClasses, userAttributes, opAttributes);
    if (entry != null) {
      entry.processVirtualAttributes();
    }
    return entry;
  }

  /**
   * Fetch the entry ID for a given DN.
   * @param txn Abstract transaction to be used for the database read.
   * @param dn The DN for which the entry ID is desired.
   * @param lockMode The lock mode for this operation.
   * @return The entry ID, or zero if the given DN is not in the database.
   * @throws NdbApiException If an error occurs in the database.
   */
  public long getID(AbstractTransaction txn, DN dn,
    NdbOperation.LockMode lockMode)
       throws NdbApiException
  {
    NdbOperation op = null;
    NdbResultSet rs = null;
    long eid = 0;

    NdbTransaction ndbTxn = txn.getNdbTransaction();

    op = ndbTxn.getSelectOperation(name, lockMode);

    int componentIndex = dn.getNumComponents() - 1;
    for (int i=0; i < BackendImpl.DN2ID_DN_NC; i++) {
      while (componentIndex >= 0) {
        op.equalString(BackendImpl.DN2ID_DN + Integer.toString(i),
          dn.getRDN(componentIndex).toNormalizedString());
        componentIndex--;
        i++;
      }
      op.equalString(BackendImpl.DN2ID_DN +
        Integer.toString(i), "");
    }

    op.getValue(BackendImpl.EID);

    rs = op.resultData();
    ndbTxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    if (rs.next()) {
      eid = rs.getLong(BackendImpl.EID);
    }

    return eid;
  }

  /**
   * Get referrals for a given entry ID.
   * @param txn Abstract transaction to be used for the operation.
   * @param id The ID for which the referral is desired.
   * @return The referral set, or empty set if the entry has no referrals.
   * @throws NdbApiException If an error occurs in the database.
   */
  public Set<String> getReferrals(AbstractTransaction txn, long id)
       throws NdbApiException
  {
    NdbIndexScanOperation op = null;
    NdbResultSet rs = null;
    Set<String> referrals = new HashSet<String>();

    NdbTransaction ndbDATxn =
      txn.getNdbDATransaction(BackendImpl.REFERRALS_TABLE, id);

    op = ndbDATxn.getSelectIndexScanOperation(
      PRIMARY_INDEX_NAME, BackendImpl.REFERRALS_TABLE,
      NdbOperation.LockMode.LM_CommittedRead);
    op.setBoundLong(BackendImpl.EID,
            NdbIndexScanOperation.BoundType.BoundEQ, id);

    op.getValue(ATTR_REFERRAL_URL);
    rs = op.resultData();

    ndbDATxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    while (rs.next()) {
      String referral = rs.getString(ATTR_REFERRAL_URL);
      if (rs.wasNull() || (referral.length() == 0)) {
        break;
      }
      referrals.add(referral);
    }

    return referrals;
  }

  /**
   * Get the count of rows in the database.
   * @return The number of rows in the database.
   * @throws NdbApiException If an error occurs in the database.
   */
  public long getRecordCount() throws NdbApiException
  {
    Ndb ndb = entryContainer.getRootContainer().getNDB();

    try {
      return ndb.selectCount(name);
    } finally {
      if (ndb != null) {
        entryContainer.getRootContainer().releaseNDB(ndb);
      }
    }
  }

  /**
   * Determine if the entry has subordinate entries.
   * @param txn Abstract transaction to be used for the operation.
   * @param dn The entry DN.
   * @return true if the entry has subordinates, false otherwise.
   * @throws com.mysql.cluster.ndbj.NdbApiException If an error
   * occurs in the database.
   */
  public boolean hasSubordinates(AbstractTransaction txn, DN dn)
    throws NdbApiException
  {
    NdbIndexScanOperation op;
    NdbResultSet rs;

    NdbTransaction ndbTxn = txn.getNdbTransaction();

    // Set batch size and parallel scans to
    // their minimum values to reduce churn.
    op = ndbTxn.getSelectIndexScanOperation(
      PRIMARY_INDEX_NAME, name,
      NdbOperation.LockMode.LM_CommittedRead,
      NdbScanOperation.ScanFlag.NO_FLAG, 1, 1);

    int numComponents = dn.getNumComponents();
    int componentIndex = numComponents - 1;
    for (int i=0; i < numComponents; i++) {
      op.setBoundString(BackendImpl.DN2ID_DN +
        Integer.toString(i),
        NdbIndexScanOperation.BoundType.BoundEQ,
        dn.getRDN(componentIndex).toNormalizedString());
      componentIndex--;
    }

    if (dn.getNumComponents() < BackendImpl.DN2ID_DN_NC) {
      String nextRDNColumn =
        BackendImpl.DN2ID_DN + Integer.toString(numComponents);
      op.setBoundString(nextRDNColumn,
        NdbIndexScanOperation.BoundType.BoundLT, "");
    }

    // FIXME: Need NDB/J API like native interpretExitLastRow
    // to do this more efficiently without additional hops.
    op.getValue(BackendImpl.EID);

    rs = op.resultData();
    ndbTxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    if (rs.next()) {
      return true;
    }

    return false;
  }

  /**
   * Count the number of subordinates for the requested entry DN.
   * @param  txn Abstract transaction to be used for the operation.
   * @param  dn The entry DN.
   * @param  subtree <code>true</code> will include all the entries
   *         under the given entries. <code>false</code> will only
   *         return the number of entries immediately under the
   *         given entry.
   * @return The number of subordinate entries for the requested
   *         entry or -1 if the entry does not exist.
   * @throws com.mysql.cluster.ndbj.NdbApiException If an error
   * occurs in the database.
   */
  public long numSubordinates(AbstractTransaction txn, DN dn,
    boolean subtree) throws NdbApiException
  {
    long numSubordinates = 0;
    NdbIndexScanOperation op;
    NdbResultSet rs;

    NdbTransaction ndbTxn = txn.getNdbTransaction();

    op = ndbTxn.getSelectIndexScanOperation(
      PRIMARY_INDEX_NAME, name,
      NdbOperation.LockMode.LM_CommittedRead);

    int numComponents = dn.getNumComponents();
    int componentIndex = numComponents - 1;
    for (int i=0; i < numComponents; i++) {
      op.setBoundString(BackendImpl.DN2ID_DN +
        Integer.toString(i),
        NdbIndexScanOperation.BoundType.BoundEQ,
        dn.getRDN(componentIndex).toNormalizedString());
      componentIndex--;
    }

    // FIXME: Need multi range bound here to optimize in
    // NOT subtree case which would prevent subtree walk
    // when it is not needed. need NDB/J API to do that.
    // native API: NdbIndexScanOperation::end_of_bound()
    if (dn.getNumComponents() < BackendImpl.DN2ID_DN_NC) {
      String nextRDNColumn =
        BackendImpl.DN2ID_DN + Integer.toString(numComponents);
      op.setBoundString(nextRDNColumn,
        NdbIndexScanOperation.BoundType.BoundLT, "");
    }

    // FIXME: Need NDB/J API like native interpretExitLastRow
    // to do this more efficiently without additional hops.
    op.getValue(BackendImpl.EID);

    String subtreeColumn = BackendImpl.DN2ID_DN +
      Integer.toString(dn.getNumComponents() + 1);
    if (!subtree &&
        (dn.getNumComponents() < BackendImpl.DN2ID_DN_NC))
    {
      op.getValue(subtreeColumn);
    }

    rs = op.resultData();
    ndbTxn.execute(ExecType.NoCommit, AbortOption.AO_IgnoreError, true);

    while (rs.next()) {
      if (!subtree &&
          (dn.getNumComponents() < BackendImpl.DN2ID_DN_NC))
      {
        String columnValue = rs.getString(subtreeColumn);
        if ((columnValue != null) && (columnValue.length() > 0))
        {
          continue;
        }
      }
      numSubordinates++;
    }

    return numSubordinates;
  }

  /**
   * Get a new instance of the Search Cursor object.
   * @param txn Abstract Transaction to be used for the operation.
   * @param baseDN Search Cursor base DN.
   * @return New instance of the Search Cursor object.
   */
  public DN2IDSearchCursor getSearchCursor(
    AbstractTransaction txn, DN baseDN) {
    return new DN2IDSearchCursor(txn, baseDN);
  }

  /**
   * This inner class represents the Search Cursor which can be
   * used to cursor entries in the database starting from some
   * arbitrary base DN.
   */
  protected class DN2IDSearchCursor
  {
    private NdbIndexScanOperation op;
    private NdbResultSet rs;
    private NdbTransaction ndbTxn;
    private AbstractTransaction txn;
    private DN baseDN;

    /**
     * Object constructor.
     * @param txn Abstract Transaction to be used for the operation.
     * @param baseDN Search Cursor base DN.
     */
    public DN2IDSearchCursor(
      AbstractTransaction txn,
      DN baseDN)
    {
      this.txn = txn;
      this.baseDN = baseDN;
    }

    /**
     * Open the cursor.
     * @throws com.mysql.cluster.ndbj.NdbApiException If an error
     * occurs in the database.
     */
    public void open() throws NdbApiException
    {
      ndbTxn = txn.getNdbTransaction();

      op = ndbTxn.getSelectIndexScanOperation(
        PRIMARY_INDEX_NAME, name, NdbOperation.LockMode.LM_CommittedRead);

      int numComponents = baseDN.getNumComponents();
      int componentIndex = numComponents - 1;
      for (int i = 0; i < numComponents; i++) {
        op.setBoundString(BackendImpl.DN2ID_DN +
          Integer.toString(i),
          NdbIndexScanOperation.BoundType.BoundEQ,
          baseDN.getRDN(componentIndex).toNormalizedString());
        componentIndex--;
      }

      if (baseDN.getNumComponents() < BackendImpl.DN2ID_DN_NC) {
        String nextRDNColumn =
          BackendImpl.DN2ID_DN + Integer.toString(numComponents);
        op.setBoundString(nextRDNColumn,
          NdbIndexScanOperation.BoundType.BoundLT, "");
      }

      op.getValue(BackendImpl.EID);

      for (int i = 0; i < BackendImpl.DN2ID_DN_NC; i++) {
        op.getValue(BackendImpl.DN2ID_DN +
          Integer.toString(i));
      }

      rs = op.resultData();
      ndbTxn.execute(ExecType.NoCommit, AbortOption.AbortOnError, true);
    }

    /**
     * Advance one position and return the result.
     * @return An instance of Search Cursor Result.
     * @throws com.mysql.cluster.ndbj.NdbApiException If an error
     * occurs in the database.
     */
    public SearchCursorResult getNext() throws NdbApiException
    {
      if (rs.next()) {
        long eid = rs.getLong(BackendImpl.EID);

        StringBuilder dnBuffer = new StringBuilder();
        int dnColumnIndex = BackendImpl.DN2ID_DN_NC - 1;
        while (dnColumnIndex >= 0) {
          String rdnString = rs.getString(BackendImpl.DN2ID_DN +
            Integer.toString(dnColumnIndex));
          if (rdnString.length() > 0) {
            dnBuffer.append(rdnString);
            if (dnColumnIndex > 0) {
              dnBuffer.append(",");
            }
          }
          dnColumnIndex--;
        }
        String dnString = dnBuffer.toString();

        if ((eid == 0) || (dnString.length() == 0)) {
          return null;
        }

        SearchCursorResult result =
          new SearchCursorResult(dnString, eid);
        return result;
      }
      return null;
    }

    /**
     * Close the cursor.
     */
    public void close()
    {
      ndbTxn = null;
      txn = null;
    }
  }

  /**
   * This inner class represents a Search Cursor Result
   * as returned by the Search Cursor operations.
   */
  protected class SearchCursorResult
  {
    /**
     * Entry DN.
     */
    public String dn;

    /**
     * Entry ID.
     */
    public long id;

    /**
     * Object constructor.
     * @param dn The entry DN.
     * @param id The entry ID.
     */
    public SearchCursorResult(String dn, long id)
    {
      this.dn = dn;
      this.id = id;
    }
  }
}
