001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 *      Portions copyright 2013 Manuel Gaupp
027 */
028package org.opends.server.backends.pluggable;
029
030import static org.forgerock.util.Utils.*;
031import static org.opends.messages.BackendMessages.*;
032import static org.opends.server.backends.pluggable.DnKeyFormat.*;
033import static org.opends.server.backends.pluggable.IndexFilter.*;
034import static org.opends.server.backends.pluggable.VLVIndex.*;
035import static org.opends.server.core.DirectoryServer.*;
036import static org.opends.server.protocols.ldap.LDAPResultCode.*;
037import static org.opends.server.types.AdditionalLogItem.*;
038import static org.opends.server.util.StaticUtils.*;
039
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.HashMap;
045import java.util.List;
046import java.util.Map;
047import java.util.NoSuchElementException;
048import java.util.Objects;
049import java.util.TreeMap;
050import java.util.concurrent.locks.Lock;
051import java.util.concurrent.locks.ReentrantReadWriteLock;
052
053import org.forgerock.i18n.LocalizableMessage;
054import org.forgerock.i18n.LocalizableMessageBuilder;
055import org.forgerock.i18n.slf4j.LocalizedLogger;
056import org.forgerock.opendj.config.server.ConfigChangeResult;
057import org.forgerock.opendj.config.server.ConfigException;
058import org.forgerock.opendj.ldap.ByteSequence;
059import org.forgerock.opendj.ldap.ByteString;
060import org.forgerock.opendj.ldap.ByteStringBuilder;
061import org.forgerock.opendj.ldap.ResultCode;
062import org.forgerock.opendj.ldap.SearchScope;
063import org.forgerock.util.Pair;
064import org.opends.messages.CoreMessages;
065import org.opends.server.admin.server.ConfigurationAddListener;
066import org.opends.server.admin.server.ConfigurationChangeListener;
067import org.opends.server.admin.server.ConfigurationDeleteListener;
068import org.opends.server.admin.std.server.BackendIndexCfg;
069import org.opends.server.admin.std.server.BackendVLVIndexCfg;
070import org.opends.server.admin.std.server.PluggableBackendCfg;
071import org.opends.server.api.ClientConnection;
072import org.opends.server.api.EntryCache;
073import org.opends.server.api.VirtualAttributeProvider;
074import org.opends.server.api.plugin.PluginResult.SubordinateDelete;
075import org.opends.server.api.plugin.PluginResult.SubordinateModifyDN;
076import org.opends.server.backends.pluggable.spi.AccessMode;
077import org.opends.server.backends.pluggable.spi.Cursor;
078import org.opends.server.backends.pluggable.spi.ReadOperation;
079import org.opends.server.backends.pluggable.spi.ReadableTransaction;
080import org.opends.server.backends.pluggable.spi.SequentialCursor;
081import org.opends.server.backends.pluggable.spi.Storage;
082import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
083import org.opends.server.backends.pluggable.spi.TreeName;
084import org.opends.server.backends.pluggable.spi.WriteOperation;
085import org.opends.server.backends.pluggable.spi.WriteableTransaction;
086import org.opends.server.controls.PagedResultsControl;
087import org.opends.server.controls.ServerSideSortRequestControl;
088import org.opends.server.controls.ServerSideSortResponseControl;
089import org.opends.server.controls.SubtreeDeleteControl;
090import org.opends.server.controls.VLVRequestControl;
091import org.opends.server.controls.VLVResponseControl;
092import org.opends.server.core.AddOperation;
093import org.opends.server.core.DeleteOperation;
094import org.opends.server.core.DirectoryServer;
095import org.opends.server.core.ModifyDNOperation;
096import org.opends.server.core.ModifyOperation;
097import org.opends.server.core.SearchOperation;
098import org.opends.server.protocols.ldap.LDAPResultCode;
099import org.opends.server.types.Attribute;
100import org.opends.server.types.AttributeType;
101import org.opends.server.types.Attributes;
102import org.opends.server.types.CanceledOperationException;
103import org.opends.server.types.Control;
104import org.opends.server.types.DN;
105import org.opends.server.types.DirectoryException;
106import org.opends.server.types.Entry;
107import org.opends.server.types.Modification;
108import org.opends.server.types.Operation;
109import org.opends.server.types.Privilege;
110import org.opends.server.types.RDN;
111import org.opends.server.types.SearchFilter;
112import org.opends.server.types.SortKey;
113import org.opends.server.types.SortOrder;
114import org.opends.server.types.VirtualAttributeRule;
115import org.opends.server.util.ServerConstants;
116import org.opends.server.util.StaticUtils;
117
118/**
119 * Storage container for LDAP entries.  Each base DN of a backend is given
120 * its own entry container.  The entry container is the object that implements
121 * the guts of the backend API methods for LDAP operations.
122 */
123public class EntryContainer
124    implements SuffixContainer, ConfigurationChangeListener<PluggableBackendCfg>
125{
126  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
127
128  /** The name of the entry tree. */
129  private static final String ID2ENTRY_TREE_NAME = ID2ENTRY_INDEX_NAME;
130  /** The name of the DN tree. */
131  private static final String DN2ID_TREE_NAME = DN2ID_INDEX_NAME;
132  /** The name of the children index tree. */
133  private static final String ID2CHILDREN_COUNT_TREE_NAME = ID2CHILDREN_COUNT_NAME;
134  /** The name of the referral tree. */
135  private static final String REFERRAL_TREE_NAME = REFERRAL_INDEX_NAME;
136  /** The name of the state tree. */
137  private static final String STATE_TREE_NAME = STATE_INDEX_NAME;
138
139  /** The attribute index configuration manager. */
140  private final AttributeIndexCfgManager attributeIndexCfgManager;
141  /** The vlv index configuration manager. */
142  private final VLVIndexCfgManager vlvIndexCfgManager;
143
144  /** The backend configuration. */
145  private PluggableBackendCfg config;
146  /** ID of the backend to which this entry container belongs. */
147  private final String backendID;
148  /** The baseDN this entry container is responsible for. */
149  private final DN baseDN;
150  /** The root container in which this entryContainer belongs. */
151  private final RootContainer rootContainer;
152  /** The tree storage. */
153  private final Storage storage;
154
155  /** The DN tree maps a normalized DN string to an entry ID (8 bytes). */
156  private final DN2ID dn2id;
157  /** The entry tree maps an entry ID (8 bytes) to a complete encoded entry. */
158  private ID2Entry id2entry;
159  /** Store the number of children for each entry. */
160  private final ID2ChildrenCount id2childrenCount;
161  /** The referral tree maps a normalized DN string to labeled URIs. */
162  private final DN2URI dn2uri;
163  /** The state tree maps a config DN to config entries. */
164  private final State state;
165
166  /** The set of attribute indexes. */
167  private final Map<AttributeType, AttributeIndex> attrIndexMap = new HashMap<>();
168  /** The set of VLV (Virtual List View) indexes. */
169  private final Map<String, VLVIndex> vlvIndexMap = new HashMap<>();
170
171  /**
172   * Prevents name clashes for common indexes (like id2entry) across multiple suffixes.
173   * For example when a root container contains multiple suffixes.
174   */
175  private final String treePrefix;
176
177  /**
178   * This class is responsible for managing the configuration for attribute
179   * indexes used within this entry container.
180   */
181  private class AttributeIndexCfgManager implements
182  ConfigurationAddListener<BackendIndexCfg>,
183  ConfigurationDeleteListener<BackendIndexCfg>
184  {
185    @Override
186    public boolean isConfigurationAddAcceptable(final BackendIndexCfg cfg, List<LocalizableMessage> unacceptableReasons)
187    {
188      try
189      {
190        new AttributeIndex(cfg, state, EntryContainer.this);
191        return true;
192      }
193      catch(Exception e)
194      {
195        unacceptableReasons.add(LocalizableMessage.raw(e.getLocalizedMessage()));
196        return false;
197      }
198    }
199
200    @Override
201    public ConfigChangeResult applyConfigurationAdd(final BackendIndexCfg cfg)
202    {
203      final ConfigChangeResult ccr = new ConfigChangeResult();
204      try
205      {
206        final AttributeIndex index = new AttributeIndex(cfg, state, EntryContainer.this);
207        storage.write(new WriteOperation()
208        {
209          @Override
210          public void run(WriteableTransaction txn) throws Exception
211          {
212            index.open(txn, true);
213            if (!index.isTrusted())
214            {
215              ccr.setAdminActionRequired(true);
216              ccr.addMessage(NOTE_INDEX_ADD_REQUIRES_REBUILD.get(cfg.getAttribute().getNameOrOID()));
217            }
218            attrIndexMap.put(cfg.getAttribute(), index);
219          }
220        });
221      }
222      catch(Exception e)
223      {
224        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
225        ccr.addMessage(LocalizableMessage.raw(e.getLocalizedMessage()));
226      }
227      return ccr;
228    }
229
230    @Override
231    public boolean isConfigurationDeleteAcceptable(
232        BackendIndexCfg cfg, List<LocalizableMessage> unacceptableReasons)
233    {
234      // TODO: validate more before returning true?
235      return true;
236    }
237
238    @Override
239    public ConfigChangeResult applyConfigurationDelete(final BackendIndexCfg cfg)
240    {
241      final ConfigChangeResult ccr = new ConfigChangeResult();
242
243      exclusiveLock.lock();
244      try
245      {
246        storage.write(new WriteOperation()
247        {
248          @Override
249          public void run(WriteableTransaction txn) throws Exception
250          {
251            attrIndexMap.remove(cfg.getAttribute()).closeAndDelete(txn);
252          }
253        });
254      }
255      catch (Exception de)
256      {
257        ccr.setResultCode(getServerErrorResultCode());
258        ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(de)));
259      }
260      finally
261      {
262        exclusiveLock.unlock();
263      }
264
265      return ccr;
266    }
267  }
268
269  /**
270   * This class is responsible for managing the configuration for VLV indexes
271   * used within this entry container.
272   */
273  private class VLVIndexCfgManager implements
274  ConfigurationAddListener<BackendVLVIndexCfg>,
275  ConfigurationDeleteListener<BackendVLVIndexCfg>
276  {
277    @Override
278    public boolean isConfigurationAddAcceptable(BackendVLVIndexCfg cfg, List<LocalizableMessage> unacceptableReasons)
279    {
280      // TODO JNR remove du-plication
281      try
282      {
283        SearchFilter.createFilterFromString(cfg.getFilter());
284      }
285      catch(Exception e)
286      {
287        unacceptableReasons.add(ERR_CONFIG_VLV_INDEX_BAD_FILTER.get(
288            cfg.getFilter(), cfg.getName(), e.getLocalizedMessage()));
289        return false;
290      }
291
292      String[] sortAttrs = cfg.getSortOrder().split(" ");
293      SortKey[] sortKeys = new SortKey[sortAttrs.length];
294      boolean[] ascending = new boolean[sortAttrs.length];
295      for(int i = 0; i < sortAttrs.length; i++)
296      {
297        try
298        {
299          ascending[i] = !sortAttrs[i].startsWith("-");
300
301          if (sortAttrs[i].startsWith("-") || sortAttrs[i].startsWith("+"))
302          {
303            sortAttrs[i] = sortAttrs[i].substring(1);
304          }
305        }
306        catch(Exception e)
307        {
308          unacceptableReasons.add(ERR_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortKeys[i], cfg.getName()));
309          return false;
310        }
311
312        AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(sortAttrs[i].toLowerCase());
313        if(attrType == null)
314        {
315          unacceptableReasons.add(ERR_CONFIG_VLV_INDEX_UNDEFINED_ATTR.get(sortAttrs[i], cfg.getName()));
316          return false;
317        }
318        // TODO Add ordering matching rule null check
319        sortKeys[i] = new SortKey(attrType, ascending[i]);
320      }
321
322      return true;
323    }
324
325    @Override
326    public ConfigChangeResult applyConfigurationAdd(final BackendVLVIndexCfg cfg)
327    {
328      final ConfigChangeResult ccr = new ConfigChangeResult();
329      try
330      {
331        storage.write(new WriteOperation()
332        {
333          @Override
334          public void run(WriteableTransaction txn) throws Exception
335          {
336            VLVIndex vlvIndex = new VLVIndex(cfg, state, storage, EntryContainer.this, txn);
337            vlvIndex.open(txn, true);
338            if(!vlvIndex.isTrusted())
339            {
340              ccr.setAdminActionRequired(true);
341              ccr.addMessage(NOTE_INDEX_ADD_REQUIRES_REBUILD.get(cfg.getName()));
342            }
343            vlvIndexMap.put(cfg.getName().toLowerCase(), vlvIndex);
344          }
345        });
346      }
347      catch(Exception e)
348      {
349        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
350        ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e)));
351      }
352      return ccr;
353    }
354
355    @Override
356    public boolean isConfigurationDeleteAcceptable(BackendVLVIndexCfg cfg, List<LocalizableMessage> unacceptableReasons)
357    {
358      // TODO: validate more before returning true?
359      return true;
360    }
361
362    @Override
363    public ConfigChangeResult applyConfigurationDelete(final BackendVLVIndexCfg cfg)
364    {
365      final ConfigChangeResult ccr = new ConfigChangeResult();
366      exclusiveLock.lock();
367      try
368      {
369        storage.write(new WriteOperation()
370        {
371          @Override
372          public void run(WriteableTransaction txn) throws Exception
373          {
374            vlvIndexMap.remove(cfg.getName().toLowerCase()).closeAndDelete(txn);
375          }
376        });
377      }
378      catch (Exception e)
379      {
380        ccr.setResultCode(getServerErrorResultCode());
381        ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e)));
382      }
383      finally
384      {
385        exclusiveLock.unlock();
386      }
387      return ccr;
388    }
389  }
390
391  /** A read write lock to handle schema changes and bulk changes. */
392  private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
393  final Lock sharedLock = lock.readLock();
394  final Lock exclusiveLock = lock.writeLock();
395
396  /**
397   * Create a new entry container object.
398   *
399   * @param baseDN  The baseDN this entry container will be responsible for
400   *                storing on disk.
401   * @param backendID  ID of the backend that is creating this entry container.
402   *                   It is needed by the Directory Server entry cache methods.
403   * @param config The configuration of the backend.
404   * @param storage The storage for this entryContainer.
405   * @param rootContainer The root container this entry container is in.
406   * @throws ConfigException if a configuration related error occurs.
407   */
408  EntryContainer(DN baseDN, String backendID, PluggableBackendCfg config, Storage storage,
409      RootContainer rootContainer) throws ConfigException
410  {
411    this.backendID = backendID;
412    this.baseDN = baseDN;
413    this.config = config;
414    this.storage = storage;
415    this.rootContainer = rootContainer;
416    this.treePrefix = baseDN.toNormalizedUrlSafeString();
417    this.id2childrenCount = new ID2ChildrenCount(getIndexName(ID2CHILDREN_COUNT_TREE_NAME));
418    this.dn2id = new DN2ID(getIndexName(DN2ID_TREE_NAME), baseDN);
419    this.dn2uri = new DN2URI(getIndexName(REFERRAL_TREE_NAME), this);
420    this.state = new State(getIndexName(STATE_TREE_NAME));
421
422    config.addPluggableChangeListener(this);
423
424    attributeIndexCfgManager = new AttributeIndexCfgManager();
425    config.addBackendIndexAddListener(attributeIndexCfgManager);
426    config.addBackendIndexDeleteListener(attributeIndexCfgManager);
427
428    vlvIndexCfgManager = new VLVIndexCfgManager();
429    config.addBackendVLVIndexAddListener(vlvIndexCfgManager);
430    config.addBackendVLVIndexDeleteListener(vlvIndexCfgManager);
431  }
432
433  private TreeName getIndexName(String indexId)
434  {
435    return new TreeName(treePrefix, indexId);
436  }
437
438  /**
439   * Opens the entryContainer for reading and writing.
440   *
441   * @param txn a non null transaction
442   * @param accessMode specifies how the container has to be opened (read-write or read-only)
443   * @throws StorageRuntimeException If an error occurs in the storage.
444   * @throws ConfigException if a configuration related error occurs.
445   */
446  void open(WriteableTransaction txn, AccessMode accessMode) throws StorageRuntimeException, ConfigException
447  {
448    boolean shouldCreate = accessMode.isWriteable();
449    try
450    {
451      DataConfig entryDataConfig = new DataConfig(
452          config.isEntriesCompressed(), config.isCompactEncoding(), rootContainer.getCompressedSchema());
453
454      id2entry = new ID2Entry(getIndexName(ID2ENTRY_TREE_NAME), entryDataConfig);
455      id2entry.open(txn, shouldCreate);
456      id2childrenCount.open(txn, shouldCreate);
457      dn2id.open(txn, shouldCreate);
458      state.open(txn, shouldCreate);
459      dn2uri.open(txn, shouldCreate);
460
461      for (String idx : config.listBackendIndexes())
462      {
463        BackendIndexCfg indexCfg = config.getBackendIndex(idx);
464
465        final AttributeIndex index = new AttributeIndex(indexCfg, state, this);
466        index.open(txn, shouldCreate);
467        if(!index.isTrusted())
468        {
469          logger.info(NOTE_INDEX_ADD_REQUIRES_REBUILD, index.getName());
470        }
471        attrIndexMap.put(indexCfg.getAttribute(), index);
472      }
473
474      for (String idx : config.listBackendVLVIndexes())
475      {
476        BackendVLVIndexCfg vlvIndexCfg = config.getBackendVLVIndex(idx);
477
478        VLVIndex vlvIndex = new VLVIndex(vlvIndexCfg, state, storage, this, txn);
479        vlvIndex.open(txn, shouldCreate);
480        if(!vlvIndex.isTrusted())
481        {
482          logger.info(NOTE_INDEX_ADD_REQUIRES_REBUILD, vlvIndex.getName());
483        }
484
485        vlvIndexMap.put(vlvIndexCfg.getName().toLowerCase(), vlvIndex);
486      }
487    }
488    catch (StorageRuntimeException de)
489    {
490      logger.traceException(de);
491      close();
492      throw de;
493    }
494  }
495
496  /**
497   * Closes the entry container.
498   *
499   * @throws StorageRuntimeException If an error occurs in the storage.
500   */
501  @Override
502  public void close() throws StorageRuntimeException
503  {
504    closeSilently(attrIndexMap.values());
505    closeSilently(vlvIndexMap.values());
506
507    // Deregister any listeners.
508    config.removePluggableChangeListener(this);
509    config.removeBackendIndexAddListener(attributeIndexCfgManager);
510    config.removeBackendIndexDeleteListener(attributeIndexCfgManager);
511    config.removeBackendVLVIndexAddListener(vlvIndexCfgManager);
512    config.removeBackendVLVIndexDeleteListener(vlvIndexCfgManager);
513  }
514
515  /**
516   * Retrieves a reference to the root container in which this entry container
517   * exists.
518   *
519   * @return  A reference to the root container in which this entry container
520   *          exists.
521   */
522  RootContainer getRootContainer()
523  {
524    return rootContainer;
525  }
526
527  /**
528   * Get the DN tree used by this entry container.
529   * The entryContainer must have been opened.
530   *
531   * @return The DN tree.
532   */
533  DN2ID getDN2ID()
534  {
535    return dn2id;
536  }
537
538  /**
539   * Get the entry tree used by this entry container.
540   * The entryContainer must have been opened.
541   *
542   * @return The entry tree.
543   */
544  ID2Entry getID2Entry()
545  {
546    return id2entry;
547  }
548
549  /**
550   * Get the referral tree used by this entry container.
551   * The entryContainer must have been opened.
552   *
553   * @return The referral tree.
554   */
555  DN2URI getDN2URI()
556  {
557    return dn2uri;
558  }
559
560  /**
561   * Get the children tree used by this entry container.
562   * The entryContainer must have been opened.
563   *
564   * @return The children tree.
565   */
566  ID2ChildrenCount getID2ChildrenCount()
567  {
568    return id2childrenCount;
569  }
570
571  /**
572   * Look for an attribute index for the given attribute type.
573   *
574   * @param attrType The attribute type for which an attribute index is needed.
575   * @return The attribute index or null if there is none for that type.
576   */
577  AttributeIndex getAttributeIndex(AttributeType attrType)
578  {
579    return attrIndexMap.get(attrType);
580  }
581
582  /**
583   * Look for a VLV index for the given index name.
584   *
585   * @param vlvIndexName The vlv index name for which an vlv index is needed.
586   * @return The VLV index or null if there is none with that name.
587   */
588  VLVIndex getVLVIndex(String vlvIndexName)
589  {
590    return vlvIndexMap.get(vlvIndexName);
591  }
592
593  /**
594   * Retrieve all attribute indexes.
595   *
596   * @return All attribute indexes defined in this entry container.
597   */
598  Collection<AttributeIndex> getAttributeIndexes()
599  {
600    return attrIndexMap.values();
601  }
602
603  /**
604   * Retrieve all VLV indexes.
605   *
606   * @return The collection of VLV indexes defined in this entry container.
607   */
608  Collection<VLVIndex> getVLVIndexes()
609  {
610    return vlvIndexMap.values();
611  }
612
613  /**
614   * Determine the highest entryID in the entryContainer.
615   * The entryContainer must already be open.
616   *
617   * @param txn a non null transaction
618   * @return The highest entry ID.
619   * @throws StorageRuntimeException If an error occurs in the storage.
620   */
621  EntryID getHighestEntryID(ReadableTransaction txn) throws StorageRuntimeException
622  {
623    try (Cursor<ByteString, ByteString> cursor = txn.openCursor(id2entry.getName()))
624    {
625      // Position a cursor on the last data item, and the key should give the highest ID.
626      if (cursor.positionToLastKey())
627      {
628        return new EntryID(cursor.getKey());
629      }
630      return new EntryID(0);
631    }
632  }
633
634  boolean hasSubordinates(final DN dn)
635  {
636    try
637    {
638      return storage.read(new ReadOperation<Boolean>()
639      {
640        @Override
641        public Boolean run(final ReadableTransaction txn) throws Exception
642        {
643          try (final SequentialCursor<?, ?> cursor = dn2id.openChildrenCursor(txn, dn))
644          {
645            return cursor.next();
646          }
647        }
648      });
649    }
650    catch (Exception e)
651    {
652      throw new StorageRuntimeException(e);
653    }
654  }
655
656  /**
657   * Determine the number of children entries for a given entry.
658   *
659   * @param entryDN The distinguished name of the entry.
660   * @return The number of children entries for the given entry or -1 if
661   *         the entry does not exist.
662   * @throws StorageRuntimeException If an error occurs in the storage.
663   */
664  long getNumberOfChildren(final DN entryDN) throws StorageRuntimeException
665  {
666    try
667    {
668      return storage.read(new ReadOperation<Long>()
669      {
670        @Override
671        public Long run(ReadableTransaction txn) throws Exception
672        {
673          final EntryID entryID = dn2id.get(txn, entryDN);
674          return entryID != null ? id2childrenCount.getCount(txn, entryID) : -1;
675        }
676      });
677    }
678    catch (Exception e)
679    {
680      throw new StorageRuntimeException(e);
681    }
682  }
683
684  /**
685   * Processes the specified search in this entryContainer.
686   * Matching entries should be provided back to the core server using the
687   * <CODE>SearchOperation.returnEntry</CODE> method.
688   *
689   * @param searchOperation The search operation to be processed.
690   * @throws DirectoryException
691   *          If a problem occurs while processing the
692   *          search.
693   * @throws StorageRuntimeException If an error occurs in the storage.
694   * @throws CanceledOperationException if this operation should be cancelled.
695   */
696  void search(final SearchOperation searchOperation)
697  throws DirectoryException, StorageRuntimeException, CanceledOperationException
698  {
699    try
700    {
701      storage.read(new ReadOperation<Void>()
702      {
703        @Override
704        public Void run(final ReadableTransaction txn) throws Exception
705        {
706          DN aBaseDN = searchOperation.getBaseDN();
707          SearchScope searchScope = searchOperation.getScope();
708
709          PagedResultsControl pageRequest = searchOperation.getRequestControl(PagedResultsControl.DECODER);
710          ServerSideSortRequestControl sortRequest =
711              searchOperation.getRequestControl(ServerSideSortRequestControl.DECODER);
712          if (sortRequest != null && !sortRequest.containsSortKeys() && sortRequest.isCritical())
713          {
714            /*
715             * If the control's criticality field is true then the server SHOULD
716             * do the following: return unavailableCriticalExtension as a return
717             * code in the searchResultDone message; include the
718             * sortKeyResponseControl in the searchResultDone message, and not
719             * send back any search result entries.
720             */
721            searchOperation.addResponseControl(newServerSideSortControl(NO_SUCH_ATTRIBUTE));
722            searchOperation.setResultCode(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
723            return null;
724          }
725
726          VLVRequestControl vlvRequest = searchOperation.getRequestControl(VLVRequestControl.DECODER);
727          if (vlvRequest != null && pageRequest != null)
728          {
729            throw new DirectoryException(
730                ResultCode.CONSTRAINT_VIOLATION, ERR_SEARCH_CANNOT_MIX_PAGEDRESULTS_AND_VLV.get());
731          }
732
733          // Handle client abandon of paged results.
734          if (pageRequest != null)
735          {
736            if (pageRequest.getSize() == 0)
737            {
738              Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null);
739              searchOperation.getResponseControls().add(control);
740              return null;
741            }
742            if (searchOperation.getSizeLimit() > 0 && pageRequest.getSize() >= searchOperation.getSizeLimit())
743            {
744              // The RFC says : "If the page size is greater than or equal to the
745              // sizeLimit value, the server should ignore the control as the
746              // request can be satisfied in a single page"
747              pageRequest = null;
748            }
749          }
750
751          // Handle base-object search first.
752          if (searchScope == SearchScope.BASE_OBJECT)
753          {
754            final Entry baseEntry = fetchBaseEntry(txn, aBaseDN, searchScope);
755            if (!isManageDsaITOperation(searchOperation))
756            {
757              dn2uri.checkTargetForReferral(baseEntry, searchOperation.getScope());
758            }
759
760            if (searchOperation.getFilter().matchesEntry(baseEntry))
761            {
762              searchOperation.returnEntry(baseEntry, null);
763            }
764
765            if (pageRequest != null)
766            {
767              // Indicate no more pages.
768              Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null);
769              searchOperation.getResponseControls().add(control);
770            }
771
772            return null;
773          }
774
775          // Check whether the client requested debug information about the
776          // contribution of the indexes to the search.
777          StringBuilder debugBuffer = null;
778          if (searchOperation.getAttributes().contains(ATTR_DEBUG_SEARCH_INDEX))
779          {
780            debugBuffer = new StringBuilder();
781          }
782
783          EntryIDSet entryIDSet = null;
784          boolean candidatesAreInScope = false;
785          if (sortRequest != null)
786          {
787            for (VLVIndex vlvIndex : vlvIndexMap.values())
788            {
789              try
790              {
791                entryIDSet = vlvIndex.evaluate(txn, searchOperation, sortRequest, vlvRequest, debugBuffer);
792                if (entryIDSet != null)
793                {
794                  searchOperation.addResponseControl(newServerSideSortControl(SUCCESS));
795                  candidatesAreInScope = true;
796                  break;
797                }
798              }
799              catch (DirectoryException de)
800              {
801                serverSideSortControlError(searchOperation, sortRequest, de);
802              }
803            }
804          }
805
806          // Combining server-side sort with paged result controls
807          // requires us to use an entryIDSet where the entryIDs are ordered
808          // so further paging can restart where it previously stopped
809          long[] entryIDReorderedSet;
810          if (entryIDSet == null)
811          {
812            if (processSearchWithVirtualAttributeRule(searchOperation, true))
813            {
814              return null;
815            }
816
817            // Create an index filter to get the search result candidate entries
818            IndexFilter indexFilter = new IndexFilter(
819                EntryContainer.this, txn, searchOperation, debugBuffer, rootContainer.getMonitorProvider());
820
821            // Evaluate the filter against the attribute indexes.
822            entryIDSet = indexFilter.evaluate();
823
824            if (!isBelowFilterThreshold(entryIDSet))
825            {
826              final int lookThroughLimit = searchOperation.getClientConnection().getLookthroughLimit();
827              final int indexLimit =
828                  config.getIndexEntryLimit() == 0 ? CURSOR_ENTRY_LIMIT : config.getIndexEntryLimit();
829              final int idSetLimit = lookThroughLimit > 0 ? Math.min(indexLimit, lookThroughLimit) : indexLimit;
830
831              final EntryIDSet scopeSet = getIDSetFromScope(txn, aBaseDN, searchScope, idSetLimit);
832              entryIDSet.retainAll(scopeSet);
833              if (debugBuffer != null)
834              {
835                debugBuffer.append(" scope=").append(searchScope);
836                scopeSet.toString(debugBuffer);
837              }
838              if (scopeSet.isDefined())
839              {
840                // In this case we know that every candidate is in scope.
841                candidatesAreInScope = true;
842              }
843            }
844
845            if (sortRequest != null)
846            {
847              // If the sort key is not present, the sorting will generate the
848              // default ordering. VLV search request goes through as if
849              // this sort key was not found in the user entry.
850              try
851              {
852                SortOrder sortOrder = sortRequest.getSortOrder();
853                entryIDReorderedSet = sort(txn, entryIDSet, searchOperation, sortOrder, vlvRequest);
854              }
855              catch (DirectoryException de)
856              {
857                entryIDReorderedSet = entryIDSet.toLongArray();
858                serverSideSortControlError(searchOperation, sortRequest, de);
859              }
860              try
861              {
862                if (sortRequest.containsSortKeys())
863                {
864                  searchOperation.addResponseControl(newServerSideSortControl(SUCCESS));
865                }
866                else
867                {
868                  /*
869                   * There is no sort key associated with the sort control.
870                   * Since it came here it means that the criticality is false
871                   * so let the server return all search results unsorted and
872                   * include the sortKeyResponseControl in the searchResultDone
873                   * message.
874                   */
875                  searchOperation.addResponseControl(newServerSideSortControl(NO_SUCH_ATTRIBUTE));
876                }
877              }
878              catch (DirectoryException de)
879              {
880                serverSideSortControlError(searchOperation, sortRequest, de);
881              }
882            }
883            else
884            {
885              entryIDReorderedSet = entryIDSet.toLongArray();
886            }
887          }
888          else
889          {
890            entryIDReorderedSet = entryIDSet.toLongArray();
891          }
892
893          // If requested, construct and return a fictitious entry containing
894          // debug information, and no other entries.
895          if (debugBuffer != null)
896          {
897            debugBuffer.append(" final=");
898            entryIDSet.toString(debugBuffer);
899
900            Entry debugEntry = buildDebugSearchIndexEntry(debugBuffer);
901            searchOperation.returnEntry(debugEntry, null);
902            return null;
903          }
904
905          if (entryIDReorderedSet != null)
906          {
907            rootContainer.getMonitorProvider().incrementIndexedSearchCount();
908            searchIndexed(txn, entryIDReorderedSet, candidatesAreInScope, searchOperation, pageRequest);
909          }
910          else
911          {
912            rootContainer.getMonitorProvider().incrementUnindexedSearchCount();
913
914            searchOperation.addAdditionalLogItem(keyOnly(getClass(), "unindexed"));
915
916            if (processSearchWithVirtualAttributeRule(searchOperation, false))
917            {
918              return null;
919            }
920
921            ClientConnection clientConnection = searchOperation.getClientConnection();
922            if (!clientConnection.hasPrivilege(Privilege.UNINDEXED_SEARCH, searchOperation))
923            {
924              throw new DirectoryException(
925                  ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_SEARCH_UNINDEXED_INSUFFICIENT_PRIVILEGES.get());
926            }
927
928            if (sortRequest != null)
929            {
930              // FIXME -- Add support for sorting unindexed searches using indexes
931              // like DSEE currently does.
932              searchOperation.addResponseControl(newServerSideSortControl(UNWILLING_TO_PERFORM));
933
934              if (sortRequest.isCritical())
935              {
936                throw new DirectoryException(
937                    ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_SEARCH_CANNOT_SORT_UNINDEXED.get());
938              }
939            }
940
941            searchNotIndexed(txn, searchOperation, pageRequest);
942          }
943          return null;
944        }
945
946        private void serverSideSortControlError(final SearchOperation searchOperation,
947            ServerSideSortRequestControl sortRequest, DirectoryException de) throws DirectoryException
948        {
949          searchOperation.addResponseControl(newServerSideSortControl(de.getResultCode().intValue()));
950
951          if (sortRequest.isCritical())
952          {
953            throw de;
954          }
955        }
956
957        private ServerSideSortResponseControl newServerSideSortControl(int resultCode)
958        {
959          return new ServerSideSortResponseControl(resultCode, null);
960        }
961
962        private EntryIDSet getIDSetFromScope(final ReadableTransaction txn, DN aBaseDN, SearchScope searchScope,
963            int idSetLimit) throws DirectoryException
964        {
965          final EntryIDSet scopeSet;
966          try
967          {
968            switch (searchScope.asEnum())
969            {
970            case BASE_OBJECT:
971              try (final SequentialCursor<?, EntryID> scopeCursor = dn2id.openCursor(txn, aBaseDN))
972              {
973                scopeSet = EntryIDSet.newDefinedSet(scopeCursor.getValue().longValue());
974              }
975              break;
976            case SINGLE_LEVEL:
977              try (final SequentialCursor<?, EntryID> scopeCursor = dn2id.openChildrenCursor(txn, aBaseDN))
978              {
979                scopeSet = newIDSetFromCursor(scopeCursor, false, idSetLimit);
980              }
981              break;
982            case SUBORDINATES:
983            case WHOLE_SUBTREE:
984              try (final SequentialCursor<?, EntryID> scopeCursor = dn2id.openSubordinatesCursor(txn, aBaseDN))
985              {
986                scopeSet = newIDSetFromCursor(scopeCursor, searchScope.equals(SearchScope.WHOLE_SUBTREE), idSetLimit);
987              }
988              break;
989            default:
990              throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
991                  CoreMessages.INFO_ERROR_SEARCH_SCOPE_NOT_ALLOWED.get());
992            }
993          }
994          catch (NoSuchElementException e)
995          {
996            throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_SEARCH_NO_SUCH_OBJECT.get(aBaseDN),
997                getMatchedDN(txn, aBaseDN), e);
998          }
999          return scopeSet;
1000        }
1001      });
1002    }
1003    catch (Exception e)
1004    {
1005      throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class);
1006    }
1007  }
1008
1009  private static EntryIDSet newIDSetFromCursor(SequentialCursor<?, EntryID> cursor, boolean includeCurrent,
1010      int idSetLimit)
1011  {
1012    long entryIDs[] = new long[idSetLimit];
1013    int offset = 0;
1014    if (includeCurrent)
1015    {
1016      entryIDs[offset++] = cursor.getValue().longValue();
1017    }
1018
1019    while(offset < idSetLimit && cursor.next())
1020    {
1021      entryIDs[offset++] = cursor.getValue().longValue();
1022    }
1023
1024    if (offset == idSetLimit && cursor.next())
1025    {
1026      return EntryIDSet.newUndefinedSet();
1027    }
1028    else if (offset != idSetLimit)
1029    {
1030      entryIDs = Arrays.copyOf(entryIDs, offset);
1031    }
1032    Arrays.sort(entryIDs);
1033
1034    return EntryIDSet.newDefinedSet(entryIDs);
1035  }
1036
1037  private <E1 extends Exception, E2 extends Exception>
1038      void throwAllowedExceptionTypes(Exception e, Class<E1> clazz1, Class<E2> clazz2)
1039          throws E1, E2
1040  {
1041    throwIfPossible(e, clazz1, clazz2);
1042    if (e.getCause() != null)
1043    {
1044      throwIfPossible(e.getCause(), clazz1, clazz2);
1045    }
1046    else if (e instanceof StorageRuntimeException)
1047    {
1048      throw (StorageRuntimeException) e;
1049    }
1050    throw new StorageRuntimeException(e);
1051  }
1052
1053  private static <E1 extends Exception, E2 extends Exception> void throwIfPossible(final Throwable cause,
1054      Class<E1> clazz1, Class<E2> clazz2) throws E1, E2
1055  {
1056    if (clazz1.isAssignableFrom(cause.getClass()))
1057    {
1058      throw clazz1.cast(cause);
1059    }
1060    else if (clazz2.isAssignableFrom(cause.getClass()))
1061    {
1062      throw clazz2.cast(cause);
1063    }
1064  }
1065
1066  private static boolean processSearchWithVirtualAttributeRule(final SearchOperation searchOperation,
1067      boolean isPreIndexed)
1068  {
1069    for (VirtualAttributeRule rule : DirectoryServer.getVirtualAttributes())
1070    {
1071      VirtualAttributeProvider<?> provider = rule.getProvider();
1072      if (provider.isSearchable(rule, searchOperation, isPreIndexed))
1073      {
1074        provider.processSearch(rule, searchOperation);
1075        return true;
1076      }
1077    }
1078    return false;
1079  }
1080
1081  private static Entry buildDebugSearchIndexEntry(StringBuilder debugBuffer) throws DirectoryException
1082  {
1083    Attribute attr = Attributes.create(ATTR_DEBUG_SEARCH_INDEX, debugBuffer.toString());
1084    Entry entry = new Entry(DN.valueOf("cn=debugsearch"), null, null, null);
1085    entry.addAttribute(attr, new ArrayList<ByteString>());
1086    return entry;
1087  }
1088
1089  /**
1090   * We were not able to obtain a set of candidate entry IDs for the
1091   * search from the indexes.
1092   * <p>
1093   * Here we are relying on the DN key order to ensure children are
1094   * returned after their parents.
1095   * <ul>
1096   * <li>iterate through a subtree range of the DN tree
1097   * <li>discard non-children DNs if the search scope is single level
1098   * <li>fetch the entry by ID from the entry cache or the entry tree
1099   * <li>return the entry if it matches the filter
1100   * </ul>
1101   *
1102   * @param searchOperation The search operation.
1103   * @param pageRequest A Paged Results control, or null if none.
1104   * @throws DirectoryException If an error prevented the search from being
1105   * processed.
1106   */
1107  private void searchNotIndexed(ReadableTransaction txn, SearchOperation searchOperation,
1108      PagedResultsControl pageRequest) throws DirectoryException, CanceledOperationException
1109  {
1110    DN aBaseDN = searchOperation.getBaseDN();
1111    SearchScope searchScope = searchOperation.getScope();
1112    boolean manageDsaIT = isManageDsaITOperation(searchOperation);
1113
1114    // The base entry must already have been processed if this is
1115    // a request for the next page in paged results.  So we skip
1116    // the base entry processing if the cookie is set.
1117    if (pageRequest == null || pageRequest.getCookie().length() == 0)
1118    {
1119      final Entry baseEntry = fetchBaseEntry(txn, aBaseDN, searchScope);
1120      if (!manageDsaIT)
1121      {
1122        dn2uri.checkTargetForReferral(baseEntry, searchScope);
1123      }
1124
1125      /* The base entry is only included for whole subtree search. */
1126      if (searchScope == SearchScope.WHOLE_SUBTREE
1127          && searchOperation.getFilter().matchesEntry(baseEntry))
1128      {
1129        searchOperation.returnEntry(baseEntry, null);
1130      }
1131
1132      if (!manageDsaIT
1133          && !dn2uri.returnSearchReferences(txn, searchOperation)
1134          && pageRequest != null)
1135      {
1136        // Indicate no more pages.
1137        Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null);
1138        searchOperation.getResponseControls().add(control);
1139      }
1140    }
1141
1142    /*
1143     * We will iterate forwards through a range of the dn2id keys to
1144     * find subordinates of the target entry from the top of the tree
1145     * downwards. For example, any subordinates of "dc=example,dc=com" appear
1146     * in dn2id with a key ending in ",dc=example,dc=com". The entry
1147     * "cn=joe,ou=people,dc=example,dc=com" will appear after the entry
1148     * "ou=people,dc=example,dc=com".
1149     */
1150    ByteString baseDNKey = dnToDNKey(aBaseDN, this.baseDN.size());
1151    ByteStringBuilder suffix = beforeKey(baseDNKey);
1152    ByteStringBuilder end = afterKey(baseDNKey);
1153
1154    // Set the starting value.
1155    ByteSequence begin;
1156    if (pageRequest != null && pageRequest.getCookie().length() != 0)
1157    {
1158      // The cookie contains the DN of the next entry to be returned.
1159      try
1160      {
1161        begin = ByteString.wrap(pageRequest.getCookie().toByteArray());
1162      }
1163      catch (Exception e)
1164      {
1165        logger.traceException(e);
1166        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
1167            ERR_INVALID_PAGED_RESULTS_COOKIE.get(pageRequest.getCookie().toHexString()), e);
1168      }
1169    }
1170    else
1171    {
1172      // Set the starting value to the suffix.
1173      begin = suffix;
1174    }
1175
1176    int lookthroughCount = 0;
1177    int lookthroughLimit = searchOperation.getClientConnection().getLookthroughLimit();
1178
1179    try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(dn2id.getName()))
1180    {
1181      // Initialize the cursor very close to the starting value.
1182      boolean success = cursor.positionToKeyOrNext(begin);
1183
1184      // Step forward until we pass the ending value.
1185      while (success && cursor.getKey().compareTo(end) < 0)
1186      {
1187        if (lookthroughLimit > 0 && lookthroughCount > lookthroughLimit)
1188        {
1189          // Lookthrough limit exceeded
1190          searchOperation.setResultCode(ResultCode.ADMIN_LIMIT_EXCEEDED);
1191          searchOperation.appendErrorMessage(NOTE_LOOKTHROUGH_LIMIT_EXCEEDED.get(lookthroughLimit));
1192          return;
1193        }
1194
1195        // We have found a subordinate entry.
1196        EntryID entryID = new EntryID(cursor.getValue());
1197        boolean isInScope =
1198            searchScope != SearchScope.SINGLE_LEVEL
1199                // Check if this entry is an immediate child.
1200                || findDNKeyParent(cursor.getKey()) == baseDNKey.length();
1201        if (isInScope)
1202        {
1203          // Process the candidate entry.
1204          final Entry entry = getEntry(txn, entryID);
1205          if (entry != null)
1206          {
1207            lookthroughCount++;
1208
1209            if ((manageDsaIT || entry.getReferralURLs() == null)
1210                && searchOperation.getFilter().matchesEntry(entry))
1211            {
1212              if (pageRequest != null
1213                  && searchOperation.getEntriesSent() == pageRequest.getSize())
1214              {
1215                // The current page is full.
1216                // Set the cookie to remember where we were.
1217                ByteString cookie = cursor.getKey();
1218                Control control = new PagedResultsControl(pageRequest.isCritical(), 0, cookie);
1219                searchOperation.getResponseControls().add(control);
1220                return;
1221              }
1222
1223              if (!searchOperation.returnEntry(entry, null))
1224              {
1225                // We have been told to discontinue processing of the
1226                // search. This could be due to size limit exceeded or
1227                // operation cancelled.
1228                return;
1229              }
1230            }
1231          }
1232        }
1233
1234        searchOperation.checkIfCanceled(false);
1235
1236        // Move to the next record.
1237        success = cursor.next();
1238      }
1239    }
1240    catch (StorageRuntimeException e)
1241    {
1242      logger.traceException(e);
1243    }
1244
1245    if (pageRequest != null)
1246    {
1247      // Indicate no more pages.
1248      Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null);
1249      searchOperation.getResponseControls().add(control);
1250    }
1251  }
1252
1253  /**
1254   * Returns the entry corresponding to the provided entryID.
1255   *
1256   * @param txn a non null transaction
1257   * @param entryID
1258   *          the id of the entry to retrieve
1259   * @return the entry corresponding to the provided entryID
1260   * @throws DirectoryException
1261   *           If an error occurs retrieving the entry
1262   */
1263  private Entry getEntry(ReadableTransaction txn, EntryID entryID) throws DirectoryException
1264  {
1265    // Try the entry cache first.
1266    final EntryCache<?> entryCache = getEntryCache();
1267    final Entry cacheEntry = entryCache.getEntry(backendID, entryID.longValue());
1268    if (cacheEntry != null)
1269    {
1270      return cacheEntry;
1271    }
1272
1273    final Entry entry = id2entry.get(txn, entryID);
1274    if (entry != null)
1275    {
1276      // Put the entry in the cache making sure not to overwrite a newer copy
1277      // that may have been inserted since the time we read the cache.
1278      entryCache.putEntryIfAbsent(entry, backendID, entryID.longValue());
1279    }
1280    return entry;
1281  }
1282
1283  /**
1284   * We were able to obtain a set of candidate entry IDs for the search from the indexes.
1285   * <p>
1286   * Here we are relying on ID order to ensure children are returned after their parents.
1287   * <ul>
1288   * <li>Iterate through the candidate IDs
1289   * <li>fetch entry by ID from cache or id2entry
1290   * <li>put the entry in the cache if not present
1291   * <li>discard entries that are not in scope
1292   * <li>return entry if it matches the filter
1293   * </ul>
1294   *
1295   * @param entryIDReorderedSet
1296   *          The candidate entry IDs.
1297   * @param candidatesAreInScope
1298   *          true if it is certain that every candidate entry is in the search scope.
1299   * @param searchOperation
1300   *          The search operation.
1301   * @param pageRequest
1302   *          A Paged Results control, or null if none.
1303   * @throws DirectoryException
1304   *           If an error prevented the search from being processed.
1305   */
1306  private void searchIndexed(ReadableTransaction txn, long[] entryIDReorderedSet, boolean candidatesAreInScope,
1307      SearchOperation searchOperation, PagedResultsControl pageRequest) throws DirectoryException,
1308      CanceledOperationException
1309  {
1310    SearchScope searchScope = searchOperation.getScope();
1311    DN aBaseDN = searchOperation.getBaseDN();
1312    boolean manageDsaIT = isManageDsaITOperation(searchOperation);
1313    boolean continueSearch = true;
1314
1315    // Set the starting value.
1316    Long beginEntryID = null;
1317    if (pageRequest != null && pageRequest.getCookie().length() != 0)
1318    {
1319      // The cookie contains the ID of the next entry to be returned.
1320      try
1321      {
1322        beginEntryID = pageRequest.getCookie().toLong();
1323      }
1324      catch (Exception e)
1325      {
1326        logger.traceException(e);
1327        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
1328            ERR_INVALID_PAGED_RESULTS_COOKIE.get(pageRequest.getCookie().toHexString()), e);
1329      }
1330    }
1331    else if (!manageDsaIT)
1332    {
1333      continueSearch = dn2uri.returnSearchReferences(txn, searchOperation);
1334    }
1335
1336    // Make sure the candidate list is smaller than the lookthrough limit
1337    int lookthroughLimit =
1338      searchOperation.getClientConnection().getLookthroughLimit();
1339    if (lookthroughLimit > 0 && entryIDReorderedSet.length > lookthroughLimit)
1340    {
1341      //Lookthrough limit exceeded
1342      searchOperation.setResultCode(ResultCode.ADMIN_LIMIT_EXCEEDED);
1343      searchOperation.appendErrorMessage(NOTE_LOOKTHROUGH_LIMIT_EXCEEDED.get(lookthroughLimit));
1344      continueSearch = false;
1345    }
1346
1347    // Iterate through the index candidates.
1348    if (continueSearch)
1349    {
1350      final SearchFilter filter = searchOperation.getFilter();
1351      for (int i = findStartIndex(beginEntryID, entryIDReorderedSet); i < entryIDReorderedSet.length; i++)
1352      {
1353        EntryID entryID = new EntryID(entryIDReorderedSet[i]);
1354        Entry entry;
1355        try
1356        {
1357          entry = getEntry(txn, entryID);
1358        }
1359        catch (Exception e)
1360        {
1361          logger.traceException(e);
1362          continue;
1363        }
1364
1365        // Process the candidate entry.
1366        if (entry != null
1367              && isInScope(candidatesAreInScope, searchScope, aBaseDN, entry)
1368              && (manageDsaIT || entry.getReferralURLs() == null)
1369              && filter.matchesEntry(entry))
1370          {
1371            if (pageRequest != null
1372                && searchOperation.getEntriesSent() == pageRequest.getSize())
1373            {
1374              // The current page is full.
1375              // Set the cookie to remember where we were.
1376              ByteString cookie = entryID.toByteString();
1377              Control control = new PagedResultsControl(pageRequest.isCritical(), 0, cookie);
1378              searchOperation.getResponseControls().add(control);
1379              return;
1380            }
1381
1382            if (!searchOperation.returnEntry(entry, null))
1383            {
1384              // We have been told to discontinue processing of the
1385              // search. This could be due to size limit exceeded or
1386              // operation cancelled.
1387              break;
1388            }
1389          }
1390      }
1391      searchOperation.checkIfCanceled(false);
1392    }
1393
1394    // Before we return success from the search we must ensure the base entry
1395    // exists. However, if we have returned at least one entry or subordinate
1396    // reference it implies the base does exist, so we can omit the check.
1397    if (searchOperation.getEntriesSent() == 0
1398        && searchOperation.getReferencesSent() == 0)
1399    {
1400      final Entry baseEntry = fetchBaseEntry(txn, aBaseDN, searchScope);
1401      if (!manageDsaIT)
1402      {
1403        dn2uri.checkTargetForReferral(baseEntry, searchScope);
1404      }
1405    }
1406
1407    if (pageRequest != null)
1408    {
1409      // Indicate no more pages.
1410      Control control = new PagedResultsControl(pageRequest.isCritical(), 0, null);
1411      searchOperation.getResponseControls().add(control);
1412    }
1413  }
1414
1415  private int findStartIndex(Long beginEntryID, long[] entryIDReorderedSet)
1416  {
1417    if (beginEntryID == null)
1418    {
1419      return 0;
1420    }
1421    final long begin = beginEntryID.longValue();
1422    for (int i = 0; i < entryIDReorderedSet.length; i++)
1423    {
1424      if (entryIDReorderedSet[i] == begin)
1425      {
1426        return i;
1427      }
1428    }
1429    return 0;
1430  }
1431
1432  private boolean isInScope(boolean candidatesAreInScope, SearchScope searchScope, DN aBaseDN, Entry entry)
1433  {
1434    DN entryDN = entry.getName();
1435
1436    if (candidatesAreInScope)
1437    {
1438      return true;
1439    }
1440    else if (searchScope == SearchScope.SINGLE_LEVEL)
1441    {
1442      // Check if this entry is an immediate child.
1443      if (entryDN.size() == aBaseDN.size() + 1
1444          && entryDN.isDescendantOf(aBaseDN))
1445      {
1446        return true;
1447      }
1448    }
1449    else if (searchScope == SearchScope.WHOLE_SUBTREE)
1450    {
1451      if (entryDN.isDescendantOf(aBaseDN))
1452      {
1453        return true;
1454      }
1455    }
1456    else if (searchScope == SearchScope.SUBORDINATES
1457        && entryDN.size() > aBaseDN.size()
1458        && entryDN.isDescendantOf(aBaseDN))
1459    {
1460      return true;
1461    }
1462    return false;
1463  }
1464
1465  /**
1466   * Adds the provided entry to this tree.  This method must ensure that the
1467   * entry is appropriate for the tree and that no entry already exists with
1468   * the same DN.  The caller must hold a write lock on the DN of the provided
1469   * entry.
1470   *
1471   * @param entry        The entry to add to this tree.
1472   * @param addOperation The add operation with which the new entry is
1473   *                     associated.  This may be <CODE>null</CODE> for adds
1474   *                     performed internally.
1475   * @throws DirectoryException If a problem occurs while trying to add the
1476   *                            entry.
1477   * @throws StorageRuntimeException If an error occurs in the storage.
1478   * @throws CanceledOperationException if this operation should be cancelled.
1479   */
1480  void addEntry(final Entry entry, final AddOperation addOperation)
1481  throws StorageRuntimeException, DirectoryException, CanceledOperationException
1482  {
1483    final DN parentDN = getParentWithinBase(entry.getName());
1484    final EntryID entryID = rootContainer.getNextEntryID();
1485
1486    // Insert into the indexes, in index configuration order.
1487    final IndexBuffer indexBuffer = new IndexBuffer();
1488    insertEntryIntoIndexes(indexBuffer, entry, entryID);
1489
1490    final ByteString encodedEntry = id2entry.encode(entry);
1491
1492    try
1493    {
1494      storage.write(new WriteOperation()
1495      {
1496        @Override
1497        public void run(WriteableTransaction txn) throws Exception
1498        {
1499          // No need to call indexBuffer.reset() since IndexBuffer content will be the same for each retry attempt.
1500          try
1501          {
1502            // Check whether the entry already exists.
1503            if (dn2id.get(txn, entry.getName()) != null)
1504            {
1505              throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS,
1506                  ERR_ADD_ENTRY_ALREADY_EXISTS.get(entry.getName()));
1507            }
1508            // Check that the parent entry exists.
1509            EntryID parentID = null;
1510            if (parentDN != null)
1511            {
1512              // Check for referral entries above the target.
1513              dn2uri.targetEntryReferrals(txn, entry.getName(), null);
1514
1515              parentID = dn2id.get(txn, parentDN);
1516              if (parentID == null)
1517              {
1518                throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
1519                                             ERR_ADD_NO_SUCH_OBJECT.get(entry.getName()),
1520                                             getMatchedDN(txn, parentDN),
1521                                             null);
1522              }
1523            }
1524
1525            // Ensure same access ordering as deleteEntry.
1526            dn2id.put(txn, entry.getName(), entryID);
1527            id2childrenCount.updateCount(txn, parentID, 1);
1528            id2entry.put(txn, entryID, encodedEntry);
1529            dn2uri.addEntry(txn, entry);
1530            id2childrenCount.updateTotalCount(txn, 1);
1531            indexBuffer.flush(txn);
1532            if (addOperation != null)
1533            {
1534              // One last check before committing
1535              addOperation.checkIfCanceled(true);
1536            }
1537          }
1538          catch (StorageRuntimeException | DirectoryException | CanceledOperationException e)
1539          {
1540            throw e;
1541          }
1542          catch (Exception e)
1543          {
1544            String msg = e.getMessage();
1545            if (msg == null)
1546            {
1547              msg = stackTraceToSingleLineString(e);
1548            }
1549            throw new DirectoryException(
1550                DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e);
1551          }
1552        }
1553      });
1554    }
1555    catch (Exception e)
1556    {
1557      writeTrustState(indexBuffer);
1558      throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class);
1559    }
1560
1561    final EntryCache<?> entryCache = DirectoryServer.getEntryCache();
1562    if (entryCache != null)
1563    {
1564      entryCache.putEntry(entry, backendID, entryID.longValue());
1565    }
1566  }
1567
1568  private void writeTrustState(final IndexBuffer indexBuffer)
1569  {
1570    // Transaction modifying the index has been rolled back.
1571    // Ensure that the index trusted state is persisted.
1572    try
1573    {
1574      storage.write(new WriteOperation()
1575      {
1576        @Override
1577        public void run(WriteableTransaction txn) throws Exception
1578        {
1579          indexBuffer.writeTrustState(txn);
1580        }
1581      });
1582    }
1583    catch (Exception e)
1584    {
1585      // Cannot throw because this method is used in a catch block and we do not want to hide the real exception.
1586      logger.traceException(e);
1587    }
1588  }
1589
1590  void importEntry(WriteableTransaction txn, EntryID entryID, Entry entry) throws DirectoryException,
1591      StorageRuntimeException
1592  {
1593    final IndexBuffer indexBuffer = IndexBuffer.newImportIndexBuffer(txn, entryID);
1594    insertEntryIntoIndexes(indexBuffer, entry, entryID);
1595    dn2id.put(txn, entry.getName(), entryID);
1596    id2entry.put(txn, entryID, id2entry.encode(entry));
1597    dn2uri.addEntry(txn, entry);
1598    indexBuffer.flush(txn);
1599  }
1600
1601  /**
1602   * Removes the specified entry from this tree.  This method must ensure
1603   * that the entry exists and that it does not have any subordinate entries
1604   * (unless the storage supports a subtree delete operation and the client
1605   * included the appropriate information in the request).  The caller must hold
1606   * a write lock on the provided entry DN.
1607   *
1608   * @param entryDN         The DN of the entry to remove from this tree.
1609   * @param deleteOperation The delete operation with which this action is
1610   *                        associated.  This may be <CODE>null</CODE> for
1611   *                        deletes performed internally.
1612   * @throws DirectoryException If a problem occurs while trying to remove the
1613   *                            entry.
1614   * @throws StorageRuntimeException If an error occurs in the storage.
1615   * @throws CanceledOperationException if this operation should be cancelled.
1616   */
1617  void deleteEntry(final DN entryDN, final DeleteOperation deleteOperation)
1618          throws DirectoryException, StorageRuntimeException, CanceledOperationException
1619  {
1620    final IndexBuffer indexBuffer = new IndexBuffer();
1621    try
1622    {
1623      storage.write(new WriteOperation()
1624      {
1625        @Override
1626        public void run(WriteableTransaction txn) throws Exception
1627        {
1628          indexBuffer.reset();
1629          try
1630          {
1631            // Check for referral entries above the target entry.
1632            dn2uri.targetEntryReferrals(txn, entryDN, null);
1633
1634            // We'll need the parent ID when we update the id2childrenCount. Fetch it now so that accesses to dn2id
1635            // are ordered.
1636            final DN parentDN = getParentWithinBase(entryDN);
1637            EntryID parentID = null;
1638            if (parentDN != null)
1639            {
1640              parentID = dn2id.get(txn, parentDN);
1641              if (parentID == null)
1642              {
1643                throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
1644                                             ERR_DELETE_NO_SUCH_OBJECT.get(entryDN),
1645                                             getMatchedDN(txn, parentDN),
1646                                             null);
1647              }
1648            }
1649
1650            // Delete the subordinate entries in dn2id if requested.
1651            final boolean isSubtreeDelete = deleteOperation != null
1652                    && deleteOperation.getRequestControl(SubtreeDeleteControl.DECODER) != null;
1653
1654            /* draft-armijo-ldap-treedelete, 4.1 Tree Delete Semantics: The server MUST NOT chase referrals stored in
1655             * the tree. If information about referrals is stored in this section of the tree, this pointer will be
1656             * deleted.
1657             */
1658            final boolean isManageDsaIT = isSubtreeDelete || isManageDsaITOperation(deleteOperation);
1659
1660            /* Ensure that all index updates are done in the correct order to avoid deadlocks. First iterate over
1661             * dn2id collecting all the IDs of the entries to be deleted. Then update dn2uri, id2entry,
1662             * id2childrenCount, and finally the attribute indexes.
1663             */
1664            final List<Long> entriesToBeDeleted = new ArrayList<>();
1665            try (final SequentialCursor<Void, EntryID> cursor = dn2id.openSubordinatesCursor(txn, entryDN))
1666            {
1667              // Delete the target entry in dn2id.
1668              if (!cursor.isDefined())
1669              {
1670                throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
1671                                             ERR_DELETE_NO_SUCH_OBJECT.get(entryDN),
1672                                             getMatchedDN(txn, entryDN),
1673                                             null);
1674              }
1675              entriesToBeDeleted.add(cursor.getValue().longValue());
1676              cursor.delete();
1677
1678              // Now delete the subordinate entries in dn2id.
1679              while (cursor.next())
1680              {
1681                if (!isSubtreeDelete)
1682                {
1683                  throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF,
1684                                               ERR_DELETE_NOT_ALLOWED_ON_NONLEAF.get(entryDN));
1685                }
1686                entriesToBeDeleted.add(cursor.getValue().longValue());
1687                cursor.delete();
1688                checkIfCanceled(false);
1689              }
1690            }
1691            // The target entry will have the lowest entryID so it will remain the first element.
1692            Collections.sort(entriesToBeDeleted);
1693
1694            // Now update id2entry, dn2uri, and id2childrenCount in key order.
1695            id2childrenCount.updateCount(txn, parentID, -1);
1696            final EntryCache<?> entryCache = DirectoryServer.getEntryCache();
1697            boolean isBaseEntry = true;
1698            try (final Cursor<EntryID, Entry> cursor = id2entry.openCursor(txn))
1699            {
1700              for (Long entryIDLong : entriesToBeDeleted)
1701              {
1702                final EntryID entryID = new EntryID(entryIDLong);
1703                if (!cursor.positionToKey(entryID.toByteString()))
1704                {
1705                  throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1706                                               ERR_MISSING_ID2ENTRY_RECORD.get(entryID));
1707                }
1708                final Entry entry = cursor.getValue();
1709                if (isBaseEntry && !isManageDsaIT)
1710                {
1711                  dn2uri.checkTargetForReferral(entry, null);
1712                }
1713                cursor.delete();
1714                dn2uri.deleteEntry(txn, entry);
1715                id2childrenCount.removeCount(txn, entryID);
1716                removeEntryFromIndexes(indexBuffer, entry, entryID);
1717                if (!isBaseEntry)
1718                {
1719                  invokeSubordinateDeletePlugins(entry);
1720                }
1721                if (entryCache != null)
1722                {
1723                  entryCache.removeEntry(entry.getName());
1724                }
1725                isBaseEntry = false;
1726                checkIfCanceled(false);
1727              }
1728            }
1729            id2childrenCount.updateTotalCount(txn, -entriesToBeDeleted.size());
1730            indexBuffer.flush(txn);
1731            checkIfCanceled(true);
1732            if (isSubtreeDelete)
1733            {
1734              deleteOperation.addAdditionalLogItem(unquotedKeyValue(getClass(), "deletedEntries",
1735                                                                    entriesToBeDeleted.size()));
1736            }
1737          }
1738          catch (StorageRuntimeException | DirectoryException | CanceledOperationException e)
1739          {
1740            throw e;
1741          }
1742          catch (Exception e)
1743          {
1744            String msg = e.getMessage();
1745            if (msg == null)
1746            {
1747              msg = stackTraceToSingleLineString(e);
1748            }
1749            throw new DirectoryException(
1750                DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e);
1751          }
1752        }
1753
1754        private void invokeSubordinateDeletePlugins(final Entry entry) throws DirectoryException
1755        {
1756          if (deleteOperation != null && !deleteOperation.isSynchronizationOperation())
1757          {
1758            SubordinateDelete pluginResult =
1759                    getPluginConfigManager().invokeSubordinateDeletePlugins(deleteOperation, entry);
1760            if (!pluginResult.continueProcessing())
1761            {
1762              throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1763                                           ERR_DELETE_ABORTED_BY_SUBORDINATE_PLUGIN.get(entry.getName()));
1764            }
1765          }
1766        }
1767
1768        private void checkIfCanceled(boolean signalTooLate) throws CanceledOperationException
1769        {
1770          if (deleteOperation != null)
1771          {
1772            deleteOperation.checkIfCanceled(signalTooLate);
1773          }
1774        }
1775      });
1776    }
1777    catch (Exception e)
1778    {
1779      writeTrustState(indexBuffer);
1780      throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class);
1781    }
1782  }
1783
1784  /**
1785   * Indicates whether an entry with the specified DN exists.
1786   *
1787   * @param  entryDN  The DN of the entry for which to determine existence.
1788   *
1789   * @return  <CODE>true</CODE> if the specified entry exists,
1790   *          or <CODE>false</CODE> if it does not.
1791   */
1792  private boolean entryExists(ReadableTransaction txn, final DN entryDN)
1793  {
1794    // Try the entry cache first.
1795    EntryCache<?> entryCache = DirectoryServer.getEntryCache();
1796    return (entryCache != null && entryCache.containsEntry(entryDN))
1797            || dn2id.get(txn, entryDN) != null;
1798  }
1799
1800
1801  boolean entryExists(final DN entryDN) throws StorageRuntimeException
1802  {
1803    final EntryCache<?> entryCache = DirectoryServer.getEntryCache();
1804    if (entryCache != null && entryCache.containsEntry(entryDN))
1805    {
1806      return true;
1807    }
1808
1809    try
1810    {
1811      return storage.read(new ReadOperation<Boolean>()
1812      {
1813        @Override
1814        public Boolean run(ReadableTransaction txn) throws Exception
1815        {
1816          return dn2id.get(txn, entryDN) != null;
1817        }
1818      });
1819    }
1820    catch (Exception e)
1821    {
1822      throw new StorageRuntimeException(e);
1823    }
1824  }
1825
1826  /**
1827   * Fetch an entry by DN, trying the entry cache first, then the tree.
1828   * Retrieves the requested entry, trying the entry cache first,
1829   * then the tree.
1830   *
1831   * @param entryDN The distinguished name of the entry to retrieve.
1832   * @return The requested entry, or <CODE>null</CODE> if the entry does not
1833   *         exist.
1834   * @throws DirectoryException If a problem occurs while trying to retrieve
1835   *                            the entry.
1836   * @throws StorageRuntimeException An error occurred during a storage operation.
1837   */
1838  Entry getEntry(final DN entryDN) throws StorageRuntimeException, DirectoryException
1839  {
1840    try
1841    {
1842      return storage.read(new ReadOperation<Entry>()
1843      {
1844        @Override
1845        public Entry run(ReadableTransaction txn) throws Exception
1846        {
1847          Entry entry = getEntry0(txn, entryDN);
1848          if (entry == null)
1849          {
1850            // The entryDN does not exist. Check for referral entries above the target entry.
1851            dn2uri.targetEntryReferrals(txn, entryDN, null);
1852          }
1853          return entry;
1854        }
1855      });
1856    }
1857    catch (Exception e)
1858    {
1859      // it is not very clean to specify twice the same exception but it saves me some code for now
1860      throwAllowedExceptionTypes(e, DirectoryException.class, DirectoryException.class);
1861      return null; // it can never happen
1862    }
1863  }
1864
1865  private Entry getEntry0(ReadableTransaction txn, final DN entryDN) throws StorageRuntimeException, DirectoryException
1866  {
1867    final EntryCache<?> entryCache = DirectoryServer.getEntryCache();
1868    if (entryCache != null)
1869    {
1870      final Entry entry = entryCache.getEntry(entryDN);
1871      if (entry != null)
1872      {
1873        return entry;
1874      }
1875    }
1876
1877    final EntryID entryID = dn2id.get(txn, entryDN);
1878    if (entryID == null)
1879    {
1880      return null;
1881    }
1882
1883    final Entry entry = id2entry.get(txn, entryID);
1884    if (entry != null && entryCache != null)
1885    {
1886      /*
1887       * Put the entry in the cache making sure not to overwrite a newer copy that may have been
1888       * inserted since the time we read the cache.
1889       */
1890      entryCache.putEntryIfAbsent(entry, backendID, entryID.longValue());
1891    }
1892    return entry;
1893  }
1894
1895  /**
1896   * The simplest case of replacing an entry in which the entry DN has
1897   * not changed.
1898   *
1899   * @param oldEntry           The old contents of the entry
1900   * @param newEntry           The new contents of the entry
1901   * @param modifyOperation The modify operation with which this action is
1902   *                        associated.  This may be <CODE>null</CODE> for
1903   *                        modifications performed internally.
1904   * @throws StorageRuntimeException If an error occurs in the storage.
1905   * @throws DirectoryException If a Directory Server error occurs.
1906   * @throws CanceledOperationException if this operation should be cancelled.
1907   */
1908  void replaceEntry(final Entry oldEntry, final Entry newEntry, final ModifyOperation modifyOperation)
1909      throws StorageRuntimeException, DirectoryException, CanceledOperationException
1910  {
1911    final IndexBuffer indexBuffer = new IndexBuffer();
1912    final ByteString encodedNewEntry = id2entry.encode(newEntry);
1913    try
1914    {
1915      storage.write(new WriteOperation()
1916      {
1917        @Override
1918        public void run(WriteableTransaction txn) throws Exception
1919        {
1920          indexBuffer.reset();
1921          try
1922          {
1923            EntryID entryID = dn2id.get(txn, newEntry.getName());
1924            if (entryID == null)
1925            {
1926              throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
1927                                           ERR_MODIFY_NO_SUCH_OBJECT.get(newEntry.getName()),
1928                                           getMatchedDN(txn, newEntry.getName()),
1929                                           null);
1930            }
1931
1932            if (!isManageDsaITOperation(modifyOperation))
1933            {
1934              // Check if the entry is a referral entry.
1935              dn2uri.checkTargetForReferral(oldEntry, null);
1936            }
1937
1938            // Ensure same ordering as deleteEntry: id2entry, dn2uri, then indexes.
1939            id2entry.put(txn, entryID, encodedNewEntry);
1940
1941            // Update the referral tree.
1942            if (modifyOperation != null)
1943            {
1944              // In this case we know from the operation what the modifications were.
1945              List<Modification> mods = modifyOperation.getModifications();
1946              dn2uri.modifyEntry(txn, oldEntry, newEntry, mods);
1947            }
1948            else
1949            {
1950              dn2uri.replaceEntry(txn, oldEntry, newEntry);
1951            }
1952
1953            // Update the indexes.
1954            if (modifyOperation != null)
1955            {
1956              // In this case we know from the operation what the modifications were.
1957              List<Modification> mods = modifyOperation.getModifications();
1958              indexModifications(indexBuffer, oldEntry, newEntry, entryID, mods);
1959            }
1960            else
1961            {
1962              // The most optimal would be to figure out what the modifications were.
1963              removeEntryFromIndexes(indexBuffer, oldEntry, entryID);
1964              insertEntryIntoIndexes(indexBuffer, newEntry, entryID);
1965            }
1966
1967            indexBuffer.flush(txn);
1968
1969            if(modifyOperation != null)
1970            {
1971              // One last check before committing
1972              modifyOperation.checkIfCanceled(true);
1973            }
1974
1975            // Update the entry cache.
1976            EntryCache<?> entryCache = DirectoryServer.getEntryCache();
1977            if (entryCache != null)
1978            {
1979              entryCache.putEntry(newEntry, backendID, entryID.longValue());
1980            }
1981          }
1982          catch (StorageRuntimeException | DirectoryException | CanceledOperationException e)
1983          {
1984            throw e;
1985          }
1986          catch (Exception e)
1987          {
1988            String msg = e.getMessage();
1989            if (msg == null)
1990            {
1991              msg = stackTraceToSingleLineString(e);
1992            }
1993            throw new DirectoryException(
1994                DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e);
1995          }
1996        }
1997      });
1998    }
1999    catch (Exception e)
2000    {
2001      writeTrustState(indexBuffer);
2002      throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class);
2003    }
2004  }
2005
2006  /**
2007   * Moves and/or renames the provided entry in this backend, altering any
2008   * subordinate entries as necessary.  This must ensure that an entry already
2009   * exists with the provided current DN, and that no entry exists with the
2010   * target DN of the provided entry.  The caller must hold write locks on both
2011   * the current DN and the new DN for the entry.
2012   *
2013   * @param oldTargetDN             The current DN of the entry to be renamed.
2014   * @param newTargetEntry          The new content to use for the entry.
2015   * @param modifyDNOperation The modify DN operation with which this action
2016   *                          is associated.  This may be <CODE>null</CODE>
2017   *                          for modify DN operations performed internally.
2018   * @throws DirectoryException
2019   *          If a problem occurs while trying to perform the rename.
2020   * @throws CanceledOperationException
2021   *          If this backend noticed and reacted
2022   *          to a request to cancel or abandon the
2023   *          modify DN operation.
2024   * @throws StorageRuntimeException If an error occurs in the storage.
2025   */
2026  void renameEntry(final DN oldTargetDN, final Entry newTargetEntry, final ModifyDNOperation modifyDNOperation)
2027      throws StorageRuntimeException, DirectoryException, CanceledOperationException
2028  {
2029    final IndexBuffer indexBuffer = new IndexBuffer();
2030    try
2031    {
2032      storage.write(new WriteOperation()
2033      {
2034        @Override
2035        public void run(WriteableTransaction txn) throws Exception
2036        {
2037          indexBuffer.reset();
2038          try
2039          {
2040            // Validate the request.
2041            final DN newTargetDN = newTargetEntry.getName();
2042            final DN oldSuperiorDN = getParentWithinBase(oldTargetDN);
2043            final DN newSuperiorDN = getParentWithinBase(newTargetDN);
2044
2045            final EntryID oldSuperiorID = oldSuperiorDN != null ? dn2id.get(txn, oldSuperiorDN) : null;
2046            final EntryID oldTargetID = dn2id.get(txn, oldTargetDN);
2047            if ((oldSuperiorDN != null && oldSuperiorID == null) || oldTargetID == null)
2048            {
2049              // Check for referral entries above the target entry.
2050              dn2uri.targetEntryReferrals(txn, oldTargetDN, null);
2051              throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
2052                                           ERR_MODIFYDN_NO_SUCH_OBJECT.get(oldTargetDN),
2053                                           getMatchedDN(txn, oldTargetDN),
2054                                           null);
2055            }
2056
2057            final EntryID newSuperiorID = newSuperiorDN != null ? dn2id.get(txn, newSuperiorDN) : null;
2058            if (newSuperiorDN != null && newSuperiorID == null)
2059            {
2060              throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
2061                                           ERR_NEW_SUPERIOR_NO_SUCH_OBJECT.get(newSuperiorDN),
2062                                           getMatchedDN(txn, newSuperiorDN),
2063                                           null);
2064            }
2065
2066            // Check that an entry with the new name does not already exist, but take care to handle the case where
2067            // the user is renaming the entry with an equivalent name, e.g. "cn=matt" to "cn=Matt".
2068            if (!oldTargetDN.equals(newTargetDN) && dn2id.get(txn, newTargetDN) != null)
2069            {
2070              throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS,
2071                                           ERR_MODIFYDN_ALREADY_EXISTS.get(newTargetDN));
2072            }
2073
2074            /* We want to preserve the invariant that the ID of an entry is greater than its parent, since search
2075             * results are returned in ID order. Note: if the superior has changed then oldSuperiorDN and
2076             * newSuperiorDN will be non-null.
2077             */
2078            final boolean superiorHasChanged = !Objects.equals(oldSuperiorDN, newSuperiorDN);
2079            final boolean renumberEntryIDs = superiorHasChanged && newSuperiorID.compareTo(oldSuperiorID) > 0;
2080
2081            /* Ensure that all index updates are done in the correct order to avoid deadlocks. First iterate over
2082             * dn2id collecting all the IDs of the entries to be renamed. Then update dn2uri, id2entry,
2083             * id2childrenCount, and finally the attribute indexes.
2084             */
2085            final List<Pair<Long, Long>> renamedEntryIDs = dn2id.renameSubtree(txn,
2086                                                                               oldTargetDN,
2087                                                                               newTargetDN,
2088                                                                               rootContainer,
2089                                                                               renumberEntryIDs,
2090                                                                               modifyDNOperation);
2091
2092            // The target entry will have the lowest entryID so it will remain the first element.
2093            Collections.sort(renamedEntryIDs, Pair.<Long, Long>getPairComparator());
2094
2095            // Now update id2entry, dn2uri, and id2childrenCount in key order.
2096            if (superiorHasChanged)
2097            {
2098              id2childrenCount.updateCount(txn, oldSuperiorID, -1);
2099              id2childrenCount.updateCount(txn, newSuperiorID, 1);
2100            }
2101            boolean isBaseEntry = true;
2102            try (final Cursor<EntryID, Entry> cursor = id2entry.openCursor(txn))
2103            {
2104              for (Pair<Long, Long> renamedEntryID : renamedEntryIDs)
2105              {
2106                renameSingleEntry(txn, renamedEntryID, cursor, indexBuffer, newTargetDN, renumberEntryIDs, isBaseEntry);
2107                isBaseEntry = false;
2108                checkIfCanceled(false);
2109              }
2110
2111            }
2112            indexBuffer.flush(txn);
2113            checkIfCanceled(true);
2114          }
2115          catch (StorageRuntimeException | DirectoryException | CanceledOperationException e)
2116          {
2117            throw e;
2118          }
2119          catch (Exception e)
2120          {
2121            String msg = e.getMessage();
2122            if (msg == null)
2123            {
2124              msg = stackTraceToSingleLineString(e);
2125            }
2126            throw new DirectoryException(
2127                DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e);
2128          }
2129        }
2130
2131        private void renameSingleEntry(
2132                final WriteableTransaction txn,
2133                final Pair<Long, Long> renamedEntryID,
2134                final Cursor<EntryID, Entry> cursor,
2135                final IndexBuffer indexBuffer,
2136                final DN newTargetDN,
2137                final boolean renumberEntryIDs,
2138                final boolean isBaseEntry) throws DirectoryException
2139        {
2140          final EntryID oldEntryID = new EntryID(renamedEntryID.getFirst());
2141          final EntryID newEntryID = new EntryID(renamedEntryID.getSecond());
2142          if (!cursor.positionToKey(oldEntryID.toByteString()))
2143          {
2144            throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
2145                                         ERR_MISSING_ID2ENTRY_RECORD.get(oldEntryID));
2146          }
2147
2148          final Entry oldEntry = cursor.getValue();
2149          final Entry newEntry;
2150          final List<Modification> modifications;
2151          if (isBaseEntry)
2152          {
2153            if (!isManageDsaITOperation(modifyDNOperation))
2154            {
2155              dn2uri.checkTargetForReferral(oldEntry, null);
2156            }
2157            newEntry = newTargetEntry;
2158            modifications = modifyDNOperation != null ? modifyDNOperation.getModifications() : null;
2159          }
2160          else
2161          {
2162            final DN newDN = modDN(oldEntry.getName(), oldTargetDN.size(), newTargetDN);
2163            newEntry = oldEntry.duplicate(false);
2164            newEntry.setDN(newDN);
2165            modifications = invokeSubordinateModifyDNPlugins(oldEntry, newEntry);
2166          }
2167
2168          if (renumberEntryIDs)
2169          {
2170            cursor.delete();
2171          }
2172          id2entry.put(txn, newEntryID, newEntry);
2173          dn2uri.deleteEntry(txn, oldEntry);
2174          dn2uri.addEntry(txn, newEntry);
2175          if (renumberEntryIDs)
2176          {
2177            // In-order: new entryID is guaranteed to be greater than old entryID.
2178            final long count = id2childrenCount.removeCount(txn, oldEntryID);
2179            id2childrenCount.updateCount(txn, newEntryID, count);
2180          }
2181
2182          if (renumberEntryIDs || modifications == null)
2183          {
2184            // Slow path: the entry has been renumbered so we need to fully re-index.
2185            removeEntryFromIndexes(indexBuffer, oldEntry, oldEntryID);
2186            insertEntryIntoIndexes(indexBuffer, newEntry, newEntryID);
2187          }
2188          else if (!modifications.isEmpty())
2189          {
2190            // Fast-path: the entryID has not changed so we only need to re-index the mods.
2191            indexModifications(indexBuffer, oldEntry, newEntry, oldEntryID, modifications);
2192          }
2193
2194          final EntryCache<?> entryCache = DirectoryServer.getEntryCache();
2195          if (entryCache != null)
2196          {
2197            entryCache.removeEntry(oldEntry.getName());
2198          }
2199        }
2200
2201        private void checkIfCanceled(boolean signalTooLate) throws CanceledOperationException
2202        {
2203          if (modifyDNOperation != null)
2204          {
2205            modifyDNOperation.checkIfCanceled(signalTooLate);
2206          }
2207        }
2208
2209        private List<Modification> invokeSubordinateModifyDNPlugins(
2210                final Entry oldEntry, final Entry newEntry) throws DirectoryException
2211        {
2212          final List<Modification> modifications = Collections.unmodifiableList(new ArrayList<Modification>(0));
2213
2214          // Create a new entry that is a copy of the old entry but with the new DN.
2215          // Also invoke any subordinate modify DN plugins on the entry.
2216          // FIXME -- At the present time, we don't support subordinate modify DN
2217          //          plugins that make changes to subordinate entries and therefore
2218          //          provide an unmodifiable list for the modifications element.
2219          // FIXME -- This will need to be updated appropriately if we decided that
2220          //          these plugins should be invoked for synchronization operations.
2221          if (modifyDNOperation != null && !modifyDNOperation.isSynchronizationOperation())
2222          {
2223            SubordinateModifyDN pluginResult = getPluginConfigManager().invokeSubordinateModifyDNPlugins(
2224                    modifyDNOperation, oldEntry, newEntry, modifications);
2225
2226            if (!pluginResult.continueProcessing())
2227            {
2228              throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
2229                                           ERR_MODIFYDN_ABORTED_BY_SUBORDINATE_PLUGIN.get(oldEntry.getName(),
2230                                                                                          newEntry.getName()));
2231            }
2232
2233            if (!modifications.isEmpty())
2234            {
2235              LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
2236              if (!newEntry.conformsToSchema(null, false, false, false, invalidReason))
2237              {
2238                throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
2239                                             ERR_MODIFYDN_ABORTED_BY_SUBORDINATE_SCHEMA_ERROR.get(oldEntry.getName(),
2240                                                                                                  newEntry.getName(),
2241                                                                                                  invalidReason));
2242              }
2243            }
2244          }
2245          return modifications;
2246        }
2247      });
2248    }
2249    catch (Exception e)
2250    {
2251      writeTrustState(indexBuffer);
2252      throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class);
2253    }
2254  }
2255
2256  /**
2257   * Make a new DN for a subordinate entry of a renamed or moved entry.
2258   *
2259   * @param oldDN The current DN of the subordinate entry.
2260   * @param oldSuffixLen The current DN length of the renamed or moved entry.
2261   * @param newSuffixDN The new DN of the renamed or moved entry.
2262   * @return The new DN of the subordinate entry.
2263   */
2264  static DN modDN(DN oldDN, int oldSuffixLen, DN newSuffixDN)
2265  {
2266    int oldDNNumComponents    = oldDN.size();
2267    int oldDNKeepComponents   = oldDNNumComponents - oldSuffixLen;
2268    int newSuffixDNComponents = newSuffixDN.size();
2269
2270    RDN[] newDNComponents = new RDN[oldDNKeepComponents+newSuffixDNComponents];
2271    for (int i=0; i < oldDNKeepComponents; i++)
2272    {
2273      newDNComponents[i] = oldDN.getRDN(i);
2274    }
2275
2276    for (int i=oldDNKeepComponents, j=0; j < newSuffixDNComponents; i++,j++)
2277    {
2278      newDNComponents[i] = newSuffixDN.getRDN(j);
2279    }
2280
2281    return new DN(newDNComponents);
2282  }
2283
2284  /**
2285   * Insert a new entry into the attribute indexes.
2286   *
2287   * @param buffer The index buffer used to buffer up the index changes.
2288   * @param entry The entry to be inserted into the indexes.
2289   * @param entryID The ID of the entry to be inserted into the indexes.
2290   * @throws StorageRuntimeException If an error occurs in the storage.
2291   * @throws DirectoryException If a Directory Server error occurs.
2292   */
2293  private void insertEntryIntoIndexes(IndexBuffer buffer, Entry entry, EntryID entryID)
2294      throws StorageRuntimeException, DirectoryException
2295  {
2296    for (AttributeIndex index : attrIndexMap.values())
2297    {
2298      index.addEntry(buffer, entryID, entry);
2299    }
2300
2301    for (VLVIndex vlvIndex : vlvIndexMap.values())
2302    {
2303      vlvIndex.addEntry(buffer, entryID, entry);
2304    }
2305  }
2306
2307  /**
2308   * Remove an entry from the attribute indexes.
2309   *
2310   * @param buffer The index buffer used to buffer up the index changes.
2311   * @param entry The entry to be removed from the indexes.
2312   * @param entryID The ID of the entry to be removed from the indexes.
2313   * @throws StorageRuntimeException If an error occurs in the storage.
2314   * @throws DirectoryException If a Directory Server error occurs.
2315   */
2316  private void removeEntryFromIndexes(IndexBuffer buffer, Entry entry, EntryID entryID)
2317      throws StorageRuntimeException, DirectoryException
2318  {
2319    for (AttributeIndex index : attrIndexMap.values())
2320    {
2321      index.removeEntry(buffer, entryID, entry);
2322    }
2323
2324    for (VLVIndex vlvIndex : vlvIndexMap.values())
2325    {
2326      vlvIndex.removeEntry(buffer, entryID, entry);
2327    }
2328  }
2329
2330  /**
2331   * Update the attribute indexes to reflect the changes to the
2332   * attributes of an entry resulting from a sequence of modifications.
2333   *
2334   * @param buffer The index buffer used to buffer up the index changes.
2335   * @param oldEntry The contents of the entry before the change.
2336   * @param newEntry The contents of the entry after the change.
2337   * @param entryID The ID of the entry that was changed.
2338   * @param mods The sequence of modifications made to the entry.
2339   * @throws StorageRuntimeException If an error occurs in the storage.
2340   * @throws DirectoryException If a Directory Server error occurs.
2341   */
2342  private void indexModifications(IndexBuffer buffer, Entry oldEntry, Entry newEntry,
2343      EntryID entryID, List<Modification> mods)
2344  throws StorageRuntimeException, DirectoryException
2345  {
2346    // Process in index configuration order.
2347    for (AttributeIndex index : attrIndexMap.values())
2348    {
2349      // Check whether any modifications apply to this indexed attribute.
2350      if (isAttributeModified(index, mods))
2351      {
2352        index.modifyEntry(buffer, entryID, oldEntry, newEntry);
2353      }
2354    }
2355
2356    for(VLVIndex vlvIndex : vlvIndexMap.values())
2357    {
2358      vlvIndex.modifyEntry(buffer, entryID, oldEntry, newEntry, mods);
2359    }
2360  }
2361
2362  /**
2363   * Get a count of the number of entries stored in this entry container including the baseDN
2364   *
2365   * @return The number of entries stored in this entry container including the baseDN.
2366   * @throws StorageRuntimeException
2367   *           If an error occurs in the storage.
2368   */
2369  long getNumberOfEntriesInBaseDN() throws StorageRuntimeException
2370  {
2371    try
2372    {
2373      return storage.read(new ReadOperation<Long>()
2374      {
2375        @Override
2376        public Long run(ReadableTransaction txn) throws Exception
2377        {
2378          return getNumberOfEntriesInBaseDN0(txn);
2379        }
2380      });
2381    }
2382    catch (Exception e)
2383    {
2384      throw new StorageRuntimeException(e);
2385    }
2386  }
2387
2388  long getNumberOfEntriesInBaseDN0(ReadableTransaction txn)
2389  {
2390    return id2childrenCount.getTotalCount(txn);
2391  }
2392
2393  /**
2394   * Determine whether the provided operation has the ManageDsaIT request control.
2395   * @param operation The operation for which the determination is to be made.
2396   * @return true if the operation has the ManageDsaIT request control, or false if not.
2397   */
2398  private static boolean isManageDsaITOperation(Operation operation)
2399  {
2400    if(operation != null)
2401    {
2402      List<Control> controls = operation.getRequestControls();
2403      if (controls != null)
2404      {
2405        for (Control control : controls)
2406        {
2407          if (ServerConstants.OID_MANAGE_DSAIT_CONTROL.equals(control.getOID()))
2408          {
2409            return true;
2410          }
2411        }
2412      }
2413    }
2414    return false;
2415  }
2416
2417  /**
2418   * Delete this entry container from disk. The entry container should be
2419   * closed before calling this method.
2420   *
2421   * @param txn a non null transaction
2422   * @throws StorageRuntimeException If an error occurs while removing the entry container.
2423   */
2424  void delete(WriteableTransaction txn) throws StorageRuntimeException
2425  {
2426    for (Tree tree : listTrees())
2427    {
2428      tree.delete(txn);
2429    }
2430  }
2431
2432  /**
2433   * Remove a tree from disk.
2434   *
2435   * @param txn a non null transaction
2436   * @param tree The tree container to remove.
2437   * @throws StorageRuntimeException If an error occurs while attempting to delete the tree.
2438   */
2439  void deleteTree(WriteableTransaction txn, Tree tree) throws StorageRuntimeException
2440  {
2441    if(tree == state)
2442    {
2443      // The state tree cannot be removed individually.
2444      return;
2445    }
2446
2447    tree.delete(txn);
2448    if(tree instanceof Index)
2449    {
2450      state.deleteRecord(txn, tree.getName());
2451    }
2452  }
2453
2454  /**
2455   * This method constructs a container name from a base DN. Only alphanumeric
2456   * characters are preserved, all other characters are replaced with an
2457   * underscore.
2458   *
2459   * @return The container name for the base DN.
2460   */
2461  String getTreePrefix()
2462  {
2463    return treePrefix;
2464  }
2465
2466  @Override
2467  public DN getBaseDN()
2468  {
2469    return baseDN;
2470  }
2471
2472  /**
2473   * Get the parent of a DN in the scope of the base DN.
2474   *
2475   * @param dn A DN which is in the scope of the base DN.
2476   * @return The parent DN, or null if the given DN is the base DN.
2477   */
2478  DN getParentWithinBase(DN dn)
2479  {
2480    if (dn.equals(baseDN))
2481    {
2482      return null;
2483    }
2484    return dn.parent();
2485  }
2486
2487  @Override
2488  public boolean isConfigurationChangeAcceptable(
2489      PluggableBackendCfg cfg, List<LocalizableMessage> unacceptableReasons)
2490  {
2491    // This is always true because only all config attributes used
2492    // by the entry container should be validated by the admin framework.
2493    return true;
2494  }
2495
2496  @Override
2497  public ConfigChangeResult applyConfigurationChange(final PluggableBackendCfg cfg)
2498  {
2499    final ConfigChangeResult ccr = new ConfigChangeResult();
2500
2501    exclusiveLock.lock();
2502    try
2503    {
2504      storage.write(new WriteOperation()
2505      {
2506        @Override
2507        public void run(WriteableTransaction txn) throws Exception
2508        {
2509          DataConfig entryDataConfig = new DataConfig(cfg.isEntriesCompressed(),
2510              cfg.isCompactEncoding(), rootContainer.getCompressedSchema());
2511          id2entry.setDataConfig(entryDataConfig);
2512
2513          EntryContainer.this.config = cfg;
2514        }
2515      });
2516    }
2517    catch (Exception e)
2518    {
2519      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
2520      ccr.addMessage(LocalizableMessage.raw(stackTraceToSingleLineString(e)));
2521    }
2522    finally
2523    {
2524      exclusiveLock.unlock();
2525    }
2526
2527    return ccr;
2528  }
2529
2530  /**
2531   * Clear the contents of this entry container.
2532   *
2533   * @throws StorageRuntimeException If an error occurs while removing the entry
2534   *                           container.
2535   */
2536  public void clear() throws StorageRuntimeException
2537  {
2538    try
2539    {
2540      storage.write(new WriteOperation()
2541      {
2542        @Override
2543        public void run(WriteableTransaction txn) throws Exception
2544        {
2545          for (Tree tree : listTrees())
2546          {
2547            tree.delete(txn);
2548          }
2549        }
2550      });
2551    }
2552    catch (Exception e)
2553    {
2554      throw new StorageRuntimeException(e);
2555    }
2556  }
2557
2558  List<Tree> listTrees()
2559  {
2560    final List<Tree> allTrees = new ArrayList<>();
2561    allTrees.add(dn2id);
2562    allTrees.add(id2entry);
2563    allTrees.add(dn2uri);
2564    allTrees.add(id2childrenCount);
2565    allTrees.add(state);
2566
2567    for (AttributeIndex index : attrIndexMap.values())
2568    {
2569      allTrees.addAll(index.getNameToIndexes().values());
2570    }
2571
2572    allTrees.addAll(vlvIndexMap.values());
2573    return allTrees;
2574  }
2575
2576  /**
2577   * Finds an existing entry whose DN is the closest ancestor of a given baseDN.
2578   *
2579   * @param targetDN  the DN for which we are searching a matched DN.
2580   * @return the DN of the closest ancestor of the baseDN.
2581   * @throws DirectoryException If an error prevented the check of an
2582   * existing entry from being performed.
2583   */
2584  private DN getMatchedDN(ReadableTransaction txn, DN targetDN) throws DirectoryException
2585  {
2586    DN parentDN  = targetDN.getParentDNInSuffix();
2587    while (parentDN != null && parentDN.isDescendantOf(baseDN))
2588    {
2589      if (entryExists(txn, parentDN))
2590      {
2591        return parentDN;
2592      }
2593      parentDN = parentDN.getParentDNInSuffix();
2594    }
2595    return null;
2596  }
2597
2598  /**
2599   * Checks if any modifications apply to this indexed attribute.
2600   * @param index the indexed attributes.
2601   * @param mods the modifications to check for.
2602   * @return true if any apply, false otherwise.
2603   */
2604  private static boolean isAttributeModified(AttributeIndex index, List<Modification> mods)
2605  {
2606    AttributeType indexAttributeType = index.getAttributeType();
2607    List<AttributeType> subTypes =
2608            DirectoryServer.getSchema().getSubTypes(indexAttributeType);
2609
2610    for (Modification mod : mods)
2611    {
2612      Attribute modAttr = mod.getAttribute();
2613      AttributeType modAttrType = modAttr.getAttributeType();
2614      if (modAttrType.equals(indexAttributeType)
2615          || subTypes.contains(modAttrType))
2616      {
2617        return true;
2618      }
2619    }
2620    return false;
2621  }
2622
2623  /**
2624   * Fetch the base Entry of the EntryContainer.
2625   * @param searchBaseDN the DN for the base entry
2626   * @param searchScope the scope under which this is fetched.
2627   *                    Scope is used for referral processing.
2628   * @return the Entry matching the baseDN.
2629   * @throws DirectoryException if the baseDN doesn't exist.
2630   */
2631  private Entry fetchBaseEntry(ReadableTransaction txn, DN searchBaseDN, SearchScope searchScope)
2632      throws DirectoryException
2633  {
2634    Entry baseEntry = getEntry0(txn, searchBaseDN);
2635    if (baseEntry == null)
2636    {
2637      // Check for referral entries above the base entry.
2638      dn2uri.targetEntryReferrals(txn, searchBaseDN, searchScope);
2639      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
2640          ERR_SEARCH_NO_SUCH_OBJECT.get(searchBaseDN), getMatchedDN(txn, searchBaseDN), null);
2641    }
2642    return baseEntry;
2643  }
2644
2645  private long[] sort(ReadableTransaction txn, EntryIDSet entryIDSet, SearchOperation searchOperation,
2646      SortOrder sortOrder, VLVRequestControl vlvRequest) throws DirectoryException
2647  {
2648    if (!entryIDSet.isDefined())
2649    {
2650      return null;
2651    }
2652
2653    final DN baseDN = searchOperation.getBaseDN();
2654    final SearchScope scope = searchOperation.getScope();
2655    final SearchFilter filter = searchOperation.getFilter();
2656
2657    final TreeMap<ByteString, EntryID> sortMap = new TreeMap<>();
2658    for (EntryID id : entryIDSet)
2659    {
2660      try
2661      {
2662        Entry e = getEntry(txn, id);
2663        if (e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e))
2664        {
2665          sortMap.put(encodeVLVKey(sortOrder, e, id.longValue()), id);
2666        }
2667      }
2668      catch (Exception e)
2669      {
2670        LocalizableMessage message = ERR_ENTRYIDSORTER_CANNOT_EXAMINE_ENTRY.get(id, getExceptionMessage(e));
2671        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
2672      }
2673    }
2674
2675    // See if there is a VLV request to further pare down the set of results, and if there is where it should be
2676    // processed by offset or assertion value.
2677    if (vlvRequest == null)
2678    {
2679      return toArray(sortMap.values());
2680    }
2681
2682    if (vlvRequest.getTargetType() == VLVRequestControl.TYPE_TARGET_BYOFFSET)
2683    {
2684      return sortByOffset(searchOperation, vlvRequest, sortMap);
2685    }
2686    return sortByGreaterThanOrEqualAssertion(searchOperation, vlvRequest, sortOrder, sortMap);
2687  }
2688
2689  private static final long[] toArray(Collection<EntryID> entryIDs)
2690  {
2691    final long[] array = new long[entryIDs.size()];
2692    int i = 0;
2693    for (EntryID entryID : entryIDs)
2694    {
2695      array[i++] = entryID.longValue();
2696    }
2697    return array;
2698  }
2699
2700  private static final long[] sortByGreaterThanOrEqualAssertion(SearchOperation searchOperation,
2701      VLVRequestControl vlvRequest, SortOrder sortOrder, final TreeMap<ByteString, EntryID> sortMap)
2702      throws DirectoryException
2703  {
2704    ByteString assertionValue = vlvRequest.getGreaterThanOrEqualAssertion();
2705    ByteSequence encodedTargetAssertion =
2706        encodeTargetAssertion(sortOrder, assertionValue, searchOperation, sortMap.size());
2707
2708    boolean targetFound = false;
2709    int index = 0;
2710    int targetIndex = 0;
2711    int startIndex = 0;
2712    int includedAfterCount = 0;
2713    long[] idSet = new long[sortMap.size()];
2714    for (Map.Entry<ByteString, EntryID> entry : sortMap.entrySet())
2715    {
2716      ByteString vlvKey = entry.getKey();
2717      EntryID id = entry.getValue();
2718      idSet[index++] = id.longValue();
2719
2720      if (targetFound)
2721      {
2722        includedAfterCount++;
2723        if (includedAfterCount >= vlvRequest.getAfterCount())
2724        {
2725          break;
2726        }
2727      }
2728      else
2729      {
2730        targetFound = vlvKey.compareTo(encodedTargetAssertion) >= 0;
2731        if (targetFound)
2732        {
2733          startIndex = Math.max(0, targetIndex - vlvRequest.getBeforeCount());
2734        }
2735        targetIndex++;
2736      }
2737    }
2738
2739    final long[] result;
2740    if (targetFound)
2741    {
2742      final long[] array = new long[index - startIndex];
2743      System.arraycopy(idSet, startIndex, array, 0, array.length);
2744      result = array;
2745    }
2746    else
2747    {
2748      /*
2749       * No entry was found to be greater than or equal to the sort key, so the target offset will
2750       * be one greater than the content count.
2751       */
2752      targetIndex = sortMap.size() + 1;
2753      result = new long[0];
2754    }
2755    searchOperation.addResponseControl(new VLVResponseControl(targetIndex, sortMap.size(), LDAPResultCode.SUCCESS));
2756    return result;
2757  }
2758
2759  private static final long[] sortByOffset(SearchOperation searchOperation, VLVRequestControl vlvRequest,
2760      TreeMap<ByteString, EntryID> sortMap) throws DirectoryException
2761  {
2762    int targetOffset = vlvRequest.getOffset();
2763    if (targetOffset < 0)
2764    {
2765      // The client specified a negative target offset. This
2766      // should never be allowed.
2767      searchOperation.addResponseControl(new VLVResponseControl(targetOffset, sortMap.size(),
2768          LDAPResultCode.OFFSET_RANGE_ERROR));
2769
2770      LocalizableMessage message = ERR_ENTRYIDSORTER_NEGATIVE_START_POS.get();
2771      throw new DirectoryException(ResultCode.VIRTUAL_LIST_VIEW_ERROR, message);
2772    }
2773
2774    // This is an easy mistake to make, since VLV offsets start at 1 instead of 0. We'll assume the client meant
2775    // to use 1.
2776    targetOffset = (targetOffset == 0) ? 1 : targetOffset;
2777
2778    int beforeCount = vlvRequest.getBeforeCount();
2779    int afterCount = vlvRequest.getAfterCount();
2780    int listOffset = targetOffset - 1; // VLV offsets start at 1, not 0.
2781    int startPos = listOffset - beforeCount;
2782    if (startPos < 0)
2783    {
2784      // This can happen if beforeCount >= offset, and in this case we'll just adjust the start position to ignore
2785      // the range of beforeCount that doesn't exist.
2786      startPos = 0;
2787      beforeCount = listOffset;
2788    }
2789    else if (startPos >= sortMap.size())
2790    {
2791      // The start position is beyond the end of the list. In this case, we'll assume that the start position was
2792      // one greater than the size of the list and will only return the beforeCount entries.
2793      targetOffset = sortMap.size() + 1;
2794      listOffset = sortMap.size();
2795      startPos = listOffset - beforeCount;
2796      afterCount = 0;
2797    }
2798
2799    int count = 1 + beforeCount + afterCount;
2800    long[] sortedIDs = new long[count];
2801    int treePos = 0;
2802    int arrayPos = 0;
2803    for (EntryID id : sortMap.values())
2804    {
2805      if (treePos++ < startPos)
2806      {
2807        continue;
2808      }
2809
2810      sortedIDs[arrayPos++] = id.longValue();
2811      if (arrayPos >= count)
2812      {
2813        break;
2814      }
2815    }
2816
2817    if (arrayPos < count)
2818    {
2819      // We don't have enough entries in the set to meet the requested page size, so we'll need to shorten the array.
2820      sortedIDs = Arrays.copyOf(sortedIDs, arrayPos);
2821    }
2822
2823    searchOperation.addResponseControl(new VLVResponseControl(targetOffset, sortMap.size(), LDAPResultCode.SUCCESS));
2824    return sortedIDs;
2825  }
2826
2827  /** Get the exclusive lock. */
2828  void lock()
2829  {
2830    exclusiveLock.lock();
2831  }
2832
2833  /** Unlock the exclusive lock. */
2834  void unlock()
2835  {
2836    exclusiveLock.unlock();
2837  }
2838
2839  @Override
2840  public String toString() {
2841    return treePrefix;
2842  }
2843}