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