001/*
002 * Copyright 2011-2022 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2011-2022 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2011-2022 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.listener;
037
038
039
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.Date;
045import java.util.HashMap;
046import java.util.Iterator;
047import java.util.LinkedHashMap;
048import java.util.LinkedHashSet;
049import java.util.List;
050import java.util.Map;
051import java.util.Set;
052import java.util.SortedSet;
053import java.util.TreeMap;
054import java.util.TreeSet;
055import java.util.concurrent.atomic.AtomicLong;
056import java.util.concurrent.atomic.AtomicReference;
057
058import com.unboundid.asn1.ASN1Integer;
059import com.unboundid.asn1.ASN1OctetString;
060import com.unboundid.ldap.protocol.AddRequestProtocolOp;
061import com.unboundid.ldap.protocol.AddResponseProtocolOp;
062import com.unboundid.ldap.protocol.BindRequestProtocolOp;
063import com.unboundid.ldap.protocol.BindResponseProtocolOp;
064import com.unboundid.ldap.protocol.CompareRequestProtocolOp;
065import com.unboundid.ldap.protocol.CompareResponseProtocolOp;
066import com.unboundid.ldap.protocol.DeleteRequestProtocolOp;
067import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
068import com.unboundid.ldap.protocol.ExtendedRequestProtocolOp;
069import com.unboundid.ldap.protocol.ExtendedResponseProtocolOp;
070import com.unboundid.ldap.protocol.LDAPMessage;
071import com.unboundid.ldap.protocol.ModifyRequestProtocolOp;
072import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
073import com.unboundid.ldap.protocol.ModifyDNRequestProtocolOp;
074import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
075import com.unboundid.ldap.protocol.ProtocolOp;
076import com.unboundid.ldap.protocol.SearchRequestProtocolOp;
077import com.unboundid.ldap.protocol.SearchResultDoneProtocolOp;
078import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule;
079import com.unboundid.ldap.matchingrules.GeneralizedTimeMatchingRule;
080import com.unboundid.ldap.matchingrules.IntegerMatchingRule;
081import com.unboundid.ldap.matchingrules.MatchingRule;
082import com.unboundid.ldap.protocol.SearchResultReferenceProtocolOp;
083import com.unboundid.ldap.sdk.AddRequest;
084import com.unboundid.ldap.sdk.Attribute;
085import com.unboundid.ldap.sdk.BindResult;
086import com.unboundid.ldap.sdk.ChangeLogEntry;
087import com.unboundid.ldap.sdk.Control;
088import com.unboundid.ldap.sdk.DN;
089import com.unboundid.ldap.sdk.DeleteRequest;
090import com.unboundid.ldap.sdk.Entry;
091import com.unboundid.ldap.sdk.EntrySorter;
092import com.unboundid.ldap.sdk.ExtendedRequest;
093import com.unboundid.ldap.sdk.ExtendedResult;
094import com.unboundid.ldap.sdk.Filter;
095import com.unboundid.ldap.sdk.LDAPException;
096import com.unboundid.ldap.sdk.LDAPResult;
097import com.unboundid.ldap.sdk.LDAPURL;
098import com.unboundid.ldap.sdk.Modification;
099import com.unboundid.ldap.sdk.ModificationType;
100import com.unboundid.ldap.sdk.ModifyDNRequest;
101import com.unboundid.ldap.sdk.ModifyRequest;
102import com.unboundid.ldap.sdk.OperationType;
103import com.unboundid.ldap.sdk.RDN;
104import com.unboundid.ldap.sdk.ReadOnlyEntry;
105import com.unboundid.ldap.sdk.ResultCode;
106import com.unboundid.ldap.sdk.SearchResultEntry;
107import com.unboundid.ldap.sdk.SearchResultReference;
108import com.unboundid.ldap.sdk.SearchScope;
109import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
110import com.unboundid.ldap.sdk.schema.DITContentRuleDefinition;
111import com.unboundid.ldap.sdk.schema.DITStructureRuleDefinition;
112import com.unboundid.ldap.sdk.schema.EntryValidator;
113import com.unboundid.ldap.sdk.schema.MatchingRuleUseDefinition;
114import com.unboundid.ldap.sdk.schema.NameFormDefinition;
115import com.unboundid.ldap.sdk.schema.ObjectClassDefinition;
116import com.unboundid.ldap.sdk.schema.Schema;
117import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
118import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
119import com.unboundid.ldap.sdk.controls.AuthorizationIdentityResponseControl;
120import com.unboundid.ldap.sdk.controls.DontUseCopyRequestControl;
121import com.unboundid.ldap.sdk.controls.DraftLDUPSubentriesRequestControl;
122import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
123import com.unboundid.ldap.sdk.controls.PermissiveModifyRequestControl;
124import com.unboundid.ldap.sdk.controls.PostReadRequestControl;
125import com.unboundid.ldap.sdk.controls.PostReadResponseControl;
126import com.unboundid.ldap.sdk.controls.PreReadRequestControl;
127import com.unboundid.ldap.sdk.controls.PreReadResponseControl;
128import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV1RequestControl;
129import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
130import com.unboundid.ldap.sdk.controls.RFC3672SubentriesRequestControl;
131import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
132import com.unboundid.ldap.sdk.controls.ServerSideSortResponseControl;
133import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
134import com.unboundid.ldap.sdk.controls.SortKey;
135import com.unboundid.ldap.sdk.controls.SubtreeDeleteRequestControl;
136import com.unboundid.ldap.sdk.controls.TransactionSpecificationRequestControl;
137import com.unboundid.ldap.sdk.controls.VirtualListViewRequestControl;
138import com.unboundid.ldap.sdk.controls.VirtualListViewResponseControl;
139import com.unboundid.ldap.sdk.experimental.
140            DraftZeilengaLDAPNoOp12RequestControl;
141import com.unboundid.ldap.sdk.extensions.AbortedTransactionExtendedResult;
142import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
143import com.unboundid.ldap.sdk.unboundidds.controls.
144            IgnoreNoUserModificationRequestControl;
145import com.unboundid.ldif.LDIFAddChangeRecord;
146import com.unboundid.ldif.LDIFChangeRecord;
147import com.unboundid.ldif.LDIFDeleteChangeRecord;
148import com.unboundid.ldif.LDIFException;
149import com.unboundid.ldif.LDIFModifyChangeRecord;
150import com.unboundid.ldif.LDIFModifyDNChangeRecord;
151import com.unboundid.ldif.LDIFReader;
152import com.unboundid.ldif.LDIFWriter;
153import com.unboundid.util.CryptoHelper;
154import com.unboundid.util.Debug;
155import com.unboundid.util.Mutable;
156import com.unboundid.util.NotNull;
157import com.unboundid.util.Nullable;
158import com.unboundid.util.ObjectPair;
159import com.unboundid.util.StaticUtils;
160import com.unboundid.util.ThreadSafety;
161import com.unboundid.util.ThreadSafetyLevel;
162
163import static com.unboundid.ldap.listener.ListenerMessages.*;
164
165
166
167/**
168 * This class provides an implementation of an LDAP request handler that can be
169 * used to store entries in memory and process operations on those entries.
170 * It is primarily intended for use in creating a simple embeddable directory
171 * server that can be used for testing purposes.  It performs only very basic
172 * validation, and is not intended to be a fully standards-compliant server.
173 */
174@Mutable()
175@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
176public final class InMemoryRequestHandler
177       extends LDAPListenerRequestHandler
178{
179  /**
180   * A pre-allocated array containing no controls.
181   */
182  @NotNull private static final Control[] NO_CONTROLS = new Control[0];
183
184
185
186  /**
187   * The OID for a proprietary control that can be used to indicate that the
188   * associated operation should be considered an internal operation that was
189   * requested by a method call in the in-memory directory server class rather
190   * than from an LDAP client.  It may be used to bypass certain restrictions
191   * that might otherwise be enforced (e.g., allowed operation types, write
192   * access to NO-USER-MODIFICATION attributes, etc.).
193   */
194  @NotNull static final String OID_INTERNAL_OPERATION_REQUEST_CONTROL =
195       "1.3.6.1.4.1.30221.2.5.18";
196
197
198
199  // The change number for the first changelog entry in the server.
200  @NotNull private final AtomicLong firstChangeNumber;
201
202  // The change number for the last changelog entry in the server.
203  @NotNull private final AtomicLong lastChangeNumber;
204
205  // A delay (in milliseconds) to insert before processing operations.
206  @NotNull private final AtomicLong processingDelayMillis;
207
208  // The reference to the entry validator that will be used for schema checking,
209  // if appropriate.
210  @NotNull private final AtomicReference<EntryValidator> entryValidatorRef;
211
212  // The entry to use as the subschema subentry.
213  @NotNull private final AtomicReference<ReadOnlyEntry> subschemaSubentryRef;
214
215  // The reference to the schema that will be used for this request handler.
216  @NotNull private final AtomicReference<Schema> schemaRef;
217
218  // Indicates whether to generate operational attributes for writes.
219  private final boolean generateOperationalAttributes;
220
221  // The DN of the currently-authenticated user for the associated connection.
222  @NotNull private DN authenticatedDN;
223
224  // The base DN for the server changelog.
225  @NotNull private final DN changeLogBaseDN;
226
227  // The DN of the subschema subentry.
228  @NotNull private final DN subschemaSubentryDN;
229
230  // The configuration used to create this request handler.
231  @NotNull private final InMemoryDirectoryServerConfig config;
232
233  // A snapshot containing the server content as it initially appeared.  It
234  // will not contain any user data, but may contain a changelog base entry.
235  @NotNull private final InMemoryDirectoryServerSnapshot initialSnapshot;
236
237  // The primary password encoder for the server.
238  @Nullable private final InMemoryPasswordEncoder primaryPasswordEncoder;
239
240  // The maximum number of changelog entries to maintain.
241  private final int maxChangelogEntries;
242
243  // The maximum number of entries to return from any single search.
244  private final int maxSizeLimit;
245
246  // The client connection for this request handler instance.
247  @Nullable private final LDAPListenerClientConnection connection;
248
249  // The list of all password encoders (primary and secondary) configured for
250  // the in-memory directory server.
251  @NotNull private final List<InMemoryPasswordEncoder> passwordEncoders;
252
253  // The list of password attributes as requested by the user.  This will be a
254  // minimal list, without multiple forms for each attribute type.
255  @NotNull private final List<String> configuredPasswordAttributes;
256
257  // The list of extended password attributes, including alternate names and
258  // OIDs for each attribute type, when available.
259  @NotNull private final List<String> extendedPasswordAttributes;
260
261  // The set of equality indexes defined for the server.
262  @NotNull private final Map<AttributeTypeDefinition,
263     InMemoryDirectoryServerEqualityAttributeIndex> equalityIndexes;
264
265  // An additional set of credentials that may be used for bind operations.
266  @NotNull private final Map<DN,byte[]> additionalBindCredentials;
267
268  // A map of the available extended operation handlers by request OID.
269  @NotNull private final Map<String,InMemoryExtendedOperationHandler>
270       extendedRequestHandlers;
271
272  // A map of the available SASL bind handlers by mechanism name.
273  @NotNull private final Map<String,InMemorySASLBindHandler> saslBindHandlers;
274
275  // A map of state information specific to the associated connection.
276  @NotNull private final Map<String,Object> connectionState;
277
278  // The set of base DNs for the server.
279  @NotNull private final Set<DN> baseDNs;
280
281  // The set of referential integrity attributes for the server.
282  @NotNull private final Set<String> referentialIntegrityAttributes;
283
284  // The map of entries currently held in the server.
285  @NotNull private final Map<DN,ReadOnlyEntry> entryMap;
286
287
288
289  /**
290   * Creates a new instance of this request handler with an initially-empty
291   * data set.
292   *
293   * @param  config  The configuration that should be used for the in-memory
294   *                 directory server.
295   *
296   * @throws  LDAPException  If there is a problem with the provided
297   *                         configuration.
298   */
299  public InMemoryRequestHandler(
300              @NotNull final InMemoryDirectoryServerConfig config)
301         throws LDAPException
302  {
303    this.config = config;
304
305    schemaRef            = new AtomicReference<>();
306    entryValidatorRef    = new AtomicReference<>();
307    subschemaSubentryRef = new AtomicReference<>();
308
309    final Schema schema = config.getSchema();
310    schemaRef.set(schema);
311    if (schema != null)
312    {
313      final EntryValidator entryValidator = new EntryValidator(schema);
314      entryValidatorRef.set(entryValidator);
315      entryValidator.setCheckAttributeSyntax(
316           config.enforceAttributeSyntaxCompliance());
317      entryValidator.setCheckStructuralObjectClasses(
318           config.enforceSingleStructuralObjectClass());
319    }
320
321    final DN[] baseDNArray = config.getBaseDNs();
322    if ((baseDNArray == null) || (baseDNArray.length == 0))
323    {
324      throw new LDAPException(ResultCode.PARAM_ERROR,
325           ERR_MEM_HANDLER_NO_BASE_DNS.get());
326    }
327
328    entryMap = new TreeMap<>();
329
330    final LinkedHashSet<DN> baseDNSet =
331         new LinkedHashSet<>(Arrays.asList(baseDNArray));
332    if (baseDNSet.contains(DN.NULL_DN))
333    {
334      throw new LDAPException(ResultCode.PARAM_ERROR,
335           ERR_MEM_HANDLER_NULL_BASE_DN.get());
336    }
337
338    changeLogBaseDN = new DN("cn=changelog", schema);
339    if (baseDNSet.contains(changeLogBaseDN))
340    {
341      throw new LDAPException(ResultCode.PARAM_ERROR,
342           ERR_MEM_HANDLER_CHANGELOG_BASE_DN.get(changeLogBaseDN));
343    }
344
345    maxChangelogEntries = config.getMaxChangeLogEntries();
346
347    if (config.getMaxSizeLimit() <= 0)
348    {
349      maxSizeLimit = Integer.MAX_VALUE;
350    }
351    else
352    {
353      maxSizeLimit = config.getMaxSizeLimit();
354    }
355
356    final TreeMap<String,InMemoryExtendedOperationHandler> extOpHandlers =
357         new TreeMap<>();
358    for (final InMemoryExtendedOperationHandler h :
359         config.getExtendedOperationHandlers())
360    {
361      for (final String oid : h.getSupportedExtendedRequestOIDs())
362      {
363        if (extOpHandlers.containsKey(oid))
364        {
365          throw new LDAPException(ResultCode.PARAM_ERROR,
366               ERR_MEM_HANDLER_EXTENDED_REQUEST_HANDLER_CONFLICT.get(oid));
367        }
368        else
369        {
370          extOpHandlers.put(oid, h);
371        }
372      }
373    }
374    extendedRequestHandlers = Collections.unmodifiableMap(extOpHandlers);
375
376    final TreeMap<String,InMemorySASLBindHandler> saslHandlers =
377         new TreeMap<>();
378    for (final InMemorySASLBindHandler h : config.getSASLBindHandlers())
379    {
380      final String mech = h.getSASLMechanismName();
381      if (saslHandlers.containsKey(mech))
382      {
383        throw new LDAPException(ResultCode.PARAM_ERROR,
384             ERR_MEM_HANDLER_SASL_BIND_HANDLER_CONFLICT.get(mech));
385      }
386      else
387      {
388        saslHandlers.put(mech, h);
389      }
390    }
391    saslBindHandlers = Collections.unmodifiableMap(saslHandlers);
392
393    additionalBindCredentials = Collections.unmodifiableMap(
394         config.getAdditionalBindCredentials());
395
396    final List<String> eqIndexAttrs = config.getEqualityIndexAttributes();
397    equalityIndexes = new HashMap<>(
398         StaticUtils.computeMapCapacity(eqIndexAttrs.size()));
399    for (final String s : eqIndexAttrs)
400    {
401      final InMemoryDirectoryServerEqualityAttributeIndex i =
402           new InMemoryDirectoryServerEqualityAttributeIndex(s, schema);
403      equalityIndexes.put(i.getAttributeType(), i);
404    }
405
406    final Set<String> pwAttrSet = config.getPasswordAttributes();
407    final LinkedHashSet<String> basePWAttrSet =
408         new LinkedHashSet<>(StaticUtils.computeMapCapacity(pwAttrSet.size()));
409    final LinkedHashSet<String> extendedPWAttrSet = new LinkedHashSet<>(
410         StaticUtils.computeMapCapacity(pwAttrSet.size()*2));
411    for (final String attr : pwAttrSet)
412    {
413      basePWAttrSet.add(attr);
414      extendedPWAttrSet.add(StaticUtils.toLowerCase(attr));
415
416      if (schema != null)
417      {
418        final AttributeTypeDefinition attrType = schema.getAttributeType(attr);
419        if (attrType != null)
420        {
421          for (final String name : attrType.getNames())
422          {
423            extendedPWAttrSet.add(StaticUtils.toLowerCase(name));
424          }
425          extendedPWAttrSet.add(StaticUtils.toLowerCase(attrType.getOID()));
426        }
427      }
428    }
429
430    configuredPasswordAttributes =
431         Collections.unmodifiableList(new ArrayList<>(basePWAttrSet));
432    extendedPasswordAttributes =
433         Collections.unmodifiableList(new ArrayList<>(extendedPWAttrSet));
434
435    referentialIntegrityAttributes = Collections.unmodifiableSet(
436         config.getReferentialIntegrityAttributes());
437
438    primaryPasswordEncoder = config.getPrimaryPasswordEncoder();
439
440    final ArrayList<InMemoryPasswordEncoder> encoderList = new ArrayList<>(10);
441    if (primaryPasswordEncoder != null)
442    {
443      encoderList.add(primaryPasswordEncoder);
444    }
445    encoderList.addAll(config.getSecondaryPasswordEncoders());
446    passwordEncoders = Collections.unmodifiableList(encoderList);
447
448    baseDNs = Collections.unmodifiableSet(baseDNSet);
449    generateOperationalAttributes = config.generateOperationalAttributes();
450    authenticatedDN               = new DN("cn=Internal Root User", schema);
451    connection                    = null;
452    connectionState               = Collections.emptyMap();
453    firstChangeNumber             = new AtomicLong(0L);
454    lastChangeNumber              = new AtomicLong(0L);
455    processingDelayMillis         = new AtomicLong(0L);
456
457    final ReadOnlyEntry subschemaSubentry = generateSubschemaSubentry(schema);
458    subschemaSubentryRef.set(subschemaSubentry);
459    subschemaSubentryDN = subschemaSubentry.getParsedDN();
460
461    if (baseDNs.contains(subschemaSubentryDN))
462    {
463      throw new LDAPException(ResultCode.PARAM_ERROR,
464           ERR_MEM_HANDLER_SCHEMA_BASE_DN.get(subschemaSubentryDN));
465    }
466
467    if (maxChangelogEntries > 0)
468    {
469      baseDNSet.add(changeLogBaseDN);
470
471      final ReadOnlyEntry changeLogBaseEntry = new ReadOnlyEntry(
472           changeLogBaseDN, schema,
473           new Attribute("objectClass", "top", "namedObject"),
474           new Attribute("cn", "changelog"),
475           new Attribute("entryDN",
476                DistinguishedNameMatchingRule.getInstance(),
477                "cn=changelog"),
478           new Attribute("entryUUID", CryptoHelper.getRandomUUID().toString()),
479           new Attribute("creatorsName",
480                DistinguishedNameMatchingRule.getInstance(),
481                DN.NULL_DN.toString()),
482           new Attribute("createTimestamp",
483                GeneralizedTimeMatchingRule.getInstance(),
484                StaticUtils.encodeGeneralizedTime(new Date())),
485           new Attribute("modifiersName",
486                DistinguishedNameMatchingRule.getInstance(),
487                DN.NULL_DN.toString()),
488           new Attribute("modifyTimestamp",
489                GeneralizedTimeMatchingRule.getInstance(),
490                StaticUtils.encodeGeneralizedTime(new Date())),
491           new Attribute("subschemaSubentry",
492                DistinguishedNameMatchingRule.getInstance(),
493                subschemaSubentryDN.toString()));
494      entryMap.put(changeLogBaseDN, changeLogBaseEntry);
495      indexAdd(changeLogBaseEntry);
496    }
497
498    initialSnapshot = createSnapshot();
499  }
500
501
502
503  /**
504   * Creates a new instance of this request handler that will use the provided
505   * entry map object.
506   *
507   * @param  parent      The parent request handler instance.
508   * @param  connection  The client connection for this instance.
509   */
510  private InMemoryRequestHandler(@NotNull final InMemoryRequestHandler parent,
511               @NotNull final LDAPListenerClientConnection connection)
512  {
513    this.connection = connection;
514
515    authenticatedDN = DN.NULL_DN;
516    connectionState =
517         Collections.synchronizedMap(new LinkedHashMap<String,Object>(0));
518
519    config                         = parent.config;
520    generateOperationalAttributes  = parent.generateOperationalAttributes;
521    additionalBindCredentials      = parent.additionalBindCredentials;
522    baseDNs                        = parent.baseDNs;
523    changeLogBaseDN                = parent.changeLogBaseDN;
524    firstChangeNumber              = parent.firstChangeNumber;
525    lastChangeNumber               = parent.lastChangeNumber;
526    processingDelayMillis          = parent.processingDelayMillis;
527    maxChangelogEntries            = parent.maxChangelogEntries;
528    maxSizeLimit                   = parent.maxSizeLimit;
529    equalityIndexes                = parent.equalityIndexes;
530    referentialIntegrityAttributes = parent.referentialIntegrityAttributes;
531    entryMap                       = parent.entryMap;
532    entryValidatorRef              = parent.entryValidatorRef;
533    extendedRequestHandlers        = parent.extendedRequestHandlers;
534    saslBindHandlers               = parent.saslBindHandlers;
535    schemaRef                      = parent.schemaRef;
536    subschemaSubentryRef           = parent.subschemaSubentryRef;
537    subschemaSubentryDN            = parent.subschemaSubentryDN;
538    initialSnapshot                = parent.initialSnapshot;
539    configuredPasswordAttributes   = parent.configuredPasswordAttributes;
540    extendedPasswordAttributes     = parent.extendedPasswordAttributes;
541    primaryPasswordEncoder         = parent.primaryPasswordEncoder;
542    passwordEncoders               = parent.passwordEncoders;
543  }
544
545
546
547  /**
548   * Creates a new instance of this request handler that will be used to process
549   * requests read by the provided connection.
550   *
551   * @param  connection  The connection with which this request handler instance
552   *                     will be associated.
553   *
554   * @return  The request handler instance that will be used for the provided
555   *          connection.
556   *
557   * @throws  LDAPException  If the connection should not be accepted.
558   */
559  @Override()
560  @NotNull()
561  public InMemoryRequestHandler newInstance(
562              @NotNull final LDAPListenerClientConnection connection)
563         throws LDAPException
564  {
565    return new InMemoryRequestHandler(this, connection);
566  }
567
568
569
570  /**
571   * Creates a point-in-time snapshot of the information contained in this
572   * in-memory request handler.  If desired, it may be restored using the
573   * {@link #restoreSnapshot} method.
574   *
575   * @return  The snapshot created based on the current content of this
576   *          in-memory request handler.
577   */
578  @NotNull()
579  public InMemoryDirectoryServerSnapshot createSnapshot()
580  {
581    synchronized (entryMap)
582    {
583      return new InMemoryDirectoryServerSnapshot(entryMap,
584           firstChangeNumber.get(), lastChangeNumber.get());
585    }
586  }
587
588
589
590  /**
591   * Updates the content of this in-memory request handler to match what it was
592   * at the time the snapshot was created.
593   *
594   * @param  snapshot  The snapshot to be restored.  It must not be
595   *                   {@code null}.
596   */
597  public void restoreSnapshot(
598                   @NotNull final InMemoryDirectoryServerSnapshot snapshot)
599  {
600    synchronized (entryMap)
601    {
602      entryMap.clear();
603      entryMap.putAll(snapshot.getEntryMap());
604
605      for (final InMemoryDirectoryServerEqualityAttributeIndex i :
606           equalityIndexes.values())
607      {
608        i.clear();
609        for (final Entry e : entryMap.values())
610        {
611          try
612          {
613            i.processAdd(e);
614          }
615          catch (final Exception ex)
616          {
617            Debug.debugException(ex);
618          }
619        }
620      }
621
622      firstChangeNumber.set(snapshot.getFirstChangeNumber());
623      lastChangeNumber.set(snapshot.getLastChangeNumber());
624    }
625  }
626
627
628
629  /**
630   * Retrieves the schema that will be used by the server, if any.
631   *
632   * @return  The schema that will be used by the server, or {@code null} if
633   *          none has been configured.
634   */
635  @Nullable()
636  public Schema getSchema()
637  {
638    return schemaRef.get();
639  }
640
641
642
643  /**
644   * Retrieves a list of the base DNs configured for use by the server.
645   *
646   * @return  A list of the base DNs configured for use by the server.
647   */
648  @NotNull()
649  public List<DN> getBaseDNs()
650  {
651    return Collections.unmodifiableList(new ArrayList<>(baseDNs));
652  }
653
654
655
656  /**
657   * Retrieves the client connection associated with this request handler
658   * instance.
659   *
660   * @return  The client connection associated with this request handler
661   *          instance, or {@code null} if this instance is not associated with
662   *          any client connection.
663   */
664  @Nullable()
665  public LDAPListenerClientConnection getClientConnection()
666  {
667    return connection;
668  }
669
670
671
672  /**
673   * Retrieves the DN of the user currently authenticated on the connection
674   * associated with this request handler instance.
675   *
676   * @return  The DN of the user currently authenticated on the connection
677   *          associated with this request handler instance, or
678   *          {@code DN#NULL_DN} if the connection is unauthenticated or is
679   *          authenticated as the anonymous user.
680   */
681  @NotNull()
682  public synchronized DN getAuthenticatedDN()
683  {
684    return authenticatedDN;
685  }
686
687
688
689  /**
690   * Sets the DN of the user currently authenticated on the connection
691   * associated with this request handler instance.
692   *
693   * @param  authenticatedDN  The DN of the user currently authenticated on the
694   *                          connection associated with this request handler.
695   *                          It may be {@code null} or {@link DN#NULL_DN} to
696   *                          indicate that the connection is unauthenticated.
697   */
698  public synchronized void setAuthenticatedDN(
699                                @Nullable final DN authenticatedDN)
700  {
701    if (authenticatedDN == null)
702    {
703      this.authenticatedDN = DN.NULL_DN;
704    }
705    else
706    {
707      this.authenticatedDN = authenticatedDN;
708    }
709  }
710
711
712
713  /**
714   * Retrieves an unmodifiable map containing the defined set of additional bind
715   * credentials, mapped from bind DN to password bytes.
716   *
717   * @return  An unmodifiable map containing the defined set of additional bind
718   *          credentials, or an empty map if no additional credentials have
719   *          been defined.
720   */
721  @NotNull()
722  public Map<DN,byte[]> getAdditionalBindCredentials()
723  {
724    return additionalBindCredentials;
725  }
726
727
728
729  /**
730   * Retrieves the password for the given DN from the set of additional bind
731   * credentials.
732   *
733   * @param  dn  The DN for which to retrieve the corresponding password.
734   *
735   * @return  The password bytes for the given DN, or {@code null} if the
736   *          additional bind credentials does not include information for the
737   *          provided DN.
738   */
739  @Nullable()
740  public byte[] getAdditionalBindCredentials(@NotNull final DN dn)
741  {
742    return additionalBindCredentials.get(dn);
743  }
744
745
746
747  /**
748   * Retrieves a map that may be used to hold state information specific to the
749   * connection associated with this request handler instance.  It may be
750   * queried and updated if necessary to store state information that may be
751   * needed at multiple different times in the life of a connection (e.g., when
752   * processing a multi-stage SASL bind).
753   *
754   * @return  An updatable map that may be used to hold state information
755   *          specific to the connection associated with this request handler
756   *          instance.
757   */
758  @NotNull()
759  public Map<String,Object> getConnectionState()
760  {
761    return connectionState;
762  }
763
764
765
766  /**
767   * Retrieves the delay in milliseconds that the server should impose before
768   * beginning processing for operations.
769   *
770   * @return  The delay in milliseconds that the server should impose before
771   *          beginning processing for operations, or 0 if there should be no
772   *          delay inserted when processing operations.
773   */
774  public long getProcessingDelayMillis()
775  {
776    return processingDelayMillis.get();
777  }
778
779
780
781  /**
782   * Specifies the delay in milliseconds that the server should impose before
783   * beginning processing for operations.
784   *
785   * @param  processingDelayMillis  The delay in milliseconds that the server
786   *                                should impose before beginning processing
787   *                                for operations.  A value less than or equal
788   *                                to zero may be used to indicate that there
789   *                                should be no delay.
790   */
791  public void setProcessingDelayMillis(final long processingDelayMillis)
792  {
793    if (processingDelayMillis > 0)
794    {
795      this.processingDelayMillis.set(processingDelayMillis);
796    }
797    else
798    {
799      this.processingDelayMillis.set(0L);
800    }
801  }
802
803
804
805  /**
806   * Processes the provided add request.
807   * <BR><BR>
808   * This method may be used regardless of whether the server is listening for
809   * client connections, and regardless of whether add operations are allowed in
810   * the server.
811   *
812   * @param  addRequest  The add request to be processed.  It must not be
813   *                     {@code null}.
814   *
815   * @return  The result of processing the add operation.
816   *
817   * @throws  LDAPException  If the server rejects the add request, or if a
818   *                         problem is encountered while sending the request or
819   *                         reading the response.
820   */
821  @NotNull()
822  public LDAPResult add(@NotNull final AddRequest addRequest)
823         throws LDAPException
824  {
825    final ArrayList<Control> requestControlList =
826         new ArrayList<>(addRequest.getControlList());
827    requestControlList.add(new Control(OID_INTERNAL_OPERATION_REQUEST_CONTROL,
828         false));
829
830    final LDAPMessage responseMessage = processAddRequest(1,
831         new AddRequestProtocolOp(addRequest.getDN(),
832              addRequest.getAttributes()),
833         requestControlList);
834
835    final AddResponseProtocolOp addResponse =
836         responseMessage.getAddResponseProtocolOp();
837
838    final LDAPResult ldapResult = new LDAPResult(responseMessage.getMessageID(),
839         ResultCode.valueOf(addResponse.getResultCode()),
840         addResponse.getDiagnosticMessage(), addResponse.getMatchedDN(),
841         addResponse.getReferralURLs(), responseMessage.getControls());
842
843    switch (addResponse.getResultCode())
844    {
845      case ResultCode.SUCCESS_INT_VALUE:
846      case ResultCode.NO_OPERATION_INT_VALUE:
847        return ldapResult;
848      default:
849        throw new LDAPException(ldapResult);
850    }
851  }
852
853
854
855  /**
856   * Attempts to add an entry to the in-memory data set.  The attempt will fail
857   * if any of the following conditions is true:
858   * <UL>
859   *   <LI>There is a problem with any of the request controls.</LI>
860   *   <LI>The provided entry has a malformed DN.</LI>
861   *   <LI>The provided entry has the null DN.</LI>
862   *   <LI>The provided entry has a DN that is the same as or subordinate to the
863   *       subschema subentry.</LI>
864   *   <LI>The provided entry has a DN that is the same as or subordinate to the
865   *       changelog base entry.</LI>
866   *   <LI>An entry already exists with the same DN as the entry in the provided
867   *       request.</LI>
868   *   <LI>The entry is outside the set of base DNs for the server.</LI>
869   *   <LI>The entry is below one of the defined base DNs but the immediate
870   *       parent entry does not exist.</LI>
871   *   <LI>If a schema was provided, and the entry is not valid according to the
872   *       constraints of that schema.</LI>
873   * </UL>
874   *
875   * @param  messageID  The message ID of the LDAP message containing the add
876   *                    request.
877   * @param  request    The add request that was included in the LDAP message
878   *                    that was received.
879   * @param  controls   The set of controls included in the LDAP message.  It
880   *                    may be empty if there were no controls, but will not be
881   *                    {@code null}.
882   *
883   * @return  The {@link LDAPMessage} containing the response to send to the
884   *          client.  The protocol op in the {@code LDAPMessage} must be an
885   *          {@code AddResponseProtocolOp}.
886   */
887  @Override()
888  @NotNull()
889  public LDAPMessage processAddRequest(final int messageID,
890                          @NotNull final AddRequestProtocolOp request,
891                          @NotNull final List<Control> controls)
892  {
893    synchronized (entryMap)
894    {
895      // Sleep before processing, if appropriate.
896      sleepBeforeProcessing();
897
898      // Process the provided request controls.
899      final Map<String,Control> controlMap;
900      try
901      {
902        controlMap = RequestControlPreProcessor.processControls(
903             LDAPMessage.PROTOCOL_OP_TYPE_ADD_REQUEST, controls);
904      }
905      catch (final LDAPException le)
906      {
907        Debug.debugException(le);
908        return new LDAPMessage(messageID, new AddResponseProtocolOp(
909             le.getResultCode().intValue(), null, le.getMessage(), null));
910      }
911      final ArrayList<Control> responseControls = new ArrayList<>(1);
912
913
914      // If this operation type is not allowed, then reject it.
915      final boolean isInternalOp =
916           controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL);
917      if ((! isInternalOp) &&
918           (! config.getAllowedOperationTypes().contains(OperationType.ADD)))
919      {
920        return new LDAPMessage(messageID, new AddResponseProtocolOp(
921             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
922             ERR_MEM_HANDLER_ADD_NOT_ALLOWED.get(), null));
923      }
924
925
926      // If this operation type requires authentication, then ensure that the
927      // client is authenticated.
928      if ((authenticatedDN.isNullDN() &&
929           config.getAuthenticationRequiredOperationTypes().contains(
930                OperationType.ADD)))
931      {
932        return new LDAPMessage(messageID, new AddResponseProtocolOp(
933             ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null,
934             ERR_MEM_HANDLER_ADD_REQUIRES_AUTH.get(), null));
935      }
936
937
938      // See if this add request is part of a transaction.  If so, then perform
939      // appropriate processing for it and return success immediately without
940      // actually doing any further processing.
941      try
942      {
943        final ASN1OctetString txnID =
944             processTransactionRequest(messageID, request, controlMap);
945        if (txnID != null)
946        {
947          return new LDAPMessage(messageID, new AddResponseProtocolOp(
948               ResultCode.SUCCESS_INT_VALUE, null,
949               INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null));
950        }
951      }
952      catch (final LDAPException le)
953      {
954        Debug.debugException(le);
955        return new LDAPMessage(messageID,
956             new AddResponseProtocolOp(le.getResultCode().intValue(),
957                  le.getMatchedDN(), le.getDiagnosticMessage(),
958                  StaticUtils.toList(le.getReferralURLs())),
959             le.getResponseControls());
960      }
961
962
963      // Get the entry to be added.  If a schema was provided, then make sure
964      // the attributes are created with the appropriate matching rules.
965      final Entry entry;
966      final Schema schema = schemaRef.get();
967      if (schema == null)
968      {
969        entry = new Entry(request.getDN(), request.getAttributes());
970      }
971      else
972      {
973        final List<Attribute> providedAttrs = request.getAttributes();
974        final List<Attribute> newAttrs = new ArrayList<>(providedAttrs.size());
975        for (final Attribute a : providedAttrs)
976        {
977          final String baseName = a.getBaseName();
978          final MatchingRule matchingRule =
979               MatchingRule.selectEqualityMatchingRule(baseName, schema);
980          newAttrs.add(new Attribute(a.getName(), matchingRule,
981               a.getRawValues()));
982        }
983
984        entry = new Entry(request.getDN(), schema, newAttrs);
985      }
986
987      // Make sure that the DN is valid.
988      final DN dn;
989      try
990      {
991        dn = entry.getParsedDN();
992      }
993      catch (final LDAPException le)
994      {
995        Debug.debugException(le);
996        return new LDAPMessage(messageID, new AddResponseProtocolOp(
997             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
998             ERR_MEM_HANDLER_ADD_MALFORMED_DN.get(request.getDN(),
999                  le.getMessage()),
1000             null));
1001      }
1002
1003      // See if the DN is the null DN, the schema entry DN, or a changelog
1004      // entry.
1005      if (dn.isNullDN())
1006      {
1007        return new LDAPMessage(messageID, new AddResponseProtocolOp(
1008             ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null,
1009             ERR_MEM_HANDLER_ADD_ROOT_DSE.get(), null));
1010      }
1011      else if (dn.isDescendantOf(subschemaSubentryDN, true))
1012      {
1013        return new LDAPMessage(messageID, new AddResponseProtocolOp(
1014             ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null,
1015             ERR_MEM_HANDLER_ADD_SCHEMA.get(subschemaSubentryDN.toString()),
1016             null));
1017      }
1018      else if (dn.isDescendantOf(changeLogBaseDN, true))
1019      {
1020        return new LDAPMessage(messageID, new AddResponseProtocolOp(
1021             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1022             ERR_MEM_HANDLER_ADD_CHANGELOG.get(changeLogBaseDN.toString()),
1023             null));
1024      }
1025
1026      // See if there is a referral at or above the target entry.
1027      if (! controlMap.containsKey(
1028           ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID))
1029      {
1030        final Entry referralEntry = findNearestReferral(dn);
1031        if (referralEntry != null)
1032        {
1033          return new LDAPMessage(messageID, new AddResponseProtocolOp(
1034               ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(),
1035               INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(),
1036               getReferralURLs(dn, referralEntry)));
1037        }
1038      }
1039
1040      // See if another entry exists with the same DN.
1041      if (entryMap.containsKey(dn))
1042      {
1043        return new LDAPMessage(messageID, new AddResponseProtocolOp(
1044             ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null,
1045             ERR_MEM_HANDLER_ADD_ALREADY_EXISTS.get(request.getDN()), null));
1046      }
1047
1048      // Make sure that all RDN attribute values are present in the entry.
1049      final RDN      rdn           = dn.getRDN();
1050      final String[] rdnAttrNames  = rdn.getAttributeNames();
1051      final byte[][] rdnAttrValues = rdn.getByteArrayAttributeValues();
1052      for (int i=0; i < rdnAttrNames.length; i++)
1053      {
1054        final MatchingRule matchingRule =
1055             MatchingRule.selectEqualityMatchingRule(rdnAttrNames[i], schema);
1056        entry.addAttribute(new Attribute(rdnAttrNames[i], matchingRule,
1057             rdnAttrValues[i]));
1058      }
1059
1060      // Make sure that all superior object classes are present in the entry.
1061      if (schema != null)
1062      {
1063        final String[] objectClasses = entry.getObjectClassValues();
1064        if (objectClasses != null)
1065        {
1066          final LinkedHashMap<String,String> ocMap = new LinkedHashMap<>(
1067               StaticUtils.computeMapCapacity(objectClasses.length));
1068          for (final String ocName : objectClasses)
1069          {
1070            final ObjectClassDefinition oc = schema.getObjectClass(ocName);
1071            if (oc == null)
1072            {
1073              ocMap.put(StaticUtils.toLowerCase(ocName), ocName);
1074            }
1075            else
1076            {
1077              ocMap.put(StaticUtils.toLowerCase(oc.getNameOrOID()), ocName);
1078              for (final ObjectClassDefinition supClass :
1079                   oc.getSuperiorClasses(schema, true))
1080              {
1081                ocMap.put(StaticUtils.toLowerCase(supClass.getNameOrOID()),
1082                     supClass.getNameOrOID());
1083              }
1084            }
1085          }
1086
1087          final String[] newObjectClasses = new String[ocMap.size()];
1088          ocMap.values().toArray(newObjectClasses);
1089          entry.setAttribute("objectClass", newObjectClasses);
1090        }
1091      }
1092
1093      // If a schema was provided, then make sure the entry complies with it.
1094      // Also make sure that there are no attributes marked with
1095      // NO-USER-MODIFICATION.
1096      final EntryValidator entryValidator = entryValidatorRef.get();
1097      if (entryValidator != null)
1098      {
1099        final ArrayList<String> invalidReasons = new ArrayList<>(1);
1100        if (! entryValidator.entryIsValid(entry, invalidReasons))
1101        {
1102          return new LDAPMessage(messageID, new AddResponseProtocolOp(
1103               ResultCode.OBJECT_CLASS_VIOLATION_INT_VALUE, null,
1104               ERR_MEM_HANDLER_ADD_VIOLATES_SCHEMA.get(request.getDN(),
1105                    StaticUtils.concatenateStrings(invalidReasons)), null));
1106        }
1107
1108        if ((! isInternalOp) && (schema != null) &&
1109            (! controlMap.containsKey(IgnoreNoUserModificationRequestControl.
1110                    IGNORE_NO_USER_MODIFICATION_REQUEST_OID)))
1111        {
1112          for (final Attribute a : entry.getAttributes())
1113          {
1114            final AttributeTypeDefinition at =
1115                 schema.getAttributeType(a.getBaseName());
1116            if ((at != null) && at.isNoUserModification())
1117            {
1118              return new LDAPMessage(messageID, new AddResponseProtocolOp(
1119                   ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null,
1120                   ERR_MEM_HANDLER_ADD_CONTAINS_NO_USER_MOD.get(request.getDN(),
1121                        a.getName()), null));
1122            }
1123          }
1124        }
1125      }
1126
1127      // If the entry contains a proxied authorization control, then process it.
1128      final DN authzDN;
1129      try
1130      {
1131        authzDN = handleProxiedAuthControl(controlMap);
1132      }
1133      catch (final LDAPException le)
1134      {
1135        Debug.debugException(le);
1136        return new LDAPMessage(messageID, new AddResponseProtocolOp(
1137             le.getResultCode().intValue(), null, le.getMessage(), null));
1138      }
1139
1140      // Add a number of operational attributes to the entry.
1141      if (generateOperationalAttributes)
1142      {
1143        final Date d = new Date();
1144        if (! entry.hasAttribute("entryDN"))
1145        {
1146          entry.addAttribute(new Attribute("entryDN",
1147               DistinguishedNameMatchingRule.getInstance(),
1148               dn.toNormalizedString()));
1149        }
1150        if (! entry.hasAttribute("entryUUID"))
1151        {
1152          entry.addAttribute(new Attribute("entryUUID",
1153               CryptoHelper.getRandomUUID().toString()));
1154        }
1155        if (! entry.hasAttribute("subschemaSubentry"))
1156        {
1157          entry.addAttribute(new Attribute("subschemaSubentry",
1158               DistinguishedNameMatchingRule.getInstance(),
1159               subschemaSubentryDN.toString()));
1160        }
1161        if (! entry.hasAttribute("creatorsName"))
1162        {
1163          entry.addAttribute(new Attribute("creatorsName",
1164               DistinguishedNameMatchingRule.getInstance(),
1165               authzDN.toString()));
1166        }
1167        if (! entry.hasAttribute("createTimestamp"))
1168        {
1169          entry.addAttribute(new Attribute("createTimestamp",
1170               GeneralizedTimeMatchingRule.getInstance(),
1171               StaticUtils.encodeGeneralizedTime(d)));
1172        }
1173        if (! entry.hasAttribute("modifiersName"))
1174        {
1175          entry.addAttribute(new Attribute("modifiersName",
1176               DistinguishedNameMatchingRule.getInstance(),
1177               authzDN.toString()));
1178        }
1179        if (! entry.hasAttribute("modifyTimestamp"))
1180        {
1181          entry.addAttribute(new Attribute("modifyTimestamp",
1182               GeneralizedTimeMatchingRule.getInstance(),
1183               StaticUtils.encodeGeneralizedTime(d)));
1184        }
1185      }
1186
1187      // If the request includes the assertion request control, then check it
1188      // now.
1189      try
1190      {
1191        handleAssertionRequestControl(controlMap, entry);
1192      }
1193      catch (final LDAPException le)
1194      {
1195        Debug.debugException(le);
1196        return new LDAPMessage(messageID, new AddResponseProtocolOp(
1197             le.getResultCode().intValue(), null, le.getMessage(), null));
1198      }
1199
1200      // See if the entry contains any passwords.  If so, then make sure their
1201      // values are properly encoded.
1202      if ((! passwordEncoders.isEmpty()) &&
1203          (! configuredPasswordAttributes.isEmpty()))
1204      {
1205        final ReadOnlyEntry readOnlyEntry =
1206             new ReadOnlyEntry(entry.duplicate());
1207        for (final String passwordAttribute : configuredPasswordAttributes)
1208        {
1209          for (final Attribute attr :
1210               readOnlyEntry.getAttributesWithOptions(passwordAttribute, null))
1211          {
1212            final ArrayList<byte[]> newValues = new ArrayList<>(attr.size());
1213            for (final ASN1OctetString value : attr.getRawValues())
1214            {
1215              try
1216              {
1217                newValues.add(encodeAddPassword(value, readOnlyEntry,
1218                     Collections.<Modification>emptyList()).getValue());
1219              }
1220              catch (final LDAPException le)
1221              {
1222                Debug.debugException(le);
1223                return new LDAPMessage(messageID, new AddResponseProtocolOp(
1224                     ResultCode.UNWILLING_TO_PERFORM_INT_VALUE,
1225                     le.getMatchedDN(), le.getMessage(), null));
1226              }
1227            }
1228
1229            final byte[][] newValuesArray = new byte[newValues.size()][];
1230            newValues.toArray(newValuesArray);
1231            entry.setAttribute(new Attribute(attr.getName(), schema,
1232                 newValuesArray));
1233          }
1234        }
1235      }
1236
1237      // If the request includes the post-read request control, then create the
1238      // appropriate response control.
1239      final PostReadResponseControl postReadResponse =
1240           handlePostReadControl(controlMap, entry);
1241      if (postReadResponse != null)
1242      {
1243        responseControls.add(postReadResponse);
1244      }
1245
1246      // See if the entry DN is one of the defined base DNs.  If so, then we can
1247      // add the entry.
1248      if (baseDNs.contains(dn))
1249      {
1250        entryMap.put(dn, new ReadOnlyEntry(entry));
1251        indexAdd(entry);
1252        addChangeLogEntry(request, authzDN);
1253        return new LDAPMessage(messageID,
1254             new AddResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null,
1255                  null),
1256             responseControls);
1257      }
1258
1259      // See if the parent entry exists.  If so, then we can add the entry.
1260      final DN parentDN = dn.getParent();
1261      if ((parentDN != null) && entryMap.containsKey(parentDN))
1262      {
1263        entryMap.put(dn, new ReadOnlyEntry(entry));
1264        indexAdd(entry);
1265        addChangeLogEntry(request, authzDN);
1266        return new LDAPMessage(messageID,
1267             new AddResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null, null,
1268                  null),
1269             responseControls);
1270      }
1271
1272      // The add attempt must fail because the parent doesn't exist.  See if
1273      // it's just that the parent doesn't exist or whether the entry isn't
1274      // within any of the configured base DNs.
1275      for (final DN baseDN : baseDNs)
1276      {
1277        if (dn.isDescendantOf(baseDN, true))
1278        {
1279          return new LDAPMessage(messageID, new AddResponseProtocolOp(
1280               ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn),
1281               ERR_MEM_HANDLER_ADD_MISSING_PARENT.get(request.getDN(),
1282                    dn.getParentString()),
1283               null));
1284        }
1285      }
1286
1287      return new LDAPMessage(messageID, new AddResponseProtocolOp(
1288           ResultCode.NO_SUCH_OBJECT_INT_VALUE, null,
1289           ERR_MEM_HANDLER_ADD_NOT_BELOW_BASE_DN.get(request.getDN()),
1290           null));
1291    }
1292  }
1293
1294
1295
1296  /**
1297   * Encodes the provided password as appropriate.
1298   *
1299   * @param  password  The password to be encoded.
1300   * @param  entry     The entry in which the password occurs.
1301   * @param  mods      A list of modifications being applied to the entry, or
1302   *                   an empty list if there are no modifications.
1303   *
1304   * @return  The encoded password.
1305   *
1306   * @throws  LDAPException  If a problem is encountered while encoding the
1307   *                         password.
1308   */
1309  @NotNull()
1310  private ASN1OctetString encodeAddPassword(
1311                               @NotNull final ASN1OctetString password,
1312                               @NotNull final ReadOnlyEntry entry,
1313                               @NotNull final List<Modification> mods)
1314          throws LDAPException
1315  {
1316    for (final InMemoryPasswordEncoder encoder : passwordEncoders)
1317    {
1318      if (encoder.passwordStartsWithPrefix(password))
1319      {
1320        encoder.ensurePreEncodedPasswordAppearsValid(password, entry, mods);
1321        return password;
1322      }
1323    }
1324
1325    if (primaryPasswordEncoder != null)
1326    {
1327      return primaryPasswordEncoder.encodePassword(password, entry, mods);
1328    }
1329    else
1330    {
1331      return password;
1332    }
1333  }
1334
1335
1336
1337  /**
1338   * Attempts to process the provided bind request.  The attempt will fail if
1339   * any of the following conditions is true:
1340   * <UL>
1341   *   <LI>There is a problem with any of the request controls.</LI>
1342   *   <LI>The bind request is for a SASL bind for which no SASL mechanism
1343   *       handler is defined.</LI>
1344   *   <LI>The bind request contains a malformed bind DN.</LI>
1345   *   <LI>The bind DN is not the null DN and is not the DN of any entry in the
1346   *       data set.</LI>
1347   *   <LI>The bind password is empty and the bind DN is not the null DN.</LI>
1348   *   <LI>The target user does not have any password value that matches the
1349   *       provided bind password.</LI>
1350   * </UL>
1351   *
1352   * @param  messageID  The message ID of the LDAP message containing the bind
1353   *                    request.
1354   * @param  request    The bind request that was included in the LDAP message
1355   *                    that was received.
1356   * @param  controls   The set of controls included in the LDAP message.  It
1357   *                    may be empty if there were no controls, but will not be
1358   *                    {@code null}.
1359   *
1360   * @return  The {@link LDAPMessage} containing the response to send to the
1361   *          client.  The protocol op in the {@code LDAPMessage} must be a
1362   *          {@code BindResponseProtocolOp}.
1363   */
1364  @Override()
1365  @NotNull()
1366  public LDAPMessage processBindRequest(final int messageID,
1367                          @NotNull final BindRequestProtocolOp request,
1368                          @NotNull final List<Control> controls)
1369  {
1370    synchronized (entryMap)
1371    {
1372      // Sleep before processing, if appropriate.
1373      sleepBeforeProcessing();
1374
1375      // If this operation type is not allowed, then reject it.
1376      if (! config.getAllowedOperationTypes().contains(OperationType.BIND))
1377      {
1378        return new LDAPMessage(messageID, new BindResponseProtocolOp(
1379             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1380             ERR_MEM_HANDLER_BIND_NOT_ALLOWED.get(), null, null));
1381      }
1382
1383
1384      authenticatedDN = DN.NULL_DN;
1385
1386
1387      // If this operation type requires authentication and it is a simple bind
1388      // request, then ensure that the request includes credentials.
1389      if ((authenticatedDN.isNullDN() &&
1390           config.getAuthenticationRequiredOperationTypes().contains(
1391                OperationType.BIND)))
1392      {
1393        if ((request.getCredentialsType() ==
1394             BindRequestProtocolOp.CRED_TYPE_SIMPLE) &&
1395             ((request.getSimplePassword() == null) ||
1396                  request.getSimplePassword().getValueLength() == 0))
1397        {
1398          return new LDAPMessage(messageID, new BindResponseProtocolOp(
1399               ResultCode.INVALID_CREDENTIALS_INT_VALUE, null,
1400               ERR_MEM_HANDLER_BIND_REQUIRES_AUTH.get(), null, null));
1401        }
1402      }
1403
1404
1405      // Get the parsed bind DN.
1406      final DN bindDN;
1407      try
1408      {
1409        bindDN = new DN(request.getBindDN(), schemaRef.get());
1410      }
1411      catch (final LDAPException le)
1412      {
1413        Debug.debugException(le);
1414        return new LDAPMessage(messageID, new BindResponseProtocolOp(
1415             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
1416             ERR_MEM_HANDLER_BIND_MALFORMED_DN.get(request.getBindDN(),
1417                  le.getMessage()),
1418             null, null));
1419      }
1420
1421      // If the bind request is for a SASL bind, then see if there is a SASL
1422      // mechanism handler that can be used to process it.
1423      if (request.getCredentialsType() == BindRequestProtocolOp.CRED_TYPE_SASL)
1424      {
1425        final String mechanism = request.getSASLMechanism();
1426        final InMemorySASLBindHandler handler = saslBindHandlers.get(mechanism);
1427        if (handler == null)
1428        {
1429          return new LDAPMessage(messageID, new BindResponseProtocolOp(
1430               ResultCode.AUTH_METHOD_NOT_SUPPORTED_INT_VALUE, null,
1431               ERR_MEM_HANDLER_SASL_MECH_NOT_SUPPORTED.get(mechanism), null,
1432               null));
1433        }
1434
1435        try
1436        {
1437          final BindResult bindResult = handler.processSASLBind(this, messageID,
1438               bindDN, request.getSASLCredentials(), controls);
1439
1440          // If the SASL bind was successful but the connection is
1441          // unauthenticated, then see if we allow that.
1442          if ((bindResult.getResultCode() == ResultCode.SUCCESS) &&
1443               (authenticatedDN == DN.NULL_DN) &&
1444               config.getAuthenticationRequiredOperationTypes().contains(
1445                    OperationType.BIND))
1446          {
1447            return new LDAPMessage(messageID, new BindResponseProtocolOp(
1448                 ResultCode.INVALID_CREDENTIALS_INT_VALUE, null,
1449                 ERR_MEM_HANDLER_BIND_REQUIRES_AUTH.get(), null, null));
1450          }
1451
1452          return new LDAPMessage(messageID, new BindResponseProtocolOp(
1453               bindResult.getResultCode().intValue(),
1454               bindResult.getMatchedDN(), bindResult.getDiagnosticMessage(),
1455               Arrays.asList(bindResult.getReferralURLs()),
1456               bindResult.getServerSASLCredentials()),
1457               Arrays.asList(bindResult.getResponseControls()));
1458        }
1459        catch (final Exception e)
1460        {
1461          Debug.debugException(e);
1462          return new LDAPMessage(messageID, new BindResponseProtocolOp(
1463               ResultCode.OTHER_INT_VALUE, null,
1464               ERR_MEM_HANDLER_SASL_BIND_FAILURE.get(
1465                    StaticUtils.getExceptionMessage(e)),
1466               null, null));
1467        }
1468      }
1469
1470      // If we've gotten here, then the bind must use simple authentication.
1471      // Process the provided request controls.
1472      final Map<String,Control> controlMap;
1473      try
1474      {
1475        controlMap = RequestControlPreProcessor.processControls(
1476             LDAPMessage.PROTOCOL_OP_TYPE_BIND_REQUEST, controls);
1477      }
1478      catch (final LDAPException le)
1479      {
1480        Debug.debugException(le);
1481        return new LDAPMessage(messageID, new BindResponseProtocolOp(
1482             le.getResultCode().intValue(), null, le.getMessage(), null, null));
1483      }
1484      final ArrayList<Control> responseControls = new ArrayList<>(1);
1485
1486      // If the bind DN is the null DN, then the bind will be considered
1487      // successful as long as the password is also empty.
1488      final ASN1OctetString bindPassword = request.getSimplePassword();
1489      if (bindDN.isNullDN())
1490      {
1491        if (bindPassword.getValueLength() == 0)
1492        {
1493          if (controlMap.containsKey(AuthorizationIdentityRequestControl.
1494               AUTHORIZATION_IDENTITY_REQUEST_OID))
1495          {
1496            responseControls.add(new AuthorizationIdentityResponseControl(""));
1497          }
1498          return new LDAPMessage(messageID,
1499               new BindResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
1500                    null, null, null),
1501               responseControls);
1502        }
1503        else
1504        {
1505          return new LDAPMessage(messageID, new BindResponseProtocolOp(
1506               ResultCode.INVALID_CREDENTIALS_INT_VALUE, null,
1507               ERR_MEM_HANDLER_BIND_WRONG_PASSWORD.get(request.getBindDN()),
1508               null, null));
1509        }
1510      }
1511
1512      // If the bind DN is not null and the password is empty, then reject the
1513      // request.
1514      if ((! bindDN.isNullDN()) && (bindPassword.getValueLength() == 0))
1515      {
1516        return new LDAPMessage(messageID, new BindResponseProtocolOp(
1517             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1518             ERR_MEM_HANDLER_BIND_SIMPLE_DN_WITHOUT_PASSWORD.get(), null,
1519             null));
1520      }
1521
1522      // See if the bind DN is in the set of additional bind credentials.  If
1523      // so, then use the password there.
1524      final byte[] additionalCreds = additionalBindCredentials.get(bindDN);
1525      if (additionalCreds != null)
1526      {
1527        if (Arrays.equals(additionalCreds, bindPassword.getValue()))
1528        {
1529          authenticatedDN = bindDN;
1530          if (controlMap.containsKey(AuthorizationIdentityRequestControl.
1531               AUTHORIZATION_IDENTITY_REQUEST_OID))
1532          {
1533            responseControls.add(new AuthorizationIdentityResponseControl(
1534                 "dn:" + bindDN.toString()));
1535          }
1536          return new LDAPMessage(messageID,
1537               new BindResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
1538                    null, null, null),
1539               responseControls);
1540        }
1541        else
1542        {
1543          return new LDAPMessage(messageID, new BindResponseProtocolOp(
1544               ResultCode.INVALID_CREDENTIALS_INT_VALUE, null,
1545               ERR_MEM_HANDLER_BIND_WRONG_PASSWORD.get(request.getBindDN()),
1546               null, null));
1547        }
1548      }
1549
1550      // If the target user doesn't exist, then reject the request.
1551      final ReadOnlyEntry userEntry = entryMap.get(bindDN);
1552      if (userEntry == null)
1553      {
1554        return new LDAPMessage(messageID, new BindResponseProtocolOp(
1555             ResultCode.INVALID_CREDENTIALS_INT_VALUE, null,
1556             ERR_MEM_HANDLER_BIND_NO_SUCH_USER.get(request.getBindDN()), null,
1557             null));
1558      }
1559
1560
1561      // Get a list of the user's passwords, restricted to those that match the
1562      // provided clear-text password.  If the list is empty, then the
1563      // authentication failed.
1564      final List<InMemoryDirectoryServerPassword> matchingPasswords =
1565           getPasswordsInEntry(userEntry, bindPassword);
1566      if (matchingPasswords.isEmpty())
1567      {
1568        return new LDAPMessage(messageID, new BindResponseProtocolOp(
1569             ResultCode.INVALID_CREDENTIALS_INT_VALUE, null,
1570             ERR_MEM_HANDLER_BIND_WRONG_PASSWORD.get(request.getBindDN()), null,
1571             null));
1572      }
1573
1574
1575      // If we've gotten here, then authentication was successful.
1576      authenticatedDN = bindDN;
1577      if (controlMap.containsKey(AuthorizationIdentityRequestControl.
1578           AUTHORIZATION_IDENTITY_REQUEST_OID))
1579      {
1580        responseControls.add(new AuthorizationIdentityResponseControl(
1581             "dn:" + bindDN.toString()));
1582      }
1583      return new LDAPMessage(messageID,
1584           new BindResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
1585                null, null, null),
1586           responseControls);
1587    }
1588  }
1589
1590
1591
1592  /**
1593   * Attempts to process the provided compare request.  The attempt will fail if
1594   * any of the following conditions is true:
1595   * <UL>
1596   *   <LI>There is a problem with any of the request controls.</LI>
1597   *   <LI>The compare request contains a malformed target DN.</LI>
1598   *   <LI>The target entry does not exist.</LI>
1599   * </UL>
1600   *
1601   * @param  messageID  The message ID of the LDAP message containing the
1602   *                    compare request.
1603   * @param  request    The compare request that was included in the LDAP
1604   *                    message that was received.
1605   * @param  controls   The set of controls included in the LDAP message.  It
1606   *                    may be empty if there were no controls, but will not be
1607   *                    {@code null}.
1608   *
1609   * @return  The {@link LDAPMessage} containing the response to send to the
1610   *          client.  The protocol op in the {@code LDAPMessage} must be a
1611   *          {@code CompareResponseProtocolOp}.
1612   */
1613  @Override()
1614  @NotNull()
1615  public LDAPMessage processCompareRequest(final int messageID,
1616                          @NotNull final CompareRequestProtocolOp request,
1617                          @NotNull final List<Control> controls)
1618  {
1619    synchronized (entryMap)
1620    {
1621      // Sleep before processing, if appropriate.
1622      sleepBeforeProcessing();
1623
1624      // Process the provided request controls.
1625      final Map<String,Control> controlMap;
1626      try
1627      {
1628        controlMap = RequestControlPreProcessor.processControls(
1629             LDAPMessage.PROTOCOL_OP_TYPE_COMPARE_REQUEST, controls);
1630      }
1631      catch (final LDAPException le)
1632      {
1633        Debug.debugException(le);
1634        return new LDAPMessage(messageID, new CompareResponseProtocolOp(
1635             le.getResultCode().intValue(), null, le.getMessage(), null));
1636      }
1637      final ArrayList<Control> responseControls = new ArrayList<>(1);
1638
1639
1640      // If this operation type is not allowed, then reject it.
1641      final boolean isInternalOp =
1642           controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL);
1643      if ((! isInternalOp) &&
1644           (! config.getAllowedOperationTypes().contains(
1645                OperationType.COMPARE)))
1646      {
1647        return new LDAPMessage(messageID, new CompareResponseProtocolOp(
1648             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1649             ERR_MEM_HANDLER_COMPARE_NOT_ALLOWED.get(), null));
1650      }
1651
1652
1653      // If this operation type requires authentication, then ensure that the
1654      // client is authenticated.
1655      if ((authenticatedDN.isNullDN() &&
1656           config.getAuthenticationRequiredOperationTypes().contains(
1657                OperationType.COMPARE)))
1658      {
1659        return new LDAPMessage(messageID, new CompareResponseProtocolOp(
1660             ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null,
1661             ERR_MEM_HANDLER_COMPARE_REQUIRES_AUTH.get(), null));
1662      }
1663
1664
1665      // Get the parsed target DN.
1666      final DN dn;
1667      try
1668      {
1669        dn = new DN(request.getDN(), schemaRef.get());
1670      }
1671      catch (final LDAPException le)
1672      {
1673        Debug.debugException(le);
1674        return new LDAPMessage(messageID, new CompareResponseProtocolOp(
1675             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
1676             ERR_MEM_HANDLER_COMPARE_MALFORMED_DN.get(request.getDN(),
1677                  le.getMessage()),
1678             null));
1679      }
1680
1681      // See if the target entry or one of its superiors is a smart referral.
1682      if (! controlMap.containsKey(
1683           ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID))
1684      {
1685        final Entry referralEntry = findNearestReferral(dn);
1686        if (referralEntry != null)
1687        {
1688          return new LDAPMessage(messageID, new CompareResponseProtocolOp(
1689               ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(),
1690               INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(),
1691               getReferralURLs(dn, referralEntry)));
1692        }
1693      }
1694
1695      // Get the target entry (optionally checking for the root DSE or subschema
1696      // subentry).  If it does not exist, then fail.
1697      final Entry entry;
1698      if (dn.isNullDN())
1699      {
1700        entry = generateRootDSE();
1701      }
1702      else if (dn.equals(subschemaSubentryDN))
1703      {
1704        entry = subschemaSubentryRef.get();
1705      }
1706      else
1707      {
1708        entry = entryMap.get(dn);
1709      }
1710      if (entry == null)
1711      {
1712        return new LDAPMessage(messageID, new CompareResponseProtocolOp(
1713             ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn),
1714             ERR_MEM_HANDLER_COMPARE_NO_SUCH_ENTRY.get(request.getDN()), null));
1715      }
1716
1717      // If the request includes an assertion or proxied authorization control,
1718      // then perform the appropriate processing.
1719      try
1720      {
1721        handleAssertionRequestControl(controlMap, entry);
1722        handleProxiedAuthControl(controlMap);
1723      }
1724      catch (final LDAPException le)
1725      {
1726        Debug.debugException(le);
1727        return new LDAPMessage(messageID, new CompareResponseProtocolOp(
1728             le.getResultCode().intValue(), null, le.getMessage(), null));
1729      }
1730
1731      // See if the entry contains the assertion value.
1732      final int resultCode;
1733      if (entry.hasAttributeValue(request.getAttributeName(),
1734           request.getAssertionValue().getValue()))
1735      {
1736        resultCode = ResultCode.COMPARE_TRUE_INT_VALUE;
1737      }
1738      else
1739      {
1740        resultCode = ResultCode.COMPARE_FALSE_INT_VALUE;
1741      }
1742      return new LDAPMessage(messageID,
1743           new CompareResponseProtocolOp(resultCode, null, null, null),
1744           responseControls);
1745    }
1746  }
1747
1748
1749
1750  /**
1751   * Processes the provided delete request.
1752   * <BR><BR>
1753   * This method may be used regardless of whether the server is listening for
1754   * client connections, and regardless of whether delete operations are
1755   * allowed in the server.
1756   *
1757   * @param  deleteRequest  The delete request to be processed.  It must not be
1758   *                        {@code null}.
1759   *
1760   * @return  The result of processing the delete operation.
1761   *
1762   * @throws  LDAPException  If the server rejects the delete request, or if a
1763   *                         problem is encountered while sending the request or
1764   *                         reading the response.
1765   */
1766  @NotNull()
1767  public LDAPResult delete(@NotNull final DeleteRequest deleteRequest)
1768         throws LDAPException
1769  {
1770    final ArrayList<Control> requestControlList =
1771         new ArrayList<>(deleteRequest.getControlList());
1772    requestControlList.add(new Control(OID_INTERNAL_OPERATION_REQUEST_CONTROL,
1773         false));
1774
1775    final LDAPMessage responseMessage = processDeleteRequest(1,
1776         new DeleteRequestProtocolOp(deleteRequest.getDN()),
1777         requestControlList);
1778
1779    final DeleteResponseProtocolOp deleteResponse =
1780         responseMessage.getDeleteResponseProtocolOp();
1781
1782    final LDAPResult ldapResult = new LDAPResult(responseMessage.getMessageID(),
1783         ResultCode.valueOf(deleteResponse.getResultCode()),
1784         deleteResponse.getDiagnosticMessage(), deleteResponse.getMatchedDN(),
1785         deleteResponse.getReferralURLs(), responseMessage.getControls());
1786
1787    switch (deleteResponse.getResultCode())
1788    {
1789      case ResultCode.SUCCESS_INT_VALUE:
1790      case ResultCode.NO_OPERATION_INT_VALUE:
1791        return ldapResult;
1792      default:
1793        throw new LDAPException(ldapResult);
1794    }
1795  }
1796
1797
1798
1799  /**
1800   * Attempts to process the provided delete request.  The attempt will fail if
1801   * any of the following conditions is true:
1802   * <UL>
1803   *   <LI>There is a problem with any of the request controls.</LI>
1804   *   <LI>The delete request contains a malformed target DN.</LI>
1805   *   <LI>The target entry is the root DSE.</LI>
1806   *   <LI>The target entry is the subschema subentry.</LI>
1807   *   <LI>The target entry is at or below the changelog base entry.</LI>
1808   *   <LI>The target entry does not exist.</LI>
1809   *   <LI>The target entry has one or more subordinate entries.</LI>
1810   * </UL>
1811   *
1812   * @param  messageID  The message ID of the LDAP message containing the delete
1813   *                    request.
1814   * @param  request    The delete request that was included in the LDAP message
1815   *                    that was received.
1816   * @param  controls   The set of controls included in the LDAP message.  It
1817   *                    may be empty if there were no controls, but will not be
1818   *                    {@code null}.
1819   *
1820   * @return  The {@link LDAPMessage} containing the response to send to the
1821   *          client.  The protocol op in the {@code LDAPMessage} must be a
1822   *          {@code DeleteResponseProtocolOp}.
1823   */
1824  @Override()
1825  @NotNull()
1826  public LDAPMessage processDeleteRequest(final int messageID,
1827                          @NotNull final DeleteRequestProtocolOp request,
1828                          @NotNull final List<Control> controls)
1829  {
1830    synchronized (entryMap)
1831    {
1832      // Sleep before processing, if appropriate.
1833      sleepBeforeProcessing();
1834
1835      // Process the provided request controls.
1836      final Map<String,Control> controlMap;
1837      try
1838      {
1839        controlMap = RequestControlPreProcessor.processControls(
1840             LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, controls);
1841      }
1842      catch (final LDAPException le)
1843      {
1844        Debug.debugException(le);
1845        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1846             le.getResultCode().intValue(), null, le.getMessage(), null));
1847      }
1848      final ArrayList<Control> responseControls = new ArrayList<>(1);
1849
1850
1851      // If this operation type is not allowed, then reject it.
1852      final boolean isInternalOp =
1853           controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL);
1854      if ((! isInternalOp) &&
1855           (! config.getAllowedOperationTypes().contains(OperationType.DELETE)))
1856      {
1857        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1858             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1859             ERR_MEM_HANDLER_DELETE_NOT_ALLOWED.get(), null));
1860      }
1861
1862
1863      // If this operation type requires authentication, then ensure that the
1864      // client is authenticated.
1865      if ((authenticatedDN.isNullDN() &&
1866           config.getAuthenticationRequiredOperationTypes().contains(
1867                OperationType.DELETE)))
1868      {
1869        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1870             ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null,
1871             ERR_MEM_HANDLER_DELETE_REQUIRES_AUTH.get(), null));
1872      }
1873
1874
1875      // See if this delete request is part of a transaction.  If so, then
1876      // perform appropriate processing for it and return success immediately
1877      // without actually doing any further processing.
1878      try
1879      {
1880        final ASN1OctetString txnID =
1881             processTransactionRequest(messageID, request, controlMap);
1882        if (txnID != null)
1883        {
1884          return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1885               ResultCode.SUCCESS_INT_VALUE, null,
1886               INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null));
1887        }
1888      }
1889      catch (final LDAPException le)
1890      {
1891        Debug.debugException(le);
1892        return new LDAPMessage(messageID,
1893             new DeleteResponseProtocolOp(le.getResultCode().intValue(),
1894                  le.getMatchedDN(), le.getDiagnosticMessage(),
1895                  StaticUtils.toList(le.getReferralURLs())),
1896             le.getResponseControls());
1897      }
1898
1899
1900      // Get the parsed target DN.
1901      final DN dn;
1902      try
1903      {
1904        dn = new DN(request.getDN(), schemaRef.get());
1905      }
1906      catch (final LDAPException le)
1907      {
1908        Debug.debugException(le);
1909        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1910             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
1911             ERR_MEM_HANDLER_DELETE_MALFORMED_DN.get(request.getDN(),
1912                  le.getMessage()),
1913             null));
1914      }
1915
1916      // See if the target entry or one of its superiors is a smart referral.
1917      if (! controlMap.containsKey(
1918           ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID))
1919      {
1920        final Entry referralEntry = findNearestReferral(dn);
1921        if (referralEntry != null)
1922        {
1923          return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1924               ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(),
1925               INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(),
1926               getReferralURLs(dn, referralEntry)));
1927        }
1928      }
1929
1930      // Make sure the target entry isn't the root DSE or schema, or a changelog
1931      // entry.
1932      if (dn.isNullDN())
1933      {
1934        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1935             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1936             ERR_MEM_HANDLER_DELETE_ROOT_DSE.get(), null));
1937      }
1938      else if (dn.equals(subschemaSubentryDN))
1939      {
1940        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1941             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1942             ERR_MEM_HANDLER_DELETE_SCHEMA.get(subschemaSubentryDN.toString()),
1943             null));
1944      }
1945      else if (dn.isDescendantOf(changeLogBaseDN, true))
1946      {
1947        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1948             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
1949             ERR_MEM_HANDLER_DELETE_CHANGELOG.get(request.getDN()), null));
1950      }
1951
1952      // Get the target entry.  If it does not exist, then fail.
1953      final Entry entry = entryMap.get(dn);
1954      if (entry == null)
1955      {
1956        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1957             ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn),
1958             ERR_MEM_HANDLER_DELETE_NO_SUCH_ENTRY.get(request.getDN()), null));
1959      }
1960
1961      // Create a list with the DN of the target entry, and all the DNs of its
1962      // subordinates.  If the entry has subordinates and the subtree delete
1963      // control was not provided, then fail.
1964      final ArrayList<DN> subordinateDNs = new ArrayList<>(entryMap.size());
1965      for (final DN mapEntryDN : entryMap.keySet())
1966      {
1967        if (mapEntryDN.isDescendantOf(dn, false))
1968        {
1969          subordinateDNs.add(mapEntryDN);
1970        }
1971      }
1972
1973      if ((! subordinateDNs.isEmpty()) &&
1974           (! controlMap.containsKey(
1975                SubtreeDeleteRequestControl.SUBTREE_DELETE_REQUEST_OID)))
1976      {
1977        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
1978             ResultCode.NOT_ALLOWED_ON_NONLEAF_INT_VALUE, null,
1979             ERR_MEM_HANDLER_DELETE_HAS_SUBORDINATES.get(request.getDN()),
1980             null));
1981      }
1982
1983      // Handle the necessary processing for the assertion, pre-read, and
1984      // proxied auth controls.
1985      final DN authzDN;
1986      try
1987      {
1988        handleAssertionRequestControl(controlMap, entry);
1989
1990        final PreReadResponseControl preReadResponse =
1991             handlePreReadControl(controlMap, entry);
1992        if (preReadResponse != null)
1993        {
1994          responseControls.add(preReadResponse);
1995        }
1996
1997        authzDN = handleProxiedAuthControl(controlMap);
1998      }
1999      catch (final LDAPException le)
2000      {
2001        Debug.debugException(le);
2002        return new LDAPMessage(messageID, new DeleteResponseProtocolOp(
2003             le.getResultCode().intValue(), null, le.getMessage(), null));
2004      }
2005
2006      // At this point, the entry will be removed.  However, if this will be a
2007      // subtree delete, then we want to delete all of its subordinates first so
2008      // that the changelog will show the deletes in the appropriate order.
2009      for (int i=(subordinateDNs.size() - 1); i >= 0; i--)
2010      {
2011        final DN subordinateDN = subordinateDNs.get(i);
2012        final Entry subEntry = entryMap.remove(subordinateDN);
2013        indexDelete(subEntry);
2014        addDeleteChangeLogEntry(subEntry, authzDN);
2015        handleReferentialIntegrityDelete(subordinateDN);
2016      }
2017
2018      // Finally, remove the target entry and create a changelog entry for it.
2019      entryMap.remove(dn);
2020      indexDelete(entry);
2021      addDeleteChangeLogEntry(entry, authzDN);
2022      handleReferentialIntegrityDelete(dn);
2023
2024      return new LDAPMessage(messageID,
2025           new DeleteResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
2026                null, null),
2027           responseControls);
2028    }
2029  }
2030
2031
2032
2033  /**
2034   * Handles any appropriate referential integrity processing for a delete
2035   * operation.
2036   *
2037   * @param  dn  The DN of the entry that has been deleted.
2038   */
2039  private void handleReferentialIntegrityDelete(@NotNull final DN dn)
2040  {
2041    if (referentialIntegrityAttributes.isEmpty())
2042    {
2043      return;
2044    }
2045
2046    final ArrayList<DN> entryDNs = new ArrayList<>(entryMap.keySet());
2047    for (final DN mapDN : entryDNs)
2048    {
2049      final ReadOnlyEntry e = entryMap.get(mapDN);
2050
2051      boolean referenceFound = false;
2052      final Schema schema = schemaRef.get();
2053      for (final String attrName : referentialIntegrityAttributes)
2054      {
2055        final Attribute a = e.getAttribute(attrName, schema);
2056        if ((a != null) &&
2057            a.hasValue(dn.toNormalizedString(),
2058                 DistinguishedNameMatchingRule.getInstance()))
2059        {
2060          referenceFound = true;
2061          break;
2062        }
2063      }
2064
2065      if (referenceFound)
2066      {
2067        final Entry copy = e.duplicate();
2068        for (final String attrName : referentialIntegrityAttributes)
2069        {
2070          copy.removeAttributeValue(attrName, dn.toNormalizedString(),
2071               DistinguishedNameMatchingRule.getInstance());
2072        }
2073        entryMap.put(mapDN, new ReadOnlyEntry(copy));
2074        indexDelete(e);
2075        indexAdd(copy);
2076      }
2077    }
2078  }
2079
2080
2081
2082  /**
2083   * Attempts to process the provided extended request, if an extended operation
2084   * handler is defined for the given request OID.
2085   *
2086   * @param  messageID  The message ID of the LDAP message containing the
2087   *                    extended request.
2088   * @param  request    The extended request that was included in the LDAP
2089   *                    message that was received.
2090   * @param  controls   The set of controls included in the LDAP message.  It
2091   *                    may be empty if there were no controls, but will not be
2092   *                    {@code null}.
2093   *
2094   * @return  The {@link LDAPMessage} containing the response to send to the
2095   *          client.  The protocol op in the {@code LDAPMessage} must be an
2096   *          {@code ExtendedResponseProtocolOp}.
2097   */
2098  @Override()
2099  @NotNull()
2100  public LDAPMessage processExtendedRequest(final int messageID,
2101                          @NotNull final ExtendedRequestProtocolOp request,
2102                          @NotNull final List<Control> controls)
2103  {
2104    synchronized (entryMap)
2105    {
2106      // Sleep before processing, if appropriate.
2107      sleepBeforeProcessing();
2108
2109      boolean isInternalOp = false;
2110      for (final Control c : controls)
2111      {
2112        if (c.getOID().equals(OID_INTERNAL_OPERATION_REQUEST_CONTROL))
2113        {
2114          isInternalOp = true;
2115          break;
2116        }
2117      }
2118
2119
2120      // If this operation type is not allowed, then reject it.
2121      if ((! isInternalOp) &&
2122           (! config.getAllowedOperationTypes().contains(
2123                OperationType.EXTENDED)))
2124      {
2125        return new LDAPMessage(messageID, new ExtendedResponseProtocolOp(
2126             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
2127             ERR_MEM_HANDLER_EXTENDED_NOT_ALLOWED.get(), null, null, null));
2128      }
2129
2130
2131      // If this operation type requires authentication, then ensure that the
2132      // client is authenticated.
2133      if ((authenticatedDN.isNullDN() &&
2134           config.getAuthenticationRequiredOperationTypes().contains(
2135                OperationType.EXTENDED)))
2136      {
2137        return new LDAPMessage(messageID, new ExtendedResponseProtocolOp(
2138             ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null,
2139             ERR_MEM_HANDLER_EXTENDED_REQUIRES_AUTH.get(), null, null, null));
2140      }
2141
2142
2143      final String oid = request.getOID();
2144      final InMemoryExtendedOperationHandler handler =
2145           extendedRequestHandlers.get(oid);
2146      if (handler == null)
2147      {
2148        return new LDAPMessage(messageID, new ExtendedResponseProtocolOp(
2149             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
2150             ERR_MEM_HANDLER_EXTENDED_OP_NOT_SUPPORTED.get(oid), null, null,
2151             null));
2152      }
2153
2154      try
2155      {
2156        final Control[] controlArray = new Control[controls.size()];
2157        controls.toArray(controlArray);
2158
2159        final ExtendedRequest extendedRequest = new ExtendedRequest(oid,
2160             request.getValue(), controlArray);
2161
2162        final ExtendedResult extendedResult =
2163             handler.processExtendedOperation(this, messageID, extendedRequest);
2164
2165        return new LDAPMessage(messageID,
2166             new ExtendedResponseProtocolOp(
2167                  extendedResult.getResultCode().intValue(),
2168                  extendedResult.getMatchedDN(),
2169                  extendedResult.getDiagnosticMessage(),
2170                  Arrays.asList(extendedResult.getReferralURLs()),
2171                  extendedResult.getOID(), extendedResult.getValue()),
2172             extendedResult.getResponseControls());
2173      }
2174      catch (final Exception e)
2175      {
2176        Debug.debugException(e);
2177
2178        return new LDAPMessage(messageID, new ExtendedResponseProtocolOp(
2179             ResultCode.OTHER_INT_VALUE, null,
2180             ERR_MEM_HANDLER_EXTENDED_OP_FAILURE.get(
2181                  StaticUtils.getExceptionMessage(e)),
2182             null, null, null));
2183      }
2184    }
2185  }
2186
2187
2188
2189  /**
2190   * Processes the provided modify request.
2191   * <BR><BR>
2192   * This method may be used regardless of whether the server is listening for
2193   * client connections, and regardless of whether modify operations are allowed
2194   * in the server.
2195   *
2196   * @param  modifyRequest  The modify request to be processed.  It must not be
2197   *                        {@code null}.
2198   *
2199   * @return  The result of processing the modify operation.
2200   *
2201   * @throws  LDAPException  If the server rejects the modify request, or if a
2202   *                         problem is encountered while sending the request or
2203   *                         reading the response.
2204   */
2205  @NotNull()
2206  public LDAPResult modify(@NotNull final ModifyRequest modifyRequest)
2207         throws LDAPException
2208  {
2209    final ArrayList<Control> requestControlList =
2210         new ArrayList<>(modifyRequest.getControlList());
2211    requestControlList.add(new Control(OID_INTERNAL_OPERATION_REQUEST_CONTROL,
2212         false));
2213
2214    final LDAPMessage responseMessage = processModifyRequest(1,
2215         new ModifyRequestProtocolOp(modifyRequest.getDN(),
2216              modifyRequest.getModifications()),
2217         requestControlList);
2218
2219    final ModifyResponseProtocolOp modifyResponse =
2220         responseMessage.getModifyResponseProtocolOp();
2221
2222    final LDAPResult ldapResult = new LDAPResult(responseMessage.getMessageID(),
2223         ResultCode.valueOf(modifyResponse.getResultCode()),
2224         modifyResponse.getDiagnosticMessage(), modifyResponse.getMatchedDN(),
2225         modifyResponse.getReferralURLs(), responseMessage.getControls());
2226
2227    switch (modifyResponse.getResultCode())
2228    {
2229      case ResultCode.SUCCESS_INT_VALUE:
2230      case ResultCode.NO_OPERATION_INT_VALUE:
2231        return ldapResult;
2232      default:
2233        throw new LDAPException(ldapResult);
2234    }
2235  }
2236
2237
2238
2239  /**
2240   * Attempts to process the provided modify request.  The attempt will fail if
2241   * any of the following conditions is true:
2242   * <UL>
2243   *   <LI>There is a problem with any of the request controls.</LI>
2244   *   <LI>The modify request contains a malformed target DN.</LI>
2245   *   <LI>The target entry is the root DSE.</LI>
2246   *   <LI>The target entry is the subschema subentry.</LI>
2247   *   <LI>The target entry does not exist.</LI>
2248   *   <LI>Any of the modifications cannot be applied to the entry.</LI>
2249   *   <LI>If a schema was provided, and the entry violates any of the
2250   *       constraints of that schema.</LI>
2251   * </UL>
2252   *
2253   * @param  messageID  The message ID of the LDAP message containing the modify
2254   *                    request.
2255   * @param  request    The modify request that was included in the LDAP message
2256   *                    that was received.
2257   * @param  controls   The set of controls included in the LDAP message.  It
2258   *                    may be empty if there were no controls, but will not be
2259   *                    {@code null}.
2260   *
2261   * @return  The {@link LDAPMessage} containing the response to send to the
2262   *          client.  The protocol op in the {@code LDAPMessage} must be an
2263   *          {@code ModifyResponseProtocolOp}.
2264   */
2265  @Override()
2266  @NotNull()
2267  public LDAPMessage processModifyRequest(final int messageID,
2268                          @NotNull final ModifyRequestProtocolOp request,
2269                          @NotNull final List<Control> controls)
2270  {
2271    synchronized (entryMap)
2272    {
2273      // Sleep before processing, if appropriate.
2274      sleepBeforeProcessing();
2275
2276      // Process the provided request controls.
2277      final Map<String,Control> controlMap;
2278      try
2279      {
2280        controlMap = RequestControlPreProcessor.processControls(
2281             LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST, controls);
2282      }
2283      catch (final LDAPException le)
2284      {
2285        Debug.debugException(le);
2286        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2287             le.getResultCode().intValue(), null, le.getMessage(), null));
2288      }
2289      final ArrayList<Control> responseControls = new ArrayList<>(1);
2290
2291
2292      // If this operation type is not allowed, then reject it.
2293      final boolean isInternalOp =
2294           controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL);
2295      if ((! isInternalOp) &&
2296           (! config.getAllowedOperationTypes().contains(OperationType.MODIFY)))
2297      {
2298        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2299             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
2300             ERR_MEM_HANDLER_MODIFY_NOT_ALLOWED.get(), null));
2301      }
2302
2303
2304      // If this operation type requires authentication, then ensure that the
2305      // client is authenticated.
2306      if ((authenticatedDN.isNullDN() &&
2307           config.getAuthenticationRequiredOperationTypes().contains(
2308                OperationType.MODIFY)))
2309      {
2310        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2311             ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null,
2312             ERR_MEM_HANDLER_MODIFY_REQUIRES_AUTH.get(), null));
2313      }
2314
2315
2316      // See if this modify request is part of a transaction.  If so, then
2317      // perform appropriate processing for it and return success immediately
2318      // without actually doing any further processing.
2319      try
2320      {
2321        final ASN1OctetString txnID =
2322             processTransactionRequest(messageID, request, controlMap);
2323        if (txnID != null)
2324        {
2325          return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2326               ResultCode.SUCCESS_INT_VALUE, null,
2327               INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null));
2328        }
2329      }
2330      catch (final LDAPException le)
2331      {
2332        Debug.debugException(le);
2333        return new LDAPMessage(messageID,
2334             new ModifyResponseProtocolOp(le.getResultCode().intValue(),
2335                  le.getMatchedDN(), le.getDiagnosticMessage(),
2336                  StaticUtils.toList(le.getReferralURLs())),
2337             le.getResponseControls());
2338      }
2339
2340
2341      // Get the parsed target DN.
2342      final DN dn;
2343      final Schema schema = schemaRef.get();
2344      try
2345      {
2346        dn = new DN(request.getDN(), schema);
2347      }
2348      catch (final LDAPException le)
2349      {
2350        Debug.debugException(le);
2351        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2352             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
2353             ERR_MEM_HANDLER_MOD_MALFORMED_DN.get(request.getDN(),
2354                  le.getMessage()),
2355             null));
2356      }
2357
2358      // See if the target entry or one of its superiors is a smart referral.
2359      if (! controlMap.containsKey(
2360           ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID))
2361      {
2362        final Entry referralEntry = findNearestReferral(dn);
2363        if (referralEntry != null)
2364        {
2365          return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2366               ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(),
2367               INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(),
2368               getReferralURLs(dn, referralEntry)));
2369        }
2370      }
2371
2372      // See if the target entry is the root DSE, the subschema subentry, or a
2373      // changelog entry.
2374      if (dn.isNullDN())
2375      {
2376        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2377             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
2378             ERR_MEM_HANDLER_MOD_ROOT_DSE.get(), null));
2379      }
2380      else if (dn.equals(subschemaSubentryDN))
2381      {
2382        try
2383        {
2384          validateSchemaMods(request);
2385        }
2386        catch (final LDAPException le)
2387        {
2388          return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2389               le.getResultCode().intValue(), le.getMatchedDN(),
2390               le.getMessage(), null));
2391        }
2392      }
2393      else if (dn.isDescendantOf(changeLogBaseDN, true))
2394      {
2395        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2396             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
2397             ERR_MEM_HANDLER_MOD_CHANGELOG.get(request.getDN()), null));
2398      }
2399
2400      // Get the target entry.  If it does not exist, then fail.
2401      Entry entry = entryMap.get(dn);
2402      if (entry == null)
2403      {
2404        if (dn.equals(subschemaSubentryDN))
2405        {
2406          entry = subschemaSubentryRef.get().duplicate();
2407        }
2408        else
2409        {
2410          return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2411               ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn),
2412               ERR_MEM_HANDLER_MOD_NO_SUCH_ENTRY.get(request.getDN()), null));
2413        }
2414      }
2415
2416
2417      // If any of the modifications target password attributes, then make sure
2418      // they are properly encoded.
2419      final ReadOnlyEntry readOnlyEntry = new ReadOnlyEntry(entry);
2420      final List<Modification> unencodedMods = request.getModifications();
2421      final ArrayList<Modification> modifications =
2422           new ArrayList<>(unencodedMods.size());
2423      for (final Modification m : unencodedMods)
2424      {
2425        try
2426        {
2427          modifications.add(encodeModificationPasswords(m, readOnlyEntry,
2428               unencodedMods));
2429        }
2430        catch (final LDAPException le)
2431        {
2432          Debug.debugException(le);
2433          if (le.getResultCode().isClientSideResultCode())
2434          {
2435            return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2436                 ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, le.getMatchedDN(),
2437                 le.getMessage(), null));
2438          }
2439          else
2440          {
2441            return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2442                 le.getResultCode().intValue(), le.getMatchedDN(),
2443                 le.getMessage(), null));
2444          }
2445        }
2446      }
2447
2448
2449      // Attempt to apply the modifications to the entry.  If successful, then a
2450      // copy of the entry will be returned with the modifications applied.
2451      final Entry modifiedEntry;
2452      try
2453      {
2454        modifiedEntry = Entry.applyModifications(entry,
2455             controlMap.containsKey(
2456                  PermissiveModifyRequestControl.PERMISSIVE_MODIFY_REQUEST_OID),
2457             modifications);
2458      }
2459      catch (final LDAPException le)
2460      {
2461        Debug.debugException(le);
2462        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2463             le.getResultCode().intValue(), null,
2464             ERR_MEM_HANDLER_MOD_FAILED.get(request.getDN(), le.getMessage()),
2465             null));
2466      }
2467
2468      // If a schema was provided, use it to validate the resulting entry.
2469      // Also, ensure that no NO-USER-MODIFICATION attributes were targeted.
2470      final EntryValidator entryValidator = entryValidatorRef.get();
2471      if (entryValidator != null)
2472      {
2473        final ArrayList<String> invalidReasons = new ArrayList<>(1);
2474        if (! entryValidator.entryIsValid(modifiedEntry, invalidReasons))
2475        {
2476          return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2477               ResultCode.OBJECT_CLASS_VIOLATION_INT_VALUE, null,
2478               ERR_MEM_HANDLER_MOD_VIOLATES_SCHEMA.get(request.getDN(),
2479                    StaticUtils.concatenateStrings(invalidReasons)),
2480               null));
2481        }
2482
2483        for (final Modification m : modifications)
2484        {
2485          final Attribute a = m.getAttribute();
2486          final String baseName = a.getBaseName();
2487          final AttributeTypeDefinition at = schema.getAttributeType(baseName);
2488          if ((! isInternalOp) && (at != null) && at.isNoUserModification())
2489          {
2490            return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2491                 ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null,
2492                 ERR_MEM_HANDLER_MOD_NO_USER_MOD.get(request.getDN(),
2493                      a.getName()), null));
2494          }
2495        }
2496      }
2497
2498
2499      // Perform the appropriate processing for the assertion and proxied
2500      // authorization controls.
2501      // Perform the appropriate processing for the assertion, pre-read,
2502      // post-read, and proxied authorization controls.
2503      final DN authzDN;
2504      try
2505      {
2506        handleAssertionRequestControl(controlMap, entry);
2507
2508        authzDN = handleProxiedAuthControl(controlMap);
2509      }
2510      catch (final LDAPException le)
2511      {
2512        Debug.debugException(le);
2513        return new LDAPMessage(messageID, new ModifyResponseProtocolOp(
2514             le.getResultCode().intValue(), null, le.getMessage(), null));
2515      }
2516
2517      // Update modifiersName and modifyTimestamp.
2518      if (generateOperationalAttributes)
2519      {
2520        modifiedEntry.setAttribute(new Attribute("modifiersName",
2521             DistinguishedNameMatchingRule.getInstance(),
2522             authzDN.toString()));
2523        modifiedEntry.setAttribute(new Attribute("modifyTimestamp",
2524             GeneralizedTimeMatchingRule.getInstance(),
2525             StaticUtils.encodeGeneralizedTime(new Date())));
2526      }
2527
2528      // Perform the appropriate processing for the pre-read and post-read
2529      // controls.
2530      final PreReadResponseControl preReadResponse =
2531           handlePreReadControl(controlMap, entry);
2532      if (preReadResponse != null)
2533      {
2534        responseControls.add(preReadResponse);
2535      }
2536
2537      final PostReadResponseControl postReadResponse =
2538           handlePostReadControl(controlMap, modifiedEntry);
2539      if (postReadResponse != null)
2540      {
2541        responseControls.add(postReadResponse);
2542      }
2543
2544
2545      // Replace the entry in the map and return a success result.
2546      if (dn.equals(subschemaSubentryDN))
2547      {
2548        final Schema newSchema = new Schema(modifiedEntry);
2549        subschemaSubentryRef.set(new ReadOnlyEntry(modifiedEntry));
2550        schemaRef.set(newSchema);
2551        entryValidatorRef.set(new EntryValidator(newSchema));
2552      }
2553      else
2554      {
2555        entryMap.put(dn, new ReadOnlyEntry(modifiedEntry));
2556        indexDelete(entry);
2557        indexAdd(modifiedEntry);
2558      }
2559      addChangeLogEntry(request, authzDN);
2560      return new LDAPMessage(messageID,
2561           new ModifyResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
2562                null, null),
2563           responseControls);
2564    }
2565  }
2566
2567
2568
2569  /**
2570   * Checks to see if the provided modification targets a password attribute.
2571   * If so, then it makes sure that the modification is properly encoded.
2572   *
2573   * @param  mod    The modification being processed.
2574   * @param  entry  The entry being modified.
2575   * @param  mods   The full set of modifications.
2576   *
2577   * @return  The encoded form of the provided modification if appropriate, or
2578   *          the original modification if no encoding is needed.
2579   *
2580   * @throws  LDAPException  If a problem is encountered during processing.
2581   */
2582  @NotNull()
2583  private Modification encodeModificationPasswords(
2584                            @NotNull final Modification mod,
2585                            @NotNull final ReadOnlyEntry entry,
2586                            @NotNull final List<Modification> mods)
2587          throws LDAPException
2588  {
2589    // If the modification doesn't have any values, then we don't need to do
2590    // anything.
2591    final ASN1OctetString[] originalValues = mod.getRawValues();
2592    if (originalValues.length == 0)
2593    {
2594      return mod;
2595    }
2596
2597
2598    // If no password attributes are defined, or if no password encoders are
2599    // defined, then we don't need to do anything.
2600    // If no password attributes are defined, then we don't need to do anything.
2601    if (extendedPasswordAttributes.isEmpty() || passwordEncoders.isEmpty())
2602    {
2603      return mod;
2604    }
2605
2606
2607    // If the modification doesn't target a password attribute, then we don't
2608    // need to do anything.
2609    boolean isPasswordAttribute = false;
2610    for (final String passwordAttribute : extendedPasswordAttributes)
2611    {
2612      if (mod.getAttribute().getBaseName().equalsIgnoreCase(passwordAttribute))
2613      {
2614        isPasswordAttribute = true;
2615        break;
2616      }
2617    }
2618
2619    if (! isPasswordAttribute)
2620    {
2621      return mod;
2622    }
2623
2624
2625    // Process the modification based on its modification type.
2626    final ASN1OctetString[] newValues =
2627         new ASN1OctetString[originalValues.length];
2628    for (int i=0; i < originalValues.length; i++)
2629    {
2630      newValues[i] = encodeModValue(originalValues[i], mod, entry, mods);
2631    }
2632
2633    return new Modification(mod.getModificationType(), mod.getAttributeName(),
2634         newValues);
2635  }
2636
2637
2638
2639  /**
2640   * Encodes the provided modification value, if necessary.
2641   *
2642   * @param  value  The modification value being processed.
2643   * @param  mod    The modification being processed.
2644   * @param  entry  The unaltered form of the entry being modified.
2645   * @param  mods   The full set of modifications being processed.
2646   *
2647   * @return  The encoded modification value, or the original value if no
2648   *          encoding is necessary.
2649   *
2650   * @throws  LDAPException  If a problem is encountered during processing.
2651   */
2652  @NotNull()
2653  private ASN1OctetString encodeModValue(@NotNull final ASN1OctetString value,
2654                                         @NotNull final Modification mod,
2655                                         @NotNull final ReadOnlyEntry entry,
2656                                         @NotNull final List<Modification> mods)
2657          throws LDAPException
2658  {
2659    // First, see if the password is already encoded.  If so, then just return
2660    // it if that encoded representation looks valid.
2661    for (final InMemoryPasswordEncoder encoder : passwordEncoders)
2662    {
2663      if (encoder.passwordStartsWithPrefix(value))
2664      {
2665        encoder.ensurePreEncodedPasswordAppearsValid(value, entry, mods);
2666        return value;
2667      }
2668    }
2669
2670
2671    // If the modification type is add or replace, then we should just encode
2672    // the password in accordance with the primary encoder.
2673    final ModificationType modificationType = mod.getModificationType();
2674    if ((modificationType == ModificationType.ADD) ||
2675        (modificationType == ModificationType.REPLACE))
2676    {
2677      // If there is no primary password encoder, then just leave the value in
2678      // the clear.  Otherwise, encode it with the primary encoder.
2679      if (primaryPasswordEncoder == null)
2680      {
2681        return value;
2682      }
2683      else
2684      {
2685        return primaryPasswordEncoder.encodePassword(value, entry, mods);
2686      }
2687    }
2688
2689
2690    // If the modification type is a delete, then we should see if the
2691    // clear-text value matches any of the values stored in the entry, whether
2692    // encoded or not.  If the provided clear-text password matches an existing
2693    // encoded value, then we'll return the encoded value.  If the clear-text
2694    // password matches an existing clear-text password, then we'll return that
2695    // clear-text password.  But even if it doesn't match anything, then we'll
2696    // still return the clear-text password.
2697    if (modificationType == ModificationType.DELETE)
2698    {
2699      final Attribute existingAttribute =
2700           entry.getAttribute(mod.getAttributeName());
2701      if (existingAttribute == null)
2702      {
2703        return value;
2704      }
2705
2706      for (final ASN1OctetString existingValue :
2707           existingAttribute.getRawValues())
2708      {
2709        if (value.equalsIgnoreType(existingValue))
2710        {
2711          return value;
2712        }
2713
2714        for (final InMemoryPasswordEncoder encoder : passwordEncoders)
2715        {
2716          if (encoder.clearPasswordMatchesEncodedPassword(value, existingValue,
2717                   entry))
2718          {
2719            return existingValue;
2720          }
2721        }
2722      }
2723
2724      return value;
2725    }
2726
2727
2728    // The only way we should be able to get here is for an increment
2729    // modification type, which is just stupid.  But in that case, we'll just
2730    // return the value as-is.
2731    return value;
2732  }
2733
2734
2735
2736  /**
2737   * Validates a modify request targeting the server schema.  Modifications to
2738   * attribute syntaxes and matching rules will not be allowed.  Modifications
2739   * to other schema elements will only be allowed for add and delete
2740   * modification types, and adds will only be allowed with a valid syntax.
2741   *
2742   * @param  request  The modify request to validate.
2743   *
2744   * @throws  LDAPException  If a problem is encountered.
2745   */
2746  private void validateSchemaMods(
2747                    @NotNull final ModifyRequestProtocolOp request)
2748          throws LDAPException
2749  {
2750    // If there is no schema, then we won't allow modifications at all.
2751    if (schemaRef.get() == null)
2752    {
2753      throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2754           ERR_MEM_HANDLER_MOD_SCHEMA.get(subschemaSubentryDN.toString()));
2755    }
2756
2757
2758    for (final Modification m : request.getModifications())
2759    {
2760      // If the modification targets attribute syntaxes or matching rules, then
2761      // reject it.
2762      final String attrName = m.getAttributeName();
2763      if (attrName.equalsIgnoreCase(Schema.ATTR_ATTRIBUTE_SYNTAX) ||
2764           attrName.equalsIgnoreCase(Schema.ATTR_MATCHING_RULE))
2765      {
2766        throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2767             ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_ATTR.get(attrName));
2768      }
2769      else if (attrName.equalsIgnoreCase(Schema.ATTR_ATTRIBUTE_TYPE))
2770      {
2771        if (m.getModificationType() == ModificationType.ADD)
2772        {
2773          for (final String value : m.getValues())
2774          {
2775            new AttributeTypeDefinition(value);
2776          }
2777        }
2778        else if (m.getModificationType() != ModificationType.DELETE)
2779        {
2780          throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2781               ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get(
2782                    m.getModificationType().getName(), attrName));
2783        }
2784      }
2785      else if (attrName.equalsIgnoreCase(Schema.ATTR_OBJECT_CLASS))
2786      {
2787        if (m.getModificationType() == ModificationType.ADD)
2788        {
2789          for (final String value : m.getValues())
2790          {
2791            new ObjectClassDefinition(value);
2792          }
2793        }
2794        else if (m.getModificationType() != ModificationType.DELETE)
2795        {
2796          throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2797               ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get(
2798                    m.getModificationType().getName(), attrName));
2799        }
2800      }
2801      else if (attrName.equalsIgnoreCase(Schema.ATTR_NAME_FORM))
2802      {
2803        if (m.getModificationType() == ModificationType.ADD)
2804        {
2805          for (final String value : m.getValues())
2806          {
2807            new NameFormDefinition(value);
2808          }
2809        }
2810        else if (m.getModificationType() != ModificationType.DELETE)
2811        {
2812          throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2813               ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get(
2814                    m.getModificationType().getName(), attrName));
2815        }
2816      }
2817      else if (attrName.equalsIgnoreCase(Schema.ATTR_DIT_CONTENT_RULE))
2818      {
2819        if (m.getModificationType() == ModificationType.ADD)
2820        {
2821          for (final String value : m.getValues())
2822          {
2823            new DITContentRuleDefinition(value);
2824          }
2825        }
2826        else if (m.getModificationType() != ModificationType.DELETE)
2827        {
2828          throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2829               ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get(
2830                    m.getModificationType().getName(), attrName));
2831        }
2832      }
2833      else if (attrName.equalsIgnoreCase(Schema.ATTR_DIT_STRUCTURE_RULE))
2834      {
2835        if (m.getModificationType() == ModificationType.ADD)
2836        {
2837          for (final String value : m.getValues())
2838          {
2839            new DITStructureRuleDefinition(value);
2840          }
2841        }
2842        else if (m.getModificationType() != ModificationType.DELETE)
2843        {
2844          throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2845               ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get(
2846                    m.getModificationType().getName(), attrName));
2847        }
2848      }
2849      else if (attrName.equalsIgnoreCase(Schema.ATTR_MATCHING_RULE_USE))
2850      {
2851        if (m.getModificationType() == ModificationType.ADD)
2852        {
2853          for (final String value : m.getValues())
2854          {
2855            new MatchingRuleUseDefinition(value);
2856          }
2857        }
2858        else if (m.getModificationType() != ModificationType.DELETE)
2859        {
2860          throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2861               ERR_MEM_HANDLER_MOD_SCHEMA_DISALLOWED_MOD_TYPE.get(
2862                    m.getModificationType().getName(), attrName));
2863        }
2864      }
2865    }
2866  }
2867
2868
2869
2870  /**
2871   * Processes the provided modify DN request.
2872   * <BR><BR>
2873   * This method may be used regardless of whether the server is listening for
2874   * client connections, and regardless of whether modify DN operations are
2875   * allowed in the server.
2876   *
2877   * @param  modifyDNRequest  The modify DN request to be processed.  It must
2878   *                          not be {@code null}.
2879   *
2880   * @return  The result of processing the modify DN operation.
2881   *
2882   * @throws  LDAPException  If the server rejects the modify DN request, or if
2883   *                         a problem is encountered while sending the request
2884   *                         or reading the response.
2885   */
2886  @NotNull()
2887  public LDAPResult modifyDN(@NotNull final ModifyDNRequest modifyDNRequest)
2888         throws LDAPException
2889  {
2890    final ArrayList<Control> requestControlList =
2891         new ArrayList<>(modifyDNRequest.getControlList());
2892    requestControlList.add(new Control(OID_INTERNAL_OPERATION_REQUEST_CONTROL,
2893         false));
2894
2895    final LDAPMessage responseMessage = processModifyDNRequest(
2896         1, new ModifyDNRequestProtocolOp(modifyDNRequest.getDN(),
2897              modifyDNRequest.getNewRDN(), modifyDNRequest.deleteOldRDN(),
2898              modifyDNRequest.getNewSuperiorDN()),
2899         requestControlList);
2900
2901    final ModifyDNResponseProtocolOp modifyDNResponse =
2902         responseMessage.getModifyDNResponseProtocolOp();
2903
2904    final LDAPResult ldapResult = new LDAPResult(responseMessage.getMessageID(),
2905         ResultCode.valueOf(modifyDNResponse.getResultCode()),
2906         modifyDNResponse.getDiagnosticMessage(),
2907         modifyDNResponse.getMatchedDN(), modifyDNResponse.getReferralURLs(),
2908         responseMessage.getControls());
2909
2910    switch (modifyDNResponse.getResultCode())
2911    {
2912      case ResultCode.SUCCESS_INT_VALUE:
2913      case ResultCode.NO_OPERATION_INT_VALUE:
2914        return ldapResult;
2915      default:
2916        throw new LDAPException(ldapResult);
2917    }
2918  }
2919
2920
2921
2922  /**
2923   * Attempts to process the provided modify DN request.  The attempt will fail
2924   * if any of the following conditions is true:
2925   * <UL>
2926   *   <LI>There is a problem with any of the request controls.</LI>
2927   *   <LI>The modify DN request contains a malformed target DN, new RDN, or
2928   *       new superior DN.</LI>
2929   *   <LI>The original or new DN is that of the root DSE.</LI>
2930   *   <LI>The original or new DN is that of the subschema subentry.</LI>
2931   *   <LI>The new DN of the entry would conflict with the DN of an existing
2932   *       entry.</LI>
2933   *   <LI>The new DN of the entry would exist outside the set of defined
2934   *       base DNs.</LI>
2935   *   <LI>The new DN of the entry is not a defined base DN and does not exist
2936   *       immediately below an existing entry.</LI>
2937   * </UL>
2938   *
2939   * @param  messageID  The message ID of the LDAP message containing the modify
2940   *                    DN request.
2941   * @param  request    The modify DN request that was included in the LDAP
2942   *                    message that was received.
2943   * @param  controls   The set of controls included in the LDAP message.  It
2944   *                    may be empty if there were no controls, but will not be
2945   *                    {@code null}.
2946   *
2947   * @return  The {@link LDAPMessage} containing the response to send to the
2948   *          client.  The protocol op in the {@code LDAPMessage} must be an
2949   *          {@code ModifyDNResponseProtocolOp}.
2950   */
2951  @Override()
2952  @NotNull()
2953  public LDAPMessage processModifyDNRequest(final int messageID,
2954                          @NotNull final ModifyDNRequestProtocolOp request,
2955                          @NotNull final List<Control> controls)
2956  {
2957    synchronized (entryMap)
2958    {
2959      // Sleep before processing, if appropriate.
2960      sleepBeforeProcessing();
2961
2962      // Process the provided request controls.
2963      final Map<String,Control> controlMap;
2964      try
2965      {
2966        controlMap = RequestControlPreProcessor.processControls(
2967             LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_DN_REQUEST, controls);
2968      }
2969      catch (final LDAPException le)
2970      {
2971        Debug.debugException(le);
2972        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
2973             le.getResultCode().intValue(), null, le.getMessage(), null));
2974      }
2975      final ArrayList<Control> responseControls = new ArrayList<>(1);
2976
2977
2978      // If this operation type is not allowed, then reject it.
2979      final boolean isInternalOp =
2980           controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL);
2981      if ((! isInternalOp) &&
2982           (! config.getAllowedOperationTypes().contains(
2983                OperationType.MODIFY_DN)))
2984      {
2985        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
2986             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
2987             ERR_MEM_HANDLER_MODIFY_DN_NOT_ALLOWED.get(), null));
2988      }
2989
2990
2991      // If this operation type requires authentication, then ensure that the
2992      // client is authenticated.
2993      if ((authenticatedDN.isNullDN() &&
2994           config.getAuthenticationRequiredOperationTypes().contains(
2995                OperationType.MODIFY_DN)))
2996      {
2997        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
2998             ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null,
2999             ERR_MEM_HANDLER_MODIFY_DN_REQUIRES_AUTH.get(), null));
3000      }
3001
3002
3003      // See if this modify DN request is part of a transaction.  If so, then
3004      // perform appropriate processing for it and return success immediately
3005      // without actually doing any further processing.
3006      try
3007      {
3008        final ASN1OctetString txnID =
3009             processTransactionRequest(messageID, request, controlMap);
3010        if (txnID != null)
3011        {
3012          return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3013               ResultCode.SUCCESS_INT_VALUE, null,
3014               INFO_MEM_HANDLER_OP_IN_TXN.get(txnID.stringValue()), null));
3015        }
3016      }
3017      catch (final LDAPException le)
3018      {
3019        Debug.debugException(le);
3020        return new LDAPMessage(messageID,
3021             new ModifyDNResponseProtocolOp(le.getResultCode().intValue(),
3022                  le.getMatchedDN(), le.getDiagnosticMessage(),
3023                  StaticUtils.toList(le.getReferralURLs())),
3024             le.getResponseControls());
3025      }
3026
3027
3028      // Get the parsed target DN, new RDN, and new superior DN values.
3029      final DN dn;
3030      final Schema schema = schemaRef.get();
3031      try
3032      {
3033        dn = new DN(request.getDN(), schema);
3034      }
3035      catch (final LDAPException le)
3036      {
3037        Debug.debugException(le);
3038        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3039             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
3040             ERR_MEM_HANDLER_MOD_DN_MALFORMED_DN.get(request.getDN(),
3041                  le.getMessage()),
3042             null));
3043      }
3044
3045      final RDN newRDN;
3046      try
3047      {
3048        newRDN = new RDN(request.getNewRDN(), schema);
3049      }
3050      catch (final LDAPException le)
3051      {
3052        Debug.debugException(le);
3053        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3054             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
3055             ERR_MEM_HANDLER_MOD_DN_MALFORMED_NEW_RDN.get(request.getDN(),
3056                  request.getNewRDN(), le.getMessage()),
3057             null));
3058      }
3059
3060      final DN newSuperiorDN;
3061      final String newSuperiorString = request.getNewSuperiorDN();
3062      if (newSuperiorString == null)
3063      {
3064        newSuperiorDN = null;
3065      }
3066      else
3067      {
3068        try
3069        {
3070          newSuperiorDN = new DN(newSuperiorString, schema);
3071        }
3072        catch (final LDAPException le)
3073        {
3074          Debug.debugException(le);
3075          return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3076               ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
3077               ERR_MEM_HANDLER_MOD_DN_MALFORMED_NEW_SUPERIOR.get(
3078                    request.getDN(), request.getNewSuperiorDN(),
3079                    le.getMessage()),
3080               null));
3081        }
3082      }
3083
3084      // See if the target entry or one of its superiors is a smart referral.
3085      if (! controlMap.containsKey(
3086           ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID))
3087      {
3088        final Entry referralEntry = findNearestReferral(dn);
3089        if (referralEntry != null)
3090        {
3091          return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3092               ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(),
3093               INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(),
3094               getReferralURLs(dn, referralEntry)));
3095        }
3096      }
3097
3098      // See if the target is the root DSE, the subschema subentry, or a
3099      // changelog entry.
3100      if (dn.isNullDN())
3101      {
3102        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3103             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
3104             ERR_MEM_HANDLER_MOD_DN_ROOT_DSE.get(), null));
3105      }
3106      else if (dn.equals(subschemaSubentryDN))
3107      {
3108        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3109             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
3110             ERR_MEM_HANDLER_MOD_DN_SOURCE_IS_SCHEMA.get(), null));
3111      }
3112      else if (dn.isDescendantOf(changeLogBaseDN, true))
3113      {
3114        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3115             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
3116             ERR_MEM_HANDLER_MOD_DN_SOURCE_IS_CHANGELOG.get(), null));
3117      }
3118
3119      // Construct the new DN.
3120      final DN newDN;
3121      if (newSuperiorDN == null)
3122      {
3123        final DN originalParent = dn.getParent();
3124        if (originalParent == null)
3125        {
3126          newDN = new DN(newRDN);
3127        }
3128        else
3129        {
3130          newDN = new DN(newRDN, originalParent);
3131        }
3132      }
3133      else
3134      {
3135        newDN = new DN(newRDN, newSuperiorDN);
3136      }
3137
3138      // If the new DN matches the old DN, then fail.
3139      if (newDN.equals(dn))
3140      {
3141        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3142             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
3143             ERR_MEM_HANDLER_MOD_DN_NEW_DN_SAME_AS_OLD.get(request.getDN()),
3144             null));
3145      }
3146
3147      // If the new DN is below a smart referral, then fail.
3148      if (! controlMap.containsKey(
3149           ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID))
3150      {
3151        final Entry referralEntry = findNearestReferral(newDN);
3152        if (referralEntry != null)
3153        {
3154          return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3155               ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, referralEntry.getDN(),
3156               ERR_MEM_HANDLER_MOD_DN_NEW_DN_BELOW_REFERRAL.get(request.getDN(),
3157                    referralEntry.getDN().toString(), newDN.toString()),
3158               null));
3159        }
3160      }
3161
3162      // If the target entry doesn't exist, then fail.
3163      final Entry originalEntry = entryMap.get(dn);
3164      if (originalEntry == null)
3165      {
3166        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3167             ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(dn),
3168             ERR_MEM_HANDLER_MOD_DN_NO_SUCH_ENTRY.get(request.getDN()), null));
3169      }
3170
3171      // If the new DN matches the subschema subentry DN, then fail.
3172      if (newDN.equals(subschemaSubentryDN))
3173      {
3174        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3175             ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null,
3176             ERR_MEM_HANDLER_MOD_DN_TARGET_IS_SCHEMA.get(request.getDN(),
3177                  newDN.toString()),
3178             null));
3179      }
3180
3181      // If the new DN is at or below the changelog base DN, then fail.
3182      if (newDN.isDescendantOf(changeLogBaseDN, true))
3183      {
3184        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3185             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
3186             ERR_MEM_HANDLER_MOD_DN_TARGET_IS_CHANGELOG.get(request.getDN(),
3187                  newDN.toString()),
3188             null));
3189      }
3190
3191      // If the new DN already exists, then fail.
3192      if (entryMap.containsKey(newDN))
3193      {
3194        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3195             ResultCode.ENTRY_ALREADY_EXISTS_INT_VALUE, null,
3196             ERR_MEM_HANDLER_MOD_DN_TARGET_ALREADY_EXISTS.get(request.getDN(),
3197                  newDN.toString()),
3198             null));
3199      }
3200
3201      // If the new DN is not a base DN and its parent does not exist, then
3202      // fail.
3203      if (baseDNs.contains(newDN))
3204      {
3205        // The modify DN can be processed.
3206      }
3207      else
3208      {
3209        final DN newParent = newDN.getParent();
3210        if ((newParent != null) && entryMap.containsKey(newParent))
3211        {
3212          // The modify DN can be processed.
3213        }
3214        else
3215        {
3216          return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3217               ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(newDN),
3218               ERR_MEM_HANDLER_MOD_DN_PARENT_DOESNT_EXIST.get(request.getDN(),
3219                    newDN.toString()),
3220               null));
3221        }
3222      }
3223
3224      // Create a copy of the entry and update it to reflect the new DN (with
3225      // attribute value changes).
3226      final RDN originalRDN = dn.getRDN();
3227      final Entry updatedEntry = originalEntry.duplicate();
3228      updatedEntry.setDN(newDN);
3229      if (request.deleteOldRDN())
3230      {
3231        final String[] oldRDNNames  = originalRDN.getAttributeNames();
3232        final byte[][] oldRDNValues = originalRDN.getByteArrayAttributeValues();
3233        for (int i=0; i < oldRDNNames.length; i++)
3234        {
3235          updatedEntry.removeAttributeValue(oldRDNNames[i], oldRDNValues[i]);
3236        }
3237      }
3238
3239      final String[] newRDNNames  = newRDN.getAttributeNames();
3240      final byte[][] newRDNValues = newRDN.getByteArrayAttributeValues();
3241      for (int i=0; i < newRDNNames.length; i++)
3242      {
3243        final MatchingRule matchingRule =
3244             MatchingRule.selectEqualityMatchingRule(newRDNNames[i], schema);
3245        updatedEntry.addAttribute(new Attribute(newRDNNames[i], matchingRule,
3246             newRDNValues[i]));
3247      }
3248
3249      // If a schema was provided, then make sure the updated entry conforms to
3250      // the schema.  Also, reject the attempt if any of the new RDN attributes
3251      // is marked with NO-USER-MODIFICATION.
3252      final EntryValidator entryValidator = entryValidatorRef.get();
3253      if (entryValidator != null)
3254      {
3255        final ArrayList<String> invalidReasons = new ArrayList<>(1);
3256        if (! entryValidator.entryIsValid(updatedEntry, invalidReasons))
3257        {
3258          return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3259               ResultCode.OBJECT_CLASS_VIOLATION_INT_VALUE, null,
3260               ERR_MEM_HANDLER_MOD_DN_VIOLATES_SCHEMA.get(request.getDN(),
3261                    StaticUtils.concatenateStrings(invalidReasons)),
3262               null));
3263        }
3264
3265        final String[] oldRDNNames = originalRDN.getAttributeNames();
3266        for (int i=0; i < oldRDNNames.length; i++)
3267        {
3268          final String name = oldRDNNames[i];
3269          final AttributeTypeDefinition at = schema.getAttributeType(name);
3270          if ((! isInternalOp) && (at != null) && at.isNoUserModification())
3271          {
3272            final byte[] value = originalRDN.getByteArrayAttributeValues()[i];
3273            if (! updatedEntry.hasAttributeValue(name, value))
3274            {
3275              return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3276                   ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null,
3277                   ERR_MEM_HANDLER_MOD_DN_NO_USER_MOD.get(request.getDN(),
3278                        name), null));
3279            }
3280          }
3281        }
3282
3283        for (int i=0; i < newRDNNames.length; i++)
3284        {
3285          final String name = newRDNNames[i];
3286          final AttributeTypeDefinition at = schema.getAttributeType(name);
3287          if ((! isInternalOp) && (at != null) && at.isNoUserModification())
3288          {
3289            final byte[] value = newRDN.getByteArrayAttributeValues()[i];
3290            if (! originalEntry.hasAttributeValue(name, value))
3291            {
3292              return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3293                   ResultCode.CONSTRAINT_VIOLATION_INT_VALUE, null,
3294                   ERR_MEM_HANDLER_MOD_DN_NO_USER_MOD.get(request.getDN(),
3295                        name), null));
3296            }
3297          }
3298        }
3299      }
3300
3301      // Perform the appropriate processing for the assertion and proxied
3302      // authorization controls
3303      final DN authzDN;
3304      try
3305      {
3306        handleAssertionRequestControl(controlMap, originalEntry);
3307
3308        authzDN = handleProxiedAuthControl(controlMap);
3309      }
3310      catch (final LDAPException le)
3311      {
3312        Debug.debugException(le);
3313        return new LDAPMessage(messageID, new ModifyDNResponseProtocolOp(
3314             le.getResultCode().intValue(), null, le.getMessage(), null));
3315      }
3316
3317      // Update the modifiersName, modifyTimestamp, and entryDN operational
3318      // attributes.
3319      if (generateOperationalAttributes)
3320      {
3321        updatedEntry.setAttribute(new Attribute("modifiersName",
3322             DistinguishedNameMatchingRule.getInstance(),
3323             authzDN.toString()));
3324        updatedEntry.setAttribute(new Attribute("modifyTimestamp",
3325             GeneralizedTimeMatchingRule.getInstance(),
3326             StaticUtils.encodeGeneralizedTime(new Date())));
3327        updatedEntry.setAttribute(new Attribute("entryDN",
3328             DistinguishedNameMatchingRule.getInstance(),
3329             newDN.toNormalizedString()));
3330      }
3331
3332      // Perform the appropriate processing for the pre-read and post-read
3333      // controls.
3334      final PreReadResponseControl preReadResponse =
3335           handlePreReadControl(controlMap, originalEntry);
3336      if (preReadResponse != null)
3337      {
3338        responseControls.add(preReadResponse);
3339      }
3340
3341      final PostReadResponseControl postReadResponse =
3342           handlePostReadControl(controlMap, updatedEntry);
3343      if (postReadResponse != null)
3344      {
3345        responseControls.add(postReadResponse);
3346      }
3347
3348      // Remove the old entry and add the new one.
3349      entryMap.remove(dn);
3350      entryMap.put(newDN, new ReadOnlyEntry(updatedEntry));
3351      indexDelete(originalEntry);
3352      indexAdd(updatedEntry);
3353
3354      // If the target entry had any subordinates, then rename them as well.
3355      final RDN[] oldDNComps = dn.getRDNs();
3356      final RDN[] newDNComps = newDN.getRDNs();
3357      final Set<DN> dnSet = new LinkedHashSet<>(entryMap.keySet());
3358      for (final DN mapEntryDN : dnSet)
3359      {
3360        if (mapEntryDN.isDescendantOf(dn, false))
3361        {
3362          final Entry o = entryMap.remove(mapEntryDN);
3363          final Entry e = o.duplicate();
3364
3365          final RDN[] oldMapEntryComps = mapEntryDN.getRDNs();
3366          final int compsToSave = oldMapEntryComps.length - oldDNComps.length;
3367
3368          final RDN[] newMapEntryComps =
3369               new RDN[compsToSave + newDNComps.length];
3370          System.arraycopy(oldMapEntryComps, 0, newMapEntryComps, 0,
3371               compsToSave);
3372          System.arraycopy(newDNComps, 0, newMapEntryComps, compsToSave,
3373               newDNComps.length);
3374
3375          final DN newMapEntryDN = new DN(newMapEntryComps);
3376          e.setDN(newMapEntryDN);
3377          if (generateOperationalAttributes)
3378          {
3379            e.setAttribute(new Attribute("entryDN",
3380                 DistinguishedNameMatchingRule.getInstance(),
3381                 newMapEntryDN.toNormalizedString()));
3382          }
3383          entryMap.put(newMapEntryDN, new ReadOnlyEntry(e));
3384          indexDelete(o);
3385          indexAdd(e);
3386          handleReferentialIntegrityModifyDN(mapEntryDN, newMapEntryDN);
3387        }
3388      }
3389
3390      addChangeLogEntry(request, authzDN);
3391      handleReferentialIntegrityModifyDN(dn, newDN);
3392      return new LDAPMessage(messageID,
3393           new ModifyDNResponseProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
3394                null, null),
3395           responseControls);
3396    }
3397  }
3398
3399
3400
3401  /**
3402   * Handles any appropriate referential integrity processing for a modify DN
3403   * operation.
3404   *
3405   * @param  oldDN  The old DN for the entry.
3406   * @param  newDN  The new DN for the entry.
3407   */
3408  private void handleReferentialIntegrityModifyDN(@NotNull final DN oldDN,
3409                                                  @NotNull final DN newDN)
3410  {
3411    if (referentialIntegrityAttributes.isEmpty())
3412    {
3413      return;
3414    }
3415
3416    final ArrayList<DN> entryDNs = new ArrayList<>(entryMap.keySet());
3417    for (final DN mapDN : entryDNs)
3418    {
3419      final ReadOnlyEntry e = entryMap.get(mapDN);
3420
3421      boolean referenceFound = false;
3422      final Schema schema = schemaRef.get();
3423      for (final String attrName : referentialIntegrityAttributes)
3424      {
3425        final Attribute a = e.getAttribute(attrName, schema);
3426        if ((a != null) &&
3427            a.hasValue(oldDN.toNormalizedString(),
3428                 DistinguishedNameMatchingRule.getInstance()))
3429        {
3430          referenceFound = true;
3431          break;
3432        }
3433      }
3434
3435      if (referenceFound)
3436      {
3437        final Entry copy = e.duplicate();
3438        for (final String attrName : referentialIntegrityAttributes)
3439        {
3440          if (copy.removeAttributeValue(attrName, oldDN.toNormalizedString(),
3441                   DistinguishedNameMatchingRule.getInstance()))
3442          {
3443            copy.addAttribute(attrName, newDN.toString());
3444          }
3445        }
3446        entryMap.put(mapDN, new ReadOnlyEntry(copy));
3447        indexDelete(e);
3448        indexAdd(copy);
3449      }
3450    }
3451  }
3452
3453
3454
3455  /**
3456   * Attempts to process the provided search request.  The attempt will fail
3457   * if any of the following conditions is true:
3458   * <UL>
3459   *   <LI>There is a problem with any of the request controls.</LI>
3460   *   <LI>The modify DN request contains a malformed target DN, new RDN, or
3461   *       new superior DN.</LI>
3462   *   <LI>The new DN of the entry would conflict with the DN of an existing
3463   *       entry.</LI>
3464   *   <LI>The new DN of the entry would exist outside the set of defined
3465   *       base DNs.</LI>
3466   *   <LI>The new DN of the entry is not a defined base DN and does not exist
3467   *       immediately below an existing entry.</LI>
3468   * </UL>
3469   *
3470   * @param  messageID  The message ID of the LDAP message containing the search
3471   *                    request.
3472   * @param  request    The search request that was included in the LDAP message
3473   *                    that was received.
3474   * @param  controls   The set of controls included in the LDAP message.  It
3475   *                    may be empty if there were no controls, but will not be
3476   *                    {@code null}.
3477   *
3478   * @return  The {@link LDAPMessage} containing the response to send to the
3479   *          client.  The protocol op in the {@code LDAPMessage} must be an
3480   *          {@code SearchResultDoneProtocolOp}.
3481   */
3482  @Override()
3483  @NotNull()
3484  public LDAPMessage processSearchRequest(final int messageID,
3485                          @NotNull final SearchRequestProtocolOp request,
3486                          @NotNull final List<Control> controls)
3487  {
3488    synchronized (entryMap)
3489    {
3490      final List<SearchResultEntry> entryList =
3491           new ArrayList<>(entryMap.size());
3492      final List<SearchResultReference> referenceList =
3493           new ArrayList<>(entryMap.size());
3494
3495      final LDAPMessage returnMessage = processSearchRequest(messageID, request,
3496           controls, entryList, referenceList);
3497
3498      for (final SearchResultEntry e : entryList)
3499      {
3500        try
3501        {
3502          connection.sendSearchResultEntry(messageID, e, e.getControls());
3503        }
3504        catch (final LDAPException le)
3505        {
3506          Debug.debugException(le);
3507          return new LDAPMessage(messageID,
3508               new SearchResultDoneProtocolOp(le.getResultCode().intValue(),
3509                    le.getMatchedDN(), le.getDiagnosticMessage(),
3510                    StaticUtils.toList(le.getReferralURLs())),
3511               le.getResponseControls());
3512        }
3513      }
3514
3515      for (final SearchResultReference r : referenceList)
3516      {
3517        try
3518        {
3519          connection.sendSearchResultReference(messageID,
3520               new SearchResultReferenceProtocolOp(
3521                    StaticUtils.toList(r.getReferralURLs())),
3522               r.getControls());
3523        }
3524        catch (final LDAPException le)
3525        {
3526          Debug.debugException(le);
3527          return new LDAPMessage(messageID,
3528               new SearchResultDoneProtocolOp(le.getResultCode().intValue(),
3529                    le.getMatchedDN(), le.getDiagnosticMessage(),
3530                    StaticUtils.toList(le.getReferralURLs())),
3531               le.getResponseControls());
3532        }
3533      }
3534
3535      return returnMessage;
3536    }
3537  }
3538
3539
3540
3541  /**
3542   * Attempts to process the provided search request.  The attempt will fail
3543   * if any of the following conditions is true:
3544   * <UL>
3545   *   <LI>There is a problem with any of the request controls.</LI>
3546   *   <LI>The modify DN request contains a malformed target DN, new RDN, or
3547   *       new superior DN.</LI>
3548   *   <LI>The new DN of the entry would conflict with the DN of an existing
3549   *       entry.</LI>
3550   *   <LI>The new DN of the entry would exist outside the set of defined
3551   *       base DNs.</LI>
3552   *   <LI>The new DN of the entry is not a defined base DN and does not exist
3553   *       immediately below an existing entry.</LI>
3554   * </UL>
3555   *
3556   * @param  messageID      The message ID of the LDAP message containing the
3557   *                        search request.
3558   * @param  request        The search request that was included in the LDAP
3559   *                        message that was received.
3560   * @param  controls       The set of controls included in the LDAP message.
3561   *                        It may be empty if there were no controls, but will
3562   *                        not be {@code null}.
3563   * @param  entryList      A list to which to add search result entries
3564   *                        intended for return to the client.  It must not be
3565   *                        {@code null}.
3566   * @param  referenceList  A list to which to add search result references
3567   *                        intended for return to the client.  It must not be
3568   *                        {@code null}.
3569   *
3570   * @return  The {@link LDAPMessage} containing the response to send to the
3571   *          client.  The protocol op in the {@code LDAPMessage} must be an
3572   *          {@code SearchResultDoneProtocolOp}.
3573   */
3574  @NotNull()
3575  LDAPMessage processSearchRequest(final int messageID,
3576                   @NotNull final SearchRequestProtocolOp request,
3577                   @NotNull final List<Control> controls,
3578                   @NotNull final List<SearchResultEntry> entryList,
3579                   @NotNull final List<SearchResultReference> referenceList)
3580  {
3581    synchronized (entryMap)
3582    {
3583      // Sleep before processing, if appropriate.
3584      final long processingStartTime = System.currentTimeMillis();
3585      sleepBeforeProcessing();
3586
3587      // Look at the filter and see if it contains any unsupported elements.
3588      try
3589      {
3590        ensureFilterSupported(request.getFilter());
3591      }
3592      catch (final LDAPException le)
3593      {
3594        Debug.debugException(le);
3595        return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3596             le.getResultCode().intValue(), null, le.getMessage(), null));
3597      }
3598
3599      // Look at the time limit for the search request and see if sleeping
3600      // would have caused us to exceed that time limit.  It's extremely
3601      // unlikely that any search in the in-memory directory server would take
3602      // a second or more to complete, and that's the minimum time limit that
3603      // can be requested, so there's no need to check the time limit in most
3604      // cases.  However, someone may want to force a "time limit exceeded"
3605      // response by configuring a delay that is greater than the requested time
3606      // limit, so we should check now to see if that's been exceeded.
3607      final long timeLimitMillis = 1000L * request.getTimeLimit();
3608      if (timeLimitMillis > 0L)
3609      {
3610        final long timeLimitExpirationTime =
3611             processingStartTime + timeLimitMillis;
3612        if (System.currentTimeMillis() >= timeLimitExpirationTime)
3613        {
3614          return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3615               ResultCode.TIME_LIMIT_EXCEEDED_INT_VALUE, null,
3616               ERR_MEM_HANDLER_TIME_LIMIT_EXCEEDED.get(), null));
3617        }
3618      }
3619
3620      // Process the provided request controls.
3621      final Map<String,Control> controlMap;
3622      try
3623      {
3624        controlMap = RequestControlPreProcessor.processControls(
3625             LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST, controls);
3626      }
3627      catch (final LDAPException le)
3628      {
3629        Debug.debugException(le);
3630        return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3631             le.getResultCode().intValue(), null, le.getMessage(), null));
3632      }
3633      final ArrayList<Control> responseControls = new ArrayList<>(1);
3634
3635
3636      // If this operation type is not allowed, then reject it.
3637      final boolean isInternalOp =
3638           controlMap.containsKey(OID_INTERNAL_OPERATION_REQUEST_CONTROL);
3639      if ((! isInternalOp) &&
3640           (! config.getAllowedOperationTypes().contains(OperationType.SEARCH)))
3641      {
3642        return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3643             ResultCode.UNWILLING_TO_PERFORM_INT_VALUE, null,
3644             ERR_MEM_HANDLER_SEARCH_NOT_ALLOWED.get(), null));
3645      }
3646
3647
3648      // If this operation type requires authentication, then ensure that the
3649      // client is authenticated.
3650      if ((authenticatedDN.isNullDN() &&
3651           config.getAuthenticationRequiredOperationTypes().contains(
3652                OperationType.SEARCH)))
3653      {
3654        return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3655             ResultCode.INSUFFICIENT_ACCESS_RIGHTS_INT_VALUE, null,
3656             ERR_MEM_HANDLER_SEARCH_REQUIRES_AUTH.get(), null));
3657      }
3658
3659
3660      // Get the parsed base DN.
3661      final DN baseDN;
3662      final Schema schema = schemaRef.get();
3663      try
3664      {
3665        baseDN = new DN(request.getBaseDN(), schema);
3666      }
3667      catch (final LDAPException le)
3668      {
3669        Debug.debugException(le);
3670        return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3671             ResultCode.INVALID_DN_SYNTAX_INT_VALUE, null,
3672             ERR_MEM_HANDLER_SEARCH_MALFORMED_BASE.get(request.getBaseDN(),
3673                  le.getMessage()),
3674             null));
3675      }
3676
3677      // See if the search base or one of its superiors is a smart referral.
3678      final boolean hasManageDsaIT = controlMap.containsKey(
3679           ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID);
3680      if (! hasManageDsaIT)
3681      {
3682        final Entry referralEntry = findNearestReferral(baseDN);
3683        if (referralEntry != null)
3684        {
3685          return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3686               ResultCode.REFERRAL_INT_VALUE, referralEntry.getDN(),
3687               INFO_MEM_HANDLER_REFERRAL_ENCOUNTERED.get(),
3688               getReferralURLs(baseDN, referralEntry)));
3689        }
3690      }
3691
3692      // Make sure that the base entry exists.  It may be the root DSE or
3693      // subschema subentry.
3694      final Entry baseEntry;
3695      boolean includeChangeLog = true;
3696      if (baseDN.isNullDN())
3697      {
3698        baseEntry = generateRootDSE();
3699        includeChangeLog = false;
3700      }
3701      else if (baseDN.equals(subschemaSubentryDN))
3702      {
3703        baseEntry = subschemaSubentryRef.get();
3704      }
3705      else
3706      {
3707        baseEntry = entryMap.get(baseDN);
3708      }
3709
3710      if (baseEntry == null)
3711      {
3712        return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3713             ResultCode.NO_SUCH_OBJECT_INT_VALUE, getMatchedDNString(baseDN),
3714             ERR_MEM_HANDLER_SEARCH_BASE_DOES_NOT_EXIST.get(
3715                  request.getBaseDN()),
3716             null));
3717      }
3718
3719      // Perform any necessary processing for the assertion and proxied auth
3720      // controls.
3721      try
3722      {
3723        handleAssertionRequestControl(controlMap, baseEntry);
3724        handleProxiedAuthControl(controlMap);
3725      }
3726      catch (final LDAPException le)
3727      {
3728        Debug.debugException(le);
3729        return new LDAPMessage(messageID, new SearchResultDoneProtocolOp(
3730             le.getResultCode().intValue(), null, le.getMessage(), null));
3731      }
3732
3733      // Determine whether to include subentries in search results.
3734      final boolean includeSubEntries;
3735      final boolean includeNonSubEntries;
3736      final SearchScope scope = request.getScope();
3737      if (scope == SearchScope.BASE)
3738      {
3739        includeSubEntries = true;
3740        includeNonSubEntries = true;
3741      }
3742      else if (controlMap.containsKey(
3743           DraftLDUPSubentriesRequestControl.SUBENTRIES_REQUEST_OID))
3744      {
3745        includeSubEntries = true;
3746        includeNonSubEntries = false;
3747      }
3748      else if (controlMap.containsKey(
3749           RFC3672SubentriesRequestControl.SUBENTRIES_REQUEST_OID))
3750      {
3751        includeSubEntries = true;
3752
3753        final RFC3672SubentriesRequestControl c =
3754             (RFC3672SubentriesRequestControl) controlMap.get(
3755                  RFC3672SubentriesRequestControl.SUBENTRIES_REQUEST_OID);
3756        includeNonSubEntries = (! c.returnOnlySubEntries());
3757      }
3758      else if (baseEntry.hasObjectClass("ldapSubEntry") ||
3759               baseEntry.hasObjectClass("inheritableLDAPSubEntry"))
3760      {
3761        includeSubEntries = true;
3762        includeNonSubEntries = true;
3763      }
3764      else if (filterIncludesLDAPSubEntry(request.getFilter()))
3765      {
3766        includeSubEntries = true;
3767        includeNonSubEntries = true;
3768      }
3769      else
3770      {
3771        includeSubEntries = false;
3772        includeNonSubEntries = true;
3773      }
3774
3775      // Create a temporary list to hold all of the entries to be returned.
3776      // These entries will not have been pared down based on the requested
3777      // attributes.
3778      final List<Entry> fullEntryList = new ArrayList<>(entryMap.size());
3779
3780findEntriesAndRefs:
3781      {
3782        // Check the scope.  If it is a base-level search, then we only need to
3783        // examine the base entry.  Otherwise, we'll have to scan the entire
3784        // entry map.
3785        final Filter filter = request.getFilter();
3786        if (scope == SearchScope.BASE)
3787        {
3788          try
3789          {
3790            if (filter.matchesEntry(baseEntry, schema))
3791            {
3792              processSearchEntry(baseEntry, includeSubEntries,
3793                   includeNonSubEntries, includeChangeLog, hasManageDsaIT,
3794                   fullEntryList, referenceList);
3795            }
3796          }
3797          catch (final Exception e)
3798          {
3799            Debug.debugException(e);
3800          }
3801
3802          break findEntriesAndRefs;
3803        }
3804
3805        // If the search uses a single-level scope and the base DN is the root
3806        // DSE, then we will only examine the defined base entries for the data
3807        // set.
3808        if ((scope == SearchScope.ONE) && baseDN.isNullDN())
3809        {
3810          for (final DN dn : baseDNs)
3811          {
3812            final Entry e = entryMap.get(dn);
3813            if (e != null)
3814            {
3815              try
3816              {
3817                if (filter.matchesEntry(e, schema))
3818                {
3819                  processSearchEntry(e, includeSubEntries, includeNonSubEntries,
3820                       includeChangeLog, hasManageDsaIT, fullEntryList,
3821                       referenceList);
3822                }
3823              }
3824              catch (final Exception ex)
3825              {
3826                Debug.debugException(ex);
3827              }
3828            }
3829          }
3830
3831          break findEntriesAndRefs;
3832        }
3833
3834
3835        // Try to use indexes to process the request.  If we can't use any
3836        // indexes to get a candidate list, then just iterate over all the
3837        // entries.  It's not necessary to consider the root DSE for non-base
3838        // scopes.
3839        final Set<DN> candidateDNs = indexSearch(filter);
3840        if (candidateDNs == null)
3841        {
3842          for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet())
3843          {
3844            final DN dn = me.getKey();
3845            final Entry entry = me.getValue();
3846            try
3847            {
3848              if (dn.matchesBaseAndScope(baseDN, scope))
3849              {
3850                if (filter.matchesEntry(entry, schema) ||
3851                     (((! hasManageDsaIT) &&
3852                          entry.hasObjectClass("referral") &&
3853                          entry.hasAttribute("ref"))))
3854                {
3855                  processSearchEntry(entry, includeSubEntries,
3856                       includeNonSubEntries, includeChangeLog, hasManageDsaIT,
3857                       fullEntryList, referenceList);
3858                }
3859              }
3860            }
3861            catch (final Exception e)
3862            {
3863              Debug.debugException(e);
3864            }
3865          }
3866        }
3867        else
3868        {
3869          for (final DN dn : candidateDNs)
3870          {
3871            try
3872            {
3873              if (! dn.matchesBaseAndScope(baseDN, scope))
3874              {
3875                continue;
3876              }
3877
3878              final Entry entry = entryMap.get(dn);
3879              if (filter.matchesEntry(entry, schema) ||
3880                   (((! hasManageDsaIT) &&
3881                        entry.hasObjectClass("referral") &&
3882                        entry.hasAttribute("ref"))))
3883              {
3884                processSearchEntry(entry, includeSubEntries,
3885                     includeNonSubEntries, includeChangeLog, hasManageDsaIT,
3886                     fullEntryList, referenceList);
3887              }
3888            }
3889            catch (final Exception e)
3890            {
3891              Debug.debugException(e);
3892            }
3893          }
3894        }
3895      }
3896
3897
3898      // If the request included the server-side sort request control, then sort
3899      // the matching entries appropriately.
3900      final ServerSideSortRequestControl sortRequestControl =
3901           (ServerSideSortRequestControl) controlMap.get(
3902                ServerSideSortRequestControl.SERVER_SIDE_SORT_REQUEST_OID);
3903      if (sortRequestControl != null)
3904      {
3905        final EntrySorter entrySorter = new EntrySorter(false, schema,
3906             sortRequestControl.getSortKeys());
3907        final SortedSet<Entry> sortedEntrySet = entrySorter.sort(fullEntryList);
3908        fullEntryList.clear();
3909        fullEntryList.addAll(sortedEntrySet);
3910
3911        responseControls.add(new ServerSideSortResponseControl(
3912             ResultCode.SUCCESS, null));
3913      }
3914
3915
3916      // If the request included the simple paged results control, then handle
3917      // it.
3918      final SimplePagedResultsControl pagedResultsControl =
3919           (SimplePagedResultsControl)
3920                controlMap.get(SimplePagedResultsControl.PAGED_RESULTS_OID);
3921      if (pagedResultsControl != null)
3922      {
3923        final int totalSize = fullEntryList.size();
3924        final int pageSize = pagedResultsControl.getSize();
3925        final ASN1OctetString cookie = pagedResultsControl.getCookie();
3926
3927        final int offset;
3928        if ((cookie == null) || (cookie.getValueLength() == 0))
3929        {
3930          // This is the first request in the series, so start at the beginning
3931          // of the list.
3932          offset = 0;
3933        }
3934        else
3935        {
3936          // The cookie value will simply be an integer representation of the
3937          // offset within the result list at which to start the next batch.
3938          try
3939          {
3940            final ASN1Integer offsetInteger =
3941                 ASN1Integer.decodeAsInteger(cookie.getValue());
3942            offset = offsetInteger.intValue();
3943          }
3944          catch (final Exception e)
3945          {
3946            Debug.debugException(e);
3947            return new LDAPMessage(messageID,
3948                 new SearchResultDoneProtocolOp(
3949                      ResultCode.PROTOCOL_ERROR_INT_VALUE, null,
3950                      ERR_MEM_HANDLER_MALFORMED_PAGED_RESULTS_COOKIE.get(),
3951                      null),
3952                 responseControls);
3953          }
3954        }
3955
3956        // Create an iterator that will be used to remove entries from the
3957        // result set that are outside of the requested page of results.
3958        int pos = 0;
3959        final Iterator<Entry> iterator = fullEntryList.iterator();
3960
3961        // First, remove entries at the beginning of the list until we hit the
3962        // offset.
3963        while (iterator.hasNext() && (pos < offset))
3964        {
3965          iterator.next();
3966          iterator.remove();
3967          pos++;
3968        }
3969
3970        // Next, skip over the entries that should be returned.
3971        int keptEntries = 0;
3972        while (iterator.hasNext() && (keptEntries < pageSize))
3973        {
3974          iterator.next();
3975          pos++;
3976          keptEntries++;
3977        }
3978
3979        // If there are still entries left, then remove them and create a cookie
3980        // to include in the response.  Otherwise, use an empty cookie.
3981        if (iterator.hasNext())
3982        {
3983          responseControls.add(new SimplePagedResultsControl(totalSize,
3984               new ASN1OctetString(new ASN1Integer(pos).encode()), false));
3985          while (iterator.hasNext())
3986          {
3987            iterator.next();
3988            iterator.remove();
3989          }
3990        }
3991        else
3992        {
3993          responseControls.add(new SimplePagedResultsControl(totalSize,
3994               new ASN1OctetString(), false));
3995        }
3996      }
3997
3998
3999      // If the request includes the virtual list view request control, then
4000      // handle it.
4001      final VirtualListViewRequestControl vlvRequest =
4002           (VirtualListViewRequestControl) controlMap.get(
4003                VirtualListViewRequestControl.VIRTUAL_LIST_VIEW_REQUEST_OID);
4004      if (vlvRequest != null)
4005      {
4006        final int totalEntries = fullEntryList.size();
4007        final ASN1OctetString assertionValue = vlvRequest.getAssertionValue();
4008
4009        // Figure out the position of the target entry in the list.
4010        int offset = vlvRequest.getTargetOffset();
4011        if (assertionValue == null)
4012        {
4013          // The offset is one-based, so we need to adjust it for the list's
4014          // zero-based offset.  Also, make sure to put it within the bounds of
4015          // the list.
4016          offset--;
4017          offset = Math.max(0, offset);
4018          offset = Math.min(fullEntryList.size(), offset);
4019        }
4020        else
4021        {
4022          final SortKey primarySortKey = sortRequestControl.getSortKeys()[0];
4023
4024          final Entry testEntry = new Entry("cn=test", schema,
4025               new Attribute(primarySortKey.getAttributeName(),
4026                    assertionValue));
4027
4028          final EntrySorter entrySorter =
4029               new EntrySorter(false, schema, primarySortKey);
4030
4031          offset = fullEntryList.size();
4032          for (int i=0; i < fullEntryList.size(); i++)
4033          {
4034            if (entrySorter.compare(fullEntryList.get(i), testEntry) >= 0)
4035            {
4036              offset = i;
4037              break;
4038            }
4039          }
4040        }
4041
4042        // Get the start and end positions based on the before and after counts.
4043        final int beforeCount = Math.max(0, vlvRequest.getBeforeCount());
4044        final int afterCount  = Math.max(0, vlvRequest.getAfterCount());
4045
4046        final int start = Math.max(0, (offset - beforeCount));
4047        final int end =
4048             Math.min(fullEntryList.size(), (offset + afterCount + 1));
4049
4050        // Create an iterator to use to alter the list so that it only contains
4051        // the appropriate set of entries.
4052        int pos = 0;
4053        final Iterator<Entry> iterator = fullEntryList.iterator();
4054        while (iterator.hasNext())
4055        {
4056          iterator.next();
4057          if ((pos < start) || (pos >= end))
4058          {
4059            iterator.remove();
4060          }
4061          pos++;
4062        }
4063
4064        // Create the appropriate response control.
4065        responseControls.add(new VirtualListViewResponseControl((offset+1),
4066             totalEntries, ResultCode.SUCCESS, null));
4067      }
4068
4069
4070      // Process the set of requested attributes so that we can pare down the
4071      // entries.
4072      final SearchEntryParer parer = new SearchEntryParer(
4073           request.getAttributes(), schema);
4074      final int sizeLimit;
4075      if (request.getSizeLimit() > 0)
4076      {
4077        sizeLimit = Math.min(request.getSizeLimit(), maxSizeLimit);
4078      }
4079      else
4080      {
4081        sizeLimit = maxSizeLimit;
4082      }
4083
4084      int entryCount = 0;
4085      for (final Entry e : fullEntryList)
4086      {
4087        entryCount++;
4088        if (entryCount > sizeLimit)
4089        {
4090          return new LDAPMessage(messageID,
4091               new SearchResultDoneProtocolOp(
4092                    ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE, null,
4093                    ERR_MEM_HANDLER_SEARCH_SIZE_LIMIT_EXCEEDED.get(), null),
4094               responseControls);
4095        }
4096
4097        final Entry trimmedEntry = parer.pareEntry(e);
4098        if (request.typesOnly())
4099        {
4100          final Entry typesOnlyEntry = new Entry(trimmedEntry.getDN(), schema);
4101          for (final Attribute a : trimmedEntry.getAttributes())
4102          {
4103            typesOnlyEntry.addAttribute(new Attribute(a.getName()));
4104          }
4105          entryList.add(new SearchResultEntry(typesOnlyEntry));
4106        }
4107        else
4108        {
4109          entryList.add(new SearchResultEntry(trimmedEntry));
4110        }
4111      }
4112
4113      return new LDAPMessage(messageID,
4114           new SearchResultDoneProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
4115                null, null),
4116           responseControls);
4117    }
4118  }
4119
4120
4121
4122  /**
4123   * Ensures that the provided filter is supported in the in-memory directory
4124   * server.
4125   *
4126   * @param  filter  The filter being validated.
4127   *
4128   * @throws  LDAPException  If the provided filter is not acceptable.
4129   */
4130  private static void ensureFilterSupported(@NotNull final Filter filter)
4131          throws LDAPException
4132  {
4133    switch (filter.getFilterType())
4134    {
4135      case Filter.FILTER_TYPE_AND:
4136      case Filter.FILTER_TYPE_OR:
4137        // Make sure that all of the embedded components are supported.
4138        for (final Filter component : filter.getComponents())
4139        {
4140          ensureFilterSupported(component);
4141        }
4142        return;
4143
4144      case Filter.FILTER_TYPE_NOT:
4145        // Make sure that the embedded component is supported.
4146        ensureFilterSupported(filter.getNOTComponent());
4147        return;
4148
4149      case Filter.FILTER_TYPE_EQUALITY:
4150      case Filter.FILTER_TYPE_SUBSTRING:
4151      case Filter.FILTER_TYPE_GREATER_OR_EQUAL:
4152      case Filter.FILTER_TYPE_LESS_OR_EQUAL:
4153      case Filter.FILTER_TYPE_PRESENCE:
4154        // These are always acceptable.
4155        return;
4156
4157      case Filter.FILTER_TYPE_APPROXIMATE_MATCH:
4158        // Approximate match filters are never supported.
4159        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
4160             ERR_MEM_HANDLER_FILTER_UNSUPPORTED_APPROXIMATE_MATCH_FILTER.get());
4161
4162      case Filter.FILTER_TYPE_EXTENSIBLE_MATCH:
4163        // Extensible match filters are never supported.
4164        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
4165             ERR_MEM_HANDLER_FILTER_UNSUPPORTED_EXTENSIBLE_MATCH_FILTER.get());
4166
4167      default:
4168        // Unrecognized filter types are never supported.
4169        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
4170             ERR_MEM_HANDLER_FILTER_UNRECOGNIZED_FILTER_TYPE.get(
4171                  StaticUtils.toHex(filter.getFilterType())));
4172    }
4173  }
4174
4175
4176
4177  /**
4178   * Indicates whether the provided filter includes a component that targets the
4179   * ldapSubEntry object class.
4180   *
4181   * @param  filter  The filter for which to make the determination.
4182   *
4183   * @return  {@code true} if the provided filter includes a component that
4184   *          targets the ldapSubEntry object class or {@code false} if not.
4185   */
4186  private static boolean filterIncludesLDAPSubEntry(
4187                              @NotNull final Filter filter)
4188  {
4189    switch (filter.getFilterType())
4190    {
4191      case Filter.FILTER_TYPE_AND:
4192      case Filter.FILTER_TYPE_OR:
4193        for (final Filter f : filter.getComponents())
4194        {
4195          if (filterIncludesLDAPSubEntry(f))
4196          {
4197            return true;
4198          }
4199        }
4200        return false;
4201
4202      case Filter.FILTER_TYPE_EQUALITY:
4203        return  (filter.getAttributeName().equalsIgnoreCase("objectClass") ||
4204             filter.getAttributeName().equals("2.5.4.0"));
4205
4206      default:
4207        return false;
4208    }
4209  }
4210
4211
4212
4213  /**
4214   * Performs any necessary index processing to add the provided entry.
4215   *
4216   * @param  entry  The entry that has been added.
4217   */
4218  private void indexAdd(@NotNull final Entry entry)
4219  {
4220    for (final InMemoryDirectoryServerEqualityAttributeIndex i :
4221         equalityIndexes.values())
4222    {
4223      try
4224      {
4225        i.processAdd(entry);
4226      }
4227      catch (final LDAPException le)
4228      {
4229        Debug.debugException(le);
4230      }
4231    }
4232  }
4233
4234
4235
4236  /**
4237   * Performs any necessary index processing to delete the provided entry.
4238   *
4239   * @param  entry  The entry that has been deleted.
4240   */
4241  private void indexDelete(@NotNull final Entry entry)
4242  {
4243    for (final InMemoryDirectoryServerEqualityAttributeIndex i :
4244         equalityIndexes.values())
4245    {
4246      try
4247      {
4248        i.processDelete(entry);
4249      }
4250      catch (final LDAPException le)
4251      {
4252        Debug.debugException(le);
4253      }
4254    }
4255  }
4256
4257
4258
4259  /**
4260   * Attempts to use indexes to obtain a candidate list for the provided filter.
4261   *
4262   * @param  filter  The filter to be processed.
4263   *
4264   * @return  The DNs of entries which may match the given filter, or
4265   *          {@code null} if the filter is not indexed.
4266   */
4267  @Nullable()
4268  private Set<DN> indexSearch(@NotNull final Filter filter)
4269  {
4270    switch (filter.getFilterType())
4271    {
4272      case Filter.FILTER_TYPE_AND:
4273        Filter[] comps = filter.getComponents();
4274        if (comps.length == 0)
4275        {
4276          return null;
4277        }
4278        else if (comps.length == 1)
4279        {
4280          return indexSearch(comps[0]);
4281        }
4282        else
4283        {
4284          Set<DN> candidateSet = null;
4285          for (final Filter f : comps)
4286          {
4287            final Set<DN> dnSet = indexSearch(f);
4288            if (dnSet != null)
4289            {
4290              if (candidateSet == null)
4291              {
4292                candidateSet = new TreeSet<>(dnSet);
4293              }
4294              else
4295              {
4296                candidateSet.retainAll(dnSet);
4297              }
4298            }
4299          }
4300          return candidateSet;
4301        }
4302
4303      case Filter.FILTER_TYPE_OR:
4304        comps = filter.getComponents();
4305        if (comps.length == 0)
4306        {
4307          return Collections.emptySet();
4308        }
4309        else if (comps.length == 1)
4310        {
4311          return indexSearch(comps[0]);
4312        }
4313        else
4314        {
4315          Set<DN> candidateSet = null;
4316          for (final Filter f : comps)
4317          {
4318            final Set<DN> dnSet = indexSearch(f);
4319            if (dnSet == null)
4320            {
4321              return null;
4322            }
4323
4324            if (candidateSet == null)
4325            {
4326              candidateSet = new TreeSet<>(dnSet);
4327            }
4328            else
4329            {
4330              candidateSet.addAll(dnSet);
4331            }
4332          }
4333          return candidateSet;
4334        }
4335
4336      case Filter.FILTER_TYPE_EQUALITY:
4337        final Schema schema = schemaRef.get();
4338        if (schema == null)
4339        {
4340          return null;
4341        }
4342        final AttributeTypeDefinition at =
4343             schema.getAttributeType(filter.getAttributeName());
4344        if (at == null)
4345        {
4346          return null;
4347        }
4348        final InMemoryDirectoryServerEqualityAttributeIndex i =
4349             equalityIndexes.get(at);
4350        if (i == null)
4351        {
4352          return null;
4353        }
4354        try
4355        {
4356          return i.getMatchingEntries(filter.getRawAssertionValue());
4357        }
4358        catch (final Exception e)
4359        {
4360          Debug.debugException(e);
4361          return null;
4362        }
4363
4364      default:
4365        return null;
4366    }
4367  }
4368
4369
4370
4371  /**
4372   * Determines whether the provided set of controls includes a transaction
4373   * specification request control.  If so, then it will verify that it
4374   * references a valid transaction for the client.  If the request is part of a
4375   * valid transaction, then the transaction specification request control will
4376   * be removed and the request will be stashed in the client connection state
4377   * so that it can be retrieved and processed when the transaction is
4378   * committed.
4379   *
4380   * @param  messageID  The message ID for the request to be processed.
4381   * @param  request    The protocol op for the request to be processed.
4382   * @param  controls   The set of controls for the request to be processed.
4383   *
4384   * @return  The transaction ID for the associated transaction, or {@code null}
4385   *          if the request is not part of any transaction.
4386   *
4387   * @throws  LDAPException  If the transaction specification request control is
4388   *                         present but does not refer to a valid transaction
4389   *                         for the associated client connection.
4390   */
4391  @SuppressWarnings("unchecked")
4392  @Nullable()
4393  private ASN1OctetString processTransactionRequest(final int messageID,
4394                               @NotNull final ProtocolOp request,
4395                               @NotNull final Map<String,Control> controls)
4396          throws LDAPException
4397  {
4398    final TransactionSpecificationRequestControl txnControl =
4399         (TransactionSpecificationRequestControl)
4400         controls.remove(TransactionSpecificationRequestControl.
4401              TRANSACTION_SPECIFICATION_REQUEST_OID);
4402    if (txnControl == null)
4403    {
4404      return null;
4405    }
4406
4407    // See if the client has an active transaction.  If not, then fail.
4408    final ASN1OctetString txnID = txnControl.getTransactionID();
4409    final ObjectPair<ASN1OctetString,List<LDAPMessage>> txnInfo =
4410         (ObjectPair<ASN1OctetString,List<LDAPMessage>>) connectionState.get(
4411              TransactionExtendedOperationHandler.STATE_VARIABLE_TXN_INFO);
4412    if (txnInfo == null)
4413    {
4414      throw new LDAPException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
4415           ERR_MEM_HANDLER_TXN_CONTROL_WITHOUT_TXN.get(txnID.stringValue()));
4416    }
4417
4418
4419    // Make sure that the active transaction has a transaction ID that matches
4420    // the transaction ID from the control.  If not, then abort the existing
4421    // transaction and fail.
4422    final ASN1OctetString existingTxnID = txnInfo.getFirst();
4423    if (! txnID.stringValue().equals(existingTxnID.stringValue()))
4424    {
4425      connectionState.remove(
4426           TransactionExtendedOperationHandler.STATE_VARIABLE_TXN_INFO);
4427      connection.sendUnsolicitedNotification(
4428           new AbortedTransactionExtendedResult(existingTxnID,
4429                ResultCode.CONSTRAINT_VIOLATION,
4430                ERR_MEM_HANDLER_TXN_ABORTED_BY_CONTROL_TXN_ID_MISMATCH.get(
4431                     existingTxnID.stringValue(), txnID.stringValue()),
4432                null, null, null));
4433      throw new LDAPException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
4434           ERR_MEM_HANDLER_TXN_CONTROL_ID_MISMATCH.get(txnID.stringValue(),
4435                existingTxnID.stringValue()));
4436    }
4437
4438
4439    // Stash the request in the transaction state information so that it will
4440    // be processed when the transaction is committed.
4441    txnInfo.getSecond().add(new LDAPMessage(messageID, request,
4442         new ArrayList<>(controls.values())));
4443
4444    return txnID;
4445  }
4446
4447
4448
4449  /**
4450   * Sleeps for a period of time (if appropriate) before beginning processing
4451   * for an operation.
4452   */
4453  private void sleepBeforeProcessing()
4454  {
4455    final long delay = processingDelayMillis.get();
4456    if (delay > 0)
4457    {
4458      try
4459      {
4460        Thread.sleep(delay);
4461      }
4462      catch (final Exception e)
4463      {
4464        Debug.debugException(e);
4465
4466        if (e instanceof InterruptedException)
4467        {
4468          Thread.currentThread().interrupt();
4469        }
4470      }
4471    }
4472  }
4473
4474
4475
4476  /**
4477   * Retrieves the configured list of password attributes.
4478   *
4479   * @return  The configured list of password attributes.
4480   */
4481  @NotNull()
4482  public List<String> getPasswordAttributes()
4483  {
4484    return configuredPasswordAttributes;
4485  }
4486
4487
4488
4489  /**
4490   * Retrieves the primary password encoder that has been configured for the
4491   * server.
4492   *
4493   * @return  The primary password encoder that has been configured for the
4494   *          server.
4495   */
4496  @Nullable()
4497  public InMemoryPasswordEncoder getPrimaryPasswordEncoder()
4498  {
4499    return primaryPasswordEncoder;
4500  }
4501
4502
4503
4504  /**
4505   * Retrieves a list of all password encoders configured for the server.
4506   *
4507   * @return  A list of all password encoders configured for the server.
4508   */
4509  @NotNull()
4510  public List<InMemoryPasswordEncoder> getAllPasswordEncoders()
4511  {
4512    return passwordEncoders;
4513  }
4514
4515
4516
4517  /**
4518   * Retrieves a list of the passwords contained in the provided entry.
4519   *
4520   * @param  entry                 The entry from which to obtain the list of
4521   *                               passwords.  It must not be {@code null}.
4522   * @param  clearPasswordToMatch  An optional clear-text password that should
4523   *                               match the values that are returned.  If this
4524   *                               is {@code null}, then all passwords contained
4525   *                               in the provided entry will be returned.  If
4526   *                               this is non-{@code null}, then only passwords
4527   *                               matching the clear-text password will be
4528   *                               returned.
4529   *
4530   * @return  A list of the passwords contained in the provided entry,
4531   *          optionally restricted to those matching the provided clear-text
4532   *          password, or an empty list if the entry does not contain any
4533   *          passwords.
4534   */
4535  @NotNull()
4536  public List<InMemoryDirectoryServerPassword> getPasswordsInEntry(
4537              @NotNull final Entry entry,
4538              @Nullable final ASN1OctetString clearPasswordToMatch)
4539  {
4540    final ArrayList<InMemoryDirectoryServerPassword> passwordList =
4541         new ArrayList<>(5);
4542    final ReadOnlyEntry readOnlyEntry = new ReadOnlyEntry(entry);
4543
4544    for (final String passwordAttributeName : configuredPasswordAttributes)
4545    {
4546      final List<Attribute> passwordAttributeList =
4547           entry.getAttributesWithOptions(passwordAttributeName, null);
4548
4549      for (final Attribute passwordAttribute : passwordAttributeList)
4550      {
4551        for (final ASN1OctetString value : passwordAttribute.getRawValues())
4552        {
4553          final InMemoryDirectoryServerPassword password =
4554               new InMemoryDirectoryServerPassword(value, readOnlyEntry,
4555                    passwordAttribute.getName(), passwordEncoders);
4556
4557          if (clearPasswordToMatch != null)
4558          {
4559            try
4560            {
4561              if (! password.matchesClearPassword(clearPasswordToMatch))
4562              {
4563                continue;
4564              }
4565            }
4566            catch (final Exception e)
4567            {
4568              Debug.debugException(e);
4569              continue;
4570            }
4571          }
4572
4573          passwordList.add(new InMemoryDirectoryServerPassword(value,
4574               readOnlyEntry, passwordAttribute.getName(), passwordEncoders));
4575        }
4576      }
4577    }
4578
4579    return passwordList;
4580  }
4581
4582
4583
4584  /**
4585   * Retrieves the number of entries currently held in the server.
4586   *
4587   * @param  includeChangeLog  Indicates whether to include entries that are
4588   *                           part of the changelog in the count.
4589   *
4590   * @return  The number of entries currently held in the server.
4591   */
4592  public int countEntries(final boolean includeChangeLog)
4593  {
4594    synchronized (entryMap)
4595    {
4596      if (includeChangeLog || (maxChangelogEntries == 0))
4597      {
4598        return entryMap.size();
4599      }
4600      else
4601      {
4602        int count = 0;
4603
4604        for (final DN dn : entryMap.keySet())
4605        {
4606          if (! dn.isDescendantOf(changeLogBaseDN, true))
4607          {
4608            count++;
4609          }
4610        }
4611
4612        return count;
4613      }
4614    }
4615  }
4616
4617
4618
4619  /**
4620   * Retrieves the number of entries currently held in the server whose DN
4621   * matches or is subordinate to the provided base DN.
4622   *
4623   * @param  baseDN  The base DN to use for the determination.
4624   *
4625   * @return  The number of entries currently held in the server whose DN
4626   *          matches or is subordinate to the provided base DN.
4627   *
4628   * @throws  LDAPException  If the provided string cannot be parsed as a valid
4629   *                         DN.
4630   */
4631  public int countEntriesBelow(@NotNull final String baseDN)
4632         throws LDAPException
4633  {
4634    synchronized (entryMap)
4635    {
4636      final DN parsedBaseDN = new DN(baseDN, schemaRef.get());
4637
4638      int count = 0;
4639      for (final DN dn : entryMap.keySet())
4640      {
4641        if (dn.isDescendantOf(parsedBaseDN, true))
4642        {
4643          count++;
4644        }
4645      }
4646
4647      return count;
4648    }
4649  }
4650
4651
4652
4653  /**
4654   * Removes all entries currently held in the server.  If a changelog is
4655   * enabled, then all changelog entries will also be cleared but the base
4656   * "cn=changelog" entry will be retained.
4657   */
4658  public void clear()
4659  {
4660    synchronized (entryMap)
4661    {
4662      restoreSnapshot(initialSnapshot);
4663    }
4664  }
4665
4666
4667
4668  /**
4669   * Reads entries from the provided LDIF reader and adds them to the server,
4670   * optionally clearing any existing entries before beginning to add the new
4671   * entries.  If an error is encountered while adding entries from LDIF then
4672   * the server will remain populated with the data it held before the import
4673   * attempt (even if the {@code clear} is given with a value of {@code true}).
4674   *
4675   * @param  clear       Indicates whether to remove all existing entries prior
4676   *                     to adding entries read from LDIF.
4677   * @param  ldifReader  The LDIF reader to use to obtain the entries to be
4678   *                     imported.  It will be closed by this method.
4679   *
4680   * @return  The number of entries read from LDIF and added to the server.
4681   *
4682   * @throws  LDAPException  If a problem occurs while reading entries or adding
4683   *                         them to the server.
4684   */
4685  public int importFromLDIF(final boolean clear,
4686                            @NotNull final LDIFReader ldifReader)
4687         throws LDAPException
4688  {
4689    synchronized (entryMap)
4690    {
4691      final InMemoryDirectoryServerSnapshot snapshot = createSnapshot();
4692      boolean restoreSnapshot = true;
4693
4694      try
4695      {
4696        if (clear)
4697        {
4698          restoreSnapshot(initialSnapshot);
4699        }
4700
4701        int entriesAdded = 0;
4702        while (true)
4703        {
4704          final Entry entry;
4705          try
4706          {
4707            entry = ldifReader.readEntry();
4708            if (entry == null)
4709            {
4710              restoreSnapshot = false;
4711              return entriesAdded;
4712            }
4713          }
4714          catch (final LDIFException le)
4715          {
4716            Debug.debugException(le);
4717            throw new LDAPException(ResultCode.LOCAL_ERROR,
4718                 ERR_MEM_HANDLER_INIT_FROM_LDIF_READ_ERROR.get(le.getMessage()),
4719                 le);
4720          }
4721          catch (final Exception e)
4722          {
4723            Debug.debugException(e);
4724            throw new LDAPException(ResultCode.LOCAL_ERROR,
4725                 ERR_MEM_HANDLER_INIT_FROM_LDIF_READ_ERROR.get(
4726                      StaticUtils.getExceptionMessage(e)),
4727                 e);
4728          }
4729
4730          addEntry(entry, true);
4731          entriesAdded++;
4732        }
4733      }
4734      finally
4735      {
4736        try
4737        {
4738          ldifReader.close();
4739        }
4740        catch (final Exception e)
4741        {
4742          Debug.debugException(e);
4743        }
4744
4745        if (restoreSnapshot)
4746        {
4747          restoreSnapshot(snapshot);
4748        }
4749      }
4750    }
4751  }
4752
4753
4754
4755  /**
4756   * Writes all entries contained in the server to LDIF using the provided
4757   * writer.
4758   *
4759   * @param  ldifWriter             The LDIF writer to use when writing the
4760   *                                entries.  It must not be {@code null}.
4761   * @param  excludeGeneratedAttrs  Indicates whether to exclude automatically
4762   *                                generated operational attributes like
4763   *                                entryUUID, entryDN, creatorsName, etc.
4764   * @param  excludeChangeLog       Indicates whether to exclude entries
4765   *                                contained in the changelog.
4766   * @param  closeWriter            Indicates whether the LDIF writer should be
4767   *                                closed after all entries have been written.
4768   *
4769   * @return  The number of entries written to LDIF.
4770   *
4771   * @throws  LDAPException  If a problem is encountered while attempting to
4772   *                         write an entry to LDIF.
4773   */
4774  public int exportToLDIF(@NotNull final LDIFWriter ldifWriter,
4775                          final boolean excludeGeneratedAttrs,
4776                          final boolean excludeChangeLog,
4777                          final boolean closeWriter)
4778         throws LDAPException
4779  {
4780    synchronized (entryMap)
4781    {
4782      boolean exceptionThrown = false;
4783
4784      try
4785      {
4786        int entriesWritten = 0;
4787
4788        for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet())
4789        {
4790          final DN dn = me.getKey();
4791          if (excludeChangeLog && dn.isDescendantOf(changeLogBaseDN, true))
4792          {
4793            continue;
4794          }
4795
4796          final Entry entry;
4797          if (excludeGeneratedAttrs)
4798          {
4799            entry = me.getValue().duplicate();
4800            entry.removeAttribute("entryDN");
4801            entry.removeAttribute("entryUUID");
4802            entry.removeAttribute("subschemaSubentry");
4803            entry.removeAttribute("creatorsName");
4804            entry.removeAttribute("createTimestamp");
4805            entry.removeAttribute("modifiersName");
4806            entry.removeAttribute("modifyTimestamp");
4807          }
4808          else
4809          {
4810            entry = me.getValue();
4811          }
4812
4813          try
4814          {
4815            ldifWriter.writeEntry(entry);
4816            entriesWritten++;
4817          }
4818          catch (final Exception e)
4819          {
4820            Debug.debugException(e);
4821            exceptionThrown = true;
4822            throw new LDAPException(ResultCode.LOCAL_ERROR,
4823                 ERR_MEM_HANDLER_LDIF_WRITE_ERROR.get(entry.getDN(),
4824                      StaticUtils.getExceptionMessage(e)),
4825                 e);
4826          }
4827        }
4828
4829        return entriesWritten;
4830      }
4831      finally
4832      {
4833        if (closeWriter)
4834        {
4835          try
4836          {
4837            ldifWriter.close();
4838          }
4839          catch (final Exception e)
4840          {
4841            Debug.debugException(e);
4842            if (! exceptionThrown)
4843            {
4844              throw new LDAPException(ResultCode.LOCAL_ERROR,
4845                   ERR_MEM_HANDLER_LDIF_WRITE_CLOSE_ERROR.get(
4846                        StaticUtils.getExceptionMessage(e)),
4847                   e);
4848            }
4849          }
4850        }
4851      }
4852    }
4853  }
4854
4855
4856
4857  /**
4858   * Reads entries from the provided LDIF reader and adds them to the server,
4859   * optionally clearing any existing entries before beginning to add the new
4860   * entries.  If an error is encountered while adding entries from LDIF then
4861   * the server will remain populated with the data it held before the import
4862   * attempt (even if the {@code clear} is given with a value of {@code true}).
4863   * <BR><BR>
4864   * This method may be used regardless of whether the server is listening for
4865   * client connections.
4866   *
4867   * @param  ldifReader  The LDIF reader to use to obtain the change records to
4868   *                     be applied.
4869   *
4870   * @return  The number of changes applied from the LDIF file.
4871   *
4872   * @throws  LDAPException  If a problem occurs while reading change records
4873   *                         or applying them to the server.
4874   */
4875  public int applyChangesFromLDIF(@NotNull final LDIFReader ldifReader)
4876         throws LDAPException
4877  {
4878    synchronized (entryMap)
4879    {
4880      final InMemoryDirectoryServerSnapshot snapshot = createSnapshot();
4881      boolean restoreSnapshot = true;
4882
4883      try
4884      {
4885        int changesApplied = 0;
4886        while (true)
4887        {
4888          final LDIFChangeRecord changeRecord;
4889          try
4890          {
4891            changeRecord = ldifReader.readChangeRecord(true);
4892            if (changeRecord == null)
4893            {
4894              restoreSnapshot = false;
4895              return changesApplied;
4896            }
4897          }
4898          catch (final LDIFException le)
4899          {
4900            Debug.debugException(le);
4901            throw new LDAPException(ResultCode.LOCAL_ERROR,
4902                 ERR_MEM_HANDLER_APPLY_CHANGES_FROM_LDIF_READ_ERROR.get(
4903                      le.getMessage()),
4904                 le);
4905          }
4906          catch (final Exception e)
4907          {
4908            Debug.debugException(e);
4909            throw new LDAPException(ResultCode.LOCAL_ERROR,
4910                 ERR_MEM_HANDLER_APPLY_CHANGES_FROM_LDIF_READ_ERROR.get(
4911                      StaticUtils.getExceptionMessage(e)),
4912                 e);
4913          }
4914
4915          if (changeRecord instanceof LDIFAddChangeRecord)
4916          {
4917            final LDIFAddChangeRecord addChangeRecord =
4918                 (LDIFAddChangeRecord) changeRecord;
4919            add(addChangeRecord.toAddRequest());
4920          }
4921          else if (changeRecord instanceof LDIFDeleteChangeRecord)
4922          {
4923            final LDIFDeleteChangeRecord deleteChangeRecord =
4924                 (LDIFDeleteChangeRecord) changeRecord;
4925            delete(deleteChangeRecord.toDeleteRequest());
4926          }
4927          else if (changeRecord instanceof LDIFModifyChangeRecord)
4928          {
4929            final LDIFModifyChangeRecord modifyChangeRecord =
4930                 (LDIFModifyChangeRecord) changeRecord;
4931            modify(modifyChangeRecord.toModifyRequest());
4932          }
4933          else if (changeRecord instanceof LDIFModifyDNChangeRecord)
4934          {
4935            final LDIFModifyDNChangeRecord modifyDNChangeRecord =
4936                 (LDIFModifyDNChangeRecord) changeRecord;
4937            modifyDN(modifyDNChangeRecord.toModifyDNRequest());
4938          }
4939          else
4940          {
4941            throw new LDAPException(ResultCode.LOCAL_ERROR,
4942                 ERR_MEM_HANDLER_APPLY_CHANGES_UNSUPPORTED_CHANGE.get(
4943                      String.valueOf(changeRecord)));
4944          }
4945
4946          changesApplied++;
4947        }
4948      }
4949      finally
4950      {
4951        try
4952        {
4953          ldifReader.close();
4954        }
4955        catch (final Exception e)
4956        {
4957          Debug.debugException(e);
4958        }
4959
4960        if (restoreSnapshot)
4961        {
4962          restoreSnapshot(snapshot);
4963        }
4964      }
4965    }
4966  }
4967
4968
4969
4970  /**
4971   * Attempts to add the provided entry to the in-memory data set.  The attempt
4972   * will fail if any of the following conditions is true:
4973   * <UL>
4974   *   <LI>The provided entry has a malformed DN.</LI>
4975   *   <LI>The provided entry has the null DN.</LI>
4976   *   <LI>The provided entry has a DN that is the same as or subordinate to the
4977   *       subschema subentry.</LI>
4978   *   <LI>An entry already exists with the same DN as the entry in the provided
4979   *       request.</LI>
4980   *   <LI>The entry is outside the set of base DNs for the server.</LI>
4981   *   <LI>The entry is below one of the defined base DNs but the immediate
4982   *       parent entry does not exist.</LI>
4983   *   <LI>If a schema was provided, and the entry is not valid according to the
4984   *       constraints of that schema.</LI>
4985   * </UL>
4986   *
4987   * @param  entry                     The entry to be added.  It must not be
4988   *                                   {@code null}.
4989   * @param  ignoreNoUserModification  Indicates whether to ignore constraints
4990   *                                   normally imposed by the
4991   *                                   NO-USER-MODIFICATION element in attribute
4992   *                                   type definitions.
4993   *
4994   * @throws  LDAPException  If a problem occurs while attempting to add the
4995   *                         provided entry.
4996   */
4997  public void addEntry(@NotNull final Entry entry,
4998                       final boolean ignoreNoUserModification)
4999         throws LDAPException
5000  {
5001    final List<Control> controls;
5002    if (ignoreNoUserModification)
5003    {
5004      controls = new ArrayList<>(1);
5005      controls.add(new Control(OID_INTERNAL_OPERATION_REQUEST_CONTROL, false));
5006    }
5007    else
5008    {
5009      controls = Collections.emptyList();
5010    }
5011
5012    final AddRequestProtocolOp addRequest = new AddRequestProtocolOp(
5013         entry.getDN(), new ArrayList<>(entry.getAttributes()));
5014
5015    final LDAPMessage resultMessage =
5016         processAddRequest(-1, addRequest, controls);
5017
5018    final AddResponseProtocolOp addResponse =
5019         resultMessage.getAddResponseProtocolOp();
5020    if (addResponse.getResultCode() != ResultCode.SUCCESS_INT_VALUE)
5021    {
5022      throw new LDAPException(ResultCode.valueOf(addResponse.getResultCode()),
5023           addResponse.getDiagnosticMessage(), addResponse.getMatchedDN(),
5024           stringListToArray(addResponse.getReferralURLs()));
5025    }
5026  }
5027
5028
5029
5030  /**
5031   * Attempts to add all of the provided entries to the server.  If an error is
5032   * encountered during processing, then the contents of the server will be the
5033   * same as they were before this method was called.
5034   *
5035   * @param  entries  The collection of entries to be added.
5036   *
5037   * @throws  LDAPException  If a problem was encountered while attempting to
5038   *                         add any of the entries to the server.
5039   */
5040  public void addEntries(@NotNull final List<? extends Entry> entries)
5041         throws LDAPException
5042  {
5043    synchronized (entryMap)
5044    {
5045      final InMemoryDirectoryServerSnapshot snapshot = createSnapshot();
5046      boolean restoreSnapshot = true;
5047
5048      try
5049      {
5050        for (final Entry e : entries)
5051        {
5052          addEntry(e, false);
5053        }
5054        restoreSnapshot = false;
5055      }
5056      finally
5057      {
5058        if (restoreSnapshot)
5059        {
5060          restoreSnapshot(snapshot);
5061        }
5062      }
5063    }
5064  }
5065
5066
5067
5068  /**
5069   * Removes the entry with the specified DN and any subordinate entries it may
5070   * have.
5071   *
5072   * @param  baseDN  The DN of the entry to be deleted.  It must not be
5073   *                 {@code null} or represent the null DN.
5074   *
5075   * @return  The number of entries actually removed, or zero if the specified
5076   *          base DN does not represent an entry in the server.
5077   *
5078   * @throws  LDAPException  If the provided base DN is not a valid DN, or is
5079   *                         the DN of an entry that cannot be deleted (e.g.,
5080   *                         the null DN).
5081   */
5082  public int deleteSubtree(@NotNull final String baseDN)
5083         throws LDAPException
5084  {
5085    synchronized (entryMap)
5086    {
5087      final DN dn = new DN(baseDN, schemaRef.get());
5088      if (dn.isNullDN())
5089      {
5090        throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
5091             ERR_MEM_HANDLER_DELETE_ROOT_DSE.get());
5092      }
5093
5094      int numDeleted = 0;
5095
5096      final Iterator<Map.Entry<DN,ReadOnlyEntry>> iterator =
5097           entryMap.entrySet().iterator();
5098      while (iterator.hasNext())
5099      {
5100        final Map.Entry<DN,ReadOnlyEntry> e = iterator.next();
5101        if (e.getKey().isDescendantOf(dn, true))
5102        {
5103          iterator.remove();
5104          numDeleted++;
5105        }
5106      }
5107
5108      return numDeleted;
5109    }
5110  }
5111
5112
5113
5114  /**
5115   * Attempts to apply the provided set of modifications to the specified entry.
5116   * The attempt will fail if any of the following conditions is true:
5117   * <UL>
5118   *   <LI>The target DN is malformed.</LI>
5119   *   <LI>The target entry is the root DSE.</LI>
5120   *   <LI>The target entry is the subschema subentry.</LI>
5121   *   <LI>The target entry does not exist.</LI>
5122   *   <LI>Any of the modifications cannot be applied to the entry.</LI>
5123   *   <LI>If a schema was provided, and the entry violates any of the
5124   *       constraints of that schema.</LI>
5125   * </UL>
5126   *
5127   * @param  dn    The DN of the entry to be modified.
5128   * @param  mods  The set of modifications to be applied to the entry.
5129   *
5130   * @throws  LDAPException  If a problem is encountered while attempting to
5131   *                         update the specified entry.
5132   */
5133  public void modifyEntry(@NotNull final String dn,
5134                          @NotNull final List<Modification> mods)
5135         throws LDAPException
5136  {
5137    final ModifyRequestProtocolOp modifyRequest =
5138         new ModifyRequestProtocolOp(dn, mods);
5139
5140    final LDAPMessage resultMessage = processModifyRequest(-1, modifyRequest,
5141         Collections.<Control>emptyList());
5142
5143    final ModifyResponseProtocolOp modifyResponse =
5144         resultMessage.getModifyResponseProtocolOp();
5145    if (modifyResponse.getResultCode() != ResultCode.SUCCESS_INT_VALUE)
5146    {
5147      throw new LDAPException(
5148           ResultCode.valueOf(modifyResponse.getResultCode()),
5149           modifyResponse.getDiagnosticMessage(), modifyResponse.getMatchedDN(),
5150           stringListToArray(modifyResponse.getReferralURLs()));
5151    }
5152  }
5153
5154
5155
5156  /**
5157   * Retrieves a read-only representation the entry with the specified DN, if
5158   * it exists.
5159   *
5160   * @param  dn  The DN of the entry to retrieve.
5161   *
5162   * @return  The requested entry, or {@code null} if no entry exists with the
5163   *          given DN.
5164   *
5165   * @throws  LDAPException  If the provided DN is malformed.
5166   */
5167  @Nullable()
5168  public ReadOnlyEntry getEntry(@NotNull final String dn)
5169         throws LDAPException
5170  {
5171    return getEntry(new DN(dn, schemaRef.get()));
5172  }
5173
5174
5175
5176  /**
5177   * Retrieves a read-only representation the entry with the specified DN, if
5178   * it exists.
5179   *
5180   * @param  dn  The DN of the entry to retrieve.
5181   *
5182   * @return  The requested entry, or {@code null} if no entry exists with the
5183   *          given DN.
5184   */
5185  @Nullable()
5186  public ReadOnlyEntry getEntry(@NotNull final DN dn)
5187  {
5188    synchronized (entryMap)
5189    {
5190      if (dn.isNullDN())
5191      {
5192        return generateRootDSE();
5193      }
5194      else if (dn.equals(subschemaSubentryDN))
5195      {
5196        return subschemaSubentryRef.get();
5197      }
5198      else
5199      {
5200        final Entry e = entryMap.get(dn);
5201        if (e == null)
5202        {
5203          return null;
5204        }
5205        else
5206        {
5207          return new ReadOnlyEntry(e);
5208        }
5209      }
5210    }
5211  }
5212
5213
5214
5215  /**
5216   * Retrieves a list of all entries in the server which match the given
5217   * search criteria.
5218   *
5219   * @param  baseDN  The base DN to use for the search.  It must not be
5220   *                 {@code null}.
5221   * @param  scope   The scope to use for the search.  It must not be
5222   *                 {@code null}.
5223   * @param  filter  The filter to use for the search.  It must not be
5224   *                 {@code null}.
5225   *
5226   * @return  A list of the entries that matched the provided search criteria.
5227   *
5228   * @throws  LDAPException  If a problem is encountered while performing the
5229   *                         search.
5230   */
5231  @NotNull()
5232  public List<ReadOnlyEntry> search(@NotNull final String baseDN,
5233                                    @NotNull final SearchScope scope,
5234                                    @NotNull final Filter filter)
5235         throws LDAPException
5236  {
5237    synchronized (entryMap)
5238    {
5239      final DN parsedDN;
5240      final Schema schema = schemaRef.get();
5241      try
5242      {
5243        parsedDN = new DN(baseDN, schema);
5244      }
5245      catch (final LDAPException le)
5246      {
5247        Debug.debugException(le);
5248        throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
5249             ERR_MEM_HANDLER_SEARCH_MALFORMED_BASE.get(baseDN, le.getMessage()),
5250             le);
5251      }
5252
5253      final ReadOnlyEntry baseEntry;
5254      if (parsedDN.isNullDN())
5255      {
5256        baseEntry = generateRootDSE();
5257      }
5258      else if (parsedDN.equals(subschemaSubentryDN))
5259      {
5260        baseEntry = subschemaSubentryRef.get();
5261      }
5262      else
5263      {
5264        final Entry e = entryMap.get(parsedDN);
5265        if (e == null)
5266        {
5267          throw new LDAPException(ResultCode.NO_SUCH_OBJECT,
5268               ERR_MEM_HANDLER_SEARCH_BASE_DOES_NOT_EXIST.get(baseDN),
5269               getMatchedDNString(parsedDN), null);
5270        }
5271
5272        baseEntry = new ReadOnlyEntry(e);
5273      }
5274
5275      if (scope == SearchScope.BASE)
5276      {
5277        final List<ReadOnlyEntry> entryList = new ArrayList<>(1);
5278
5279        try
5280        {
5281          if (filter.matchesEntry(baseEntry, schema))
5282          {
5283            entryList.add(baseEntry);
5284          }
5285        }
5286        catch (final LDAPException le)
5287        {
5288          Debug.debugException(le);
5289        }
5290
5291        return Collections.unmodifiableList(entryList);
5292      }
5293
5294      if ((scope == SearchScope.ONE) && parsedDN.isNullDN())
5295      {
5296        final List<ReadOnlyEntry> entryList =
5297             new ArrayList<>(baseDNs.size());
5298
5299        try
5300        {
5301          for (final DN dn : baseDNs)
5302          {
5303            final Entry e = entryMap.get(dn);
5304            if ((e != null) && filter.matchesEntry(e, schema))
5305            {
5306              entryList.add(new ReadOnlyEntry(e));
5307            }
5308          }
5309        }
5310        catch (final LDAPException le)
5311        {
5312          Debug.debugException(le);
5313        }
5314
5315        return Collections.unmodifiableList(entryList);
5316      }
5317
5318      final List<ReadOnlyEntry> entryList = new ArrayList<>(10);
5319      for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet())
5320      {
5321        final DN dn = me.getKey();
5322        if (dn.matchesBaseAndScope(parsedDN, scope))
5323        {
5324          // We don't want to return changelog entries searches based at the
5325          // root DSE.
5326          if (parsedDN.isNullDN() && dn.isDescendantOf(changeLogBaseDN, true))
5327          {
5328            continue;
5329          }
5330
5331          try
5332          {
5333            final Entry entry = me.getValue();
5334            if (filter.matchesEntry(entry, schema))
5335            {
5336              entryList.add(new ReadOnlyEntry(entry));
5337            }
5338          }
5339          catch (final LDAPException le)
5340          {
5341            Debug.debugException(le);
5342          }
5343        }
5344      }
5345
5346      return Collections.unmodifiableList(entryList);
5347    }
5348  }
5349
5350
5351
5352  /**
5353   * Generates an entry to use as the server root DSE.
5354   *
5355   * @return  The generated root DSE entry.
5356   */
5357  @NotNull()
5358  private ReadOnlyEntry generateRootDSE()
5359  {
5360    final ReadOnlyEntry rootDSEFromCfg = config.getRootDSEEntry();
5361    if (rootDSEFromCfg != null)
5362    {
5363      return rootDSEFromCfg;
5364    }
5365
5366    final Entry rootDSEEntry = new Entry(DN.NULL_DN, schemaRef.get());
5367    rootDSEEntry.addAttribute("objectClass", "top", "ds-root-dse");
5368    rootDSEEntry.addAttribute(new Attribute("supportedLDAPVersion",
5369         IntegerMatchingRule.getInstance(), "3"));
5370
5371    final String vendorName = config.getVendorName();
5372    if (vendorName != null)
5373    {
5374      rootDSEEntry.addAttribute("vendorName", vendorName);
5375    }
5376
5377    final String vendorVersion = config.getVendorVersion();
5378    if (vendorVersion != null)
5379    {
5380      rootDSEEntry.addAttribute("vendorVersion", vendorVersion);
5381    }
5382
5383    rootDSEEntry.addAttribute(new Attribute("subschemaSubentry",
5384         DistinguishedNameMatchingRule.getInstance(),
5385         subschemaSubentryDN.toString()));
5386    rootDSEEntry.addAttribute(new Attribute("entryDN",
5387         DistinguishedNameMatchingRule.getInstance(), ""));
5388    rootDSEEntry.addAttribute("entryUUID",
5389         CryptoHelper.getRandomUUID().toString());
5390
5391    rootDSEEntry.addAttribute("supportedFeatures",
5392         "1.3.6.1.4.1.4203.1.5.1",  // All operational attributes
5393         "1.3.6.1.4.1.4203.1.5.2",  // Request attributes by object class
5394         "1.3.6.1.4.1.4203.1.5.3",  // LDAP absolute true and false filters
5395         "1.3.6.1.1.14");           // Increment modification type
5396
5397    final TreeSet<String> ctlSet = new TreeSet<>();
5398
5399    ctlSet.add(AssertionRequestControl.ASSERTION_REQUEST_OID);
5400    ctlSet.add(AuthorizationIdentityRequestControl.
5401         AUTHORIZATION_IDENTITY_REQUEST_OID);
5402    ctlSet.add(DontUseCopyRequestControl.DONT_USE_COPY_REQUEST_OID);
5403    ctlSet.add(ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID);
5404    ctlSet.add(DraftLDUPSubentriesRequestControl.SUBENTRIES_REQUEST_OID);
5405    ctlSet.add(DraftZeilengaLDAPNoOp12RequestControl.NO_OP_REQUEST_OID);
5406    ctlSet.add(PermissiveModifyRequestControl.PERMISSIVE_MODIFY_REQUEST_OID);
5407    ctlSet.add(PostReadRequestControl.POST_READ_REQUEST_OID);
5408    ctlSet.add(PreReadRequestControl.PRE_READ_REQUEST_OID);
5409    ctlSet.add(ProxiedAuthorizationV1RequestControl.
5410         PROXIED_AUTHORIZATION_V1_REQUEST_OID);
5411    ctlSet.add(ProxiedAuthorizationV2RequestControl.
5412         PROXIED_AUTHORIZATION_V2_REQUEST_OID);
5413    ctlSet.add(RFC3672SubentriesRequestControl.SUBENTRIES_REQUEST_OID);
5414    ctlSet.add(ServerSideSortRequestControl.SERVER_SIDE_SORT_REQUEST_OID);
5415    ctlSet.add(SimplePagedResultsControl.PAGED_RESULTS_OID);
5416    ctlSet.add(SubtreeDeleteRequestControl.SUBTREE_DELETE_REQUEST_OID);
5417    ctlSet.add(TransactionSpecificationRequestControl.
5418         TRANSACTION_SPECIFICATION_REQUEST_OID);
5419    ctlSet.add(VirtualListViewRequestControl.VIRTUAL_LIST_VIEW_REQUEST_OID);
5420    ctlSet.add(IgnoreNoUserModificationRequestControl.
5421         IGNORE_NO_USER_MODIFICATION_REQUEST_OID);
5422
5423    final String[] controlOIDs = new String[ctlSet.size()];
5424    rootDSEEntry.addAttribute("supportedControl", ctlSet.toArray(controlOIDs));
5425
5426
5427    if (! extendedRequestHandlers.isEmpty())
5428    {
5429      final String[] oidArray = new String[extendedRequestHandlers.size()];
5430      rootDSEEntry.addAttribute("supportedExtension",
5431           extendedRequestHandlers.keySet().toArray(oidArray));
5432
5433      for (final InMemoryListenerConfig c : config.getListenerConfigs())
5434      {
5435        if (c.getStartTLSSocketFactory() != null)
5436        {
5437          rootDSEEntry.addAttribute("supportedExtension",
5438               StartTLSExtendedRequest.STARTTLS_REQUEST_OID);
5439          break;
5440        }
5441      }
5442    }
5443
5444    if (! saslBindHandlers.isEmpty())
5445    {
5446      final String[] mechanismArray = new String[saslBindHandlers.size()];
5447      rootDSEEntry.addAttribute("supportedSASLMechanisms",
5448           saslBindHandlers.keySet().toArray(mechanismArray));
5449    }
5450
5451    int pos = 0;
5452    final String[] baseDNStrings = new String[baseDNs.size()];
5453    for (final DN baseDN : baseDNs)
5454    {
5455      baseDNStrings[pos++] = baseDN.toString();
5456    }
5457    rootDSEEntry.addAttribute(new Attribute("namingContexts",
5458         DistinguishedNameMatchingRule.getInstance(), baseDNStrings));
5459
5460    if (maxChangelogEntries > 0)
5461    {
5462      rootDSEEntry.addAttribute(new Attribute("changeLog",
5463           DistinguishedNameMatchingRule.getInstance(),
5464           changeLogBaseDN.toString()));
5465      rootDSEEntry.addAttribute(new Attribute("firstChangeNumber",
5466           IntegerMatchingRule.getInstance(), firstChangeNumber.toString()));
5467      rootDSEEntry.addAttribute(new Attribute("lastChangeNumber",
5468           IntegerMatchingRule.getInstance(), lastChangeNumber.toString()));
5469    }
5470
5471    for (final Attribute customAttribute : config.getCustomRootDSEAttributes())
5472    {
5473      rootDSEEntry.setAttribute(customAttribute);
5474    }
5475
5476    return new ReadOnlyEntry(rootDSEEntry);
5477  }
5478
5479
5480
5481  /**
5482   * Generates a subschema subentry from the provided schema object.
5483   *
5484   * @param  schema  The schema to use to generate the subschema subentry.  It
5485   *                 may be {@code null} if a minimal default entry should be
5486   *                 generated.
5487   *
5488   * @return  The generated subschema subentry.
5489   */
5490  @NotNull()
5491  private static ReadOnlyEntry generateSubschemaSubentry(
5492                                    @Nullable final Schema schema)
5493  {
5494    final Entry e;
5495
5496    if (schema == null)
5497    {
5498      e = new Entry("cn=schema", schema);
5499
5500      e.addAttribute("objectClass", "namedObject", "ldapSubEntry",
5501           "subschema");
5502      e.addAttribute("cn", "schema");
5503    }
5504    else
5505    {
5506      e = schema.getSchemaEntry().duplicate();
5507    }
5508
5509    try
5510    {
5511      e.addAttribute("entryDN", DN.normalize(e.getDN(), schema));
5512    }
5513    catch (final LDAPException le)
5514    {
5515      // This should never happen.
5516      Debug.debugException(le);
5517      e.setAttribute("entryDN", StaticUtils.toLowerCase(e.getDN()));
5518    }
5519
5520
5521    e.addAttribute("entryUUID", CryptoHelper.getRandomUUID().toString());
5522    return new ReadOnlyEntry(e);
5523  }
5524
5525
5526
5527  /**
5528   * Performs the necessary processing to determine whether the given entry
5529   * should be returned as a search result entry or reference, or if it should
5530   * not be returned at all.
5531   *
5532   * @param  entry                 The entry to be processed.
5533   * @param  includeSubEntries     Indicates whether LDAP subentries should be
5534   *                               returned to the client.
5535   * @param  includeNonSubEntries  Indicates whether non-LDAP subentries should
5536   *                               be returned to the client.
5537   * @param  includeChangeLog      Indicates whether entries within the
5538   *                               changelog should be returned to the client.
5539   * @param  hasManageDsaIT        Indicates whether the request includes the
5540   *                               ManageDsaIT control, which can change how
5541   *                               smart referrals should be handled.
5542   * @param  entryList             The list to which the entry should be added
5543   *                               if it should be returned to the client as a
5544   *                               search result entry.
5545   * @param  referenceList         The list that should be updated if the
5546   *                               provided entry represents a smart referral
5547   *                               that should be returned as a search result
5548   *                               reference.
5549   */
5550  private void processSearchEntry(@NotNull final Entry entry,
5551                    final boolean includeSubEntries,
5552                    final boolean includeNonSubEntries,
5553                    final boolean includeChangeLog,
5554                    final boolean hasManageDsaIT,
5555                    @NotNull final List<Entry> entryList,
5556                    @NotNull final List<SearchResultReference> referenceList)
5557  {
5558    // Check to see if the entry should be suppressed based on whether it's an
5559    // LDAP subentry.
5560    if (entry.hasObjectClass("ldapSubEntry") ||
5561        entry.hasObjectClass("inheritableLDAPSubEntry"))
5562    {
5563      if (! includeSubEntries)
5564      {
5565        return;
5566      }
5567    }
5568    else if (! includeNonSubEntries)
5569    {
5570      return;
5571    }
5572
5573    // See if the entry should be suppressed as a changelog entry.
5574    try
5575    {
5576      if ((! includeChangeLog) &&
5577           (entry.getParsedDN().isDescendantOf(changeLogBaseDN, true)))
5578      {
5579        return;
5580      }
5581    }
5582    catch (final Exception e)
5583    {
5584      // This should never happen.
5585      Debug.debugException(e);
5586    }
5587
5588    // See if the entry is a referral and should result in a reference rather
5589    // than an entry.
5590    if ((! hasManageDsaIT) && entry.hasObjectClass("referral") &&
5591        entry.hasAttribute("ref"))
5592    {
5593      referenceList.add(new SearchResultReference(
5594           entry.getAttributeValues("ref"), NO_CONTROLS));
5595      return;
5596    }
5597
5598    entryList.add(entry);
5599  }
5600
5601
5602
5603  /**
5604   * Retrieves the DN of the existing entry which is the closest hierarchical
5605   * match to the provided DN.
5606   *
5607   * @param  dn  The DN for which to retrieve the appropriate matched DN.
5608   *
5609   * @return  The appropriate matched DN value, or {@code null} if there is
5610   *          none.
5611   */
5612  @Nullable()
5613  private String getMatchedDNString(@NotNull final DN dn)
5614  {
5615    DN parentDN = dn.getParent();
5616    while (parentDN != null)
5617    {
5618      if (entryMap.containsKey(parentDN))
5619      {
5620        return parentDN.toString();
5621      }
5622
5623      parentDN = parentDN.getParent();
5624    }
5625
5626    return null;
5627  }
5628
5629
5630
5631  /**
5632   * Converts the provided string list to an array.
5633   *
5634   * @param  l  The possibly null list to be converted.
5635   *
5636   * @return  The string array with the same elements as the given list in the
5637   *          same order, or {@code null} if the given list was null.
5638   */
5639  @Nullable()
5640  private static String[] stringListToArray(@Nullable final List<String> l)
5641  {
5642    if (l == null)
5643    {
5644      return null;
5645    }
5646    else
5647    {
5648      final String[] a = new String[l.size()];
5649      return l.toArray(a);
5650    }
5651  }
5652
5653
5654
5655  /**
5656   * Creates a changelog entry from the information in the provided add request
5657   * and adds it to the server changelog.
5658   *
5659   * @param  addRequest  The add request to use to construct the changelog
5660   *                     entry.
5661   * @param  authzDN     The authorization DN for the change.
5662   */
5663  private void addChangeLogEntry(@NotNull final AddRequestProtocolOp addRequest,
5664                                 @NotNull final DN authzDN)
5665  {
5666    // If the changelog is disabled, then don't do anything.
5667    if (maxChangelogEntries <= 0)
5668    {
5669      return;
5670    }
5671
5672    final long changeNumber = lastChangeNumber.incrementAndGet();
5673    final LDIFAddChangeRecord changeRecord = new LDIFAddChangeRecord(
5674         addRequest.getDN(), addRequest.getAttributes());
5675    try
5676    {
5677      addChangeLogEntry(
5678           ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord),
5679           authzDN);
5680    }
5681    catch (final LDAPException le)
5682    {
5683      // This should not happen.
5684      Debug.debugException(le);
5685    }
5686  }
5687
5688
5689
5690  /**
5691   * Creates a changelog entry from the information in the provided delete
5692   * request and adds it to the server changelog.
5693   *
5694   * @param  e        The entry to be deleted.
5695   * @param  authzDN  The authorization DN for the change.
5696   */
5697  private void addDeleteChangeLogEntry(@NotNull final Entry e,
5698                                       @NotNull final DN authzDN)
5699  {
5700    // If the changelog is disabled, then don't do anything.
5701    if (maxChangelogEntries <= 0)
5702    {
5703      return;
5704    }
5705
5706    final long changeNumber = lastChangeNumber.incrementAndGet();
5707    final LDIFDeleteChangeRecord changeRecord =
5708         new LDIFDeleteChangeRecord(e.getDN());
5709
5710    // Create the changelog entry.
5711    try
5712    {
5713      final ChangeLogEntry cle = ChangeLogEntry.constructChangeLogEntry(
5714           changeNumber, changeRecord);
5715
5716      // Add a set of deleted entry attributes, which is simply an LDIF-encoded
5717      // representation of the entry, excluding the first line since it contains
5718      // the DN.
5719      final StringBuilder deletedEntryAttrsBuffer = new StringBuilder();
5720      final String[] ldifLines = e.toLDIF(0);
5721      for (int i=1; i < ldifLines.length; i++)
5722      {
5723        deletedEntryAttrsBuffer.append(ldifLines[i]);
5724        deletedEntryAttrsBuffer.append(StaticUtils.EOL);
5725      }
5726
5727      final Entry copy = cle.duplicate();
5728      copy.addAttribute(ChangeLogEntry.ATTR_DELETED_ENTRY_ATTRS,
5729           deletedEntryAttrsBuffer.toString());
5730      addChangeLogEntry(new ChangeLogEntry(copy), authzDN);
5731    }
5732    catch (final LDAPException le)
5733    {
5734      // This should never happen.
5735      Debug.debugException(le);
5736    }
5737  }
5738
5739
5740
5741  /**
5742   * Creates a changelog entry from the information in the provided modify
5743   * request and adds it to the server changelog.
5744   *
5745   * @param  modifyRequest  The modify request to use to construct the changelog
5746   *                        entry.
5747   * @param  authzDN        The authorization DN for the change.
5748   */
5749  private void addChangeLogEntry(
5750                    @NotNull final ModifyRequestProtocolOp modifyRequest,
5751                    @NotNull final DN authzDN)
5752  {
5753    // If the changelog is disabled, then don't do anything.
5754    if (maxChangelogEntries <= 0)
5755    {
5756      return;
5757    }
5758
5759    final long changeNumber = lastChangeNumber.incrementAndGet();
5760    final LDIFModifyChangeRecord changeRecord =
5761         new LDIFModifyChangeRecord(modifyRequest.getDN(),
5762              modifyRequest.getModifications());
5763    try
5764    {
5765      addChangeLogEntry(
5766           ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord),
5767           authzDN);
5768    }
5769    catch (final LDAPException le)
5770    {
5771      // This should not happen.
5772      Debug.debugException(le);
5773    }
5774  }
5775
5776
5777
5778  /**
5779   * Creates a changelog entry from the information in the provided modify DN
5780   * request and adds it to the server changelog.
5781   *
5782   * @param  modifyDNRequest  The modify DN request to use to construct the
5783   *                          changelog entry.
5784   * @param  authzDN          The authorization DN for the change.
5785   */
5786  private void addChangeLogEntry(
5787                    @NotNull final ModifyDNRequestProtocolOp modifyDNRequest,
5788                    @NotNull final DN authzDN)
5789  {
5790    // If the changelog is disabled, then don't do anything.
5791    if (maxChangelogEntries <= 0)
5792    {
5793      return;
5794    }
5795
5796    final long changeNumber = lastChangeNumber.incrementAndGet();
5797    final LDIFModifyDNChangeRecord changeRecord =
5798         new LDIFModifyDNChangeRecord(modifyDNRequest.getDN(),
5799              modifyDNRequest.getNewRDN(), modifyDNRequest.deleteOldRDN(),
5800              modifyDNRequest.getNewSuperiorDN());
5801    try
5802    {
5803      addChangeLogEntry(
5804           ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord),
5805           authzDN);
5806    }
5807    catch (final LDAPException le)
5808    {
5809      // This should not happen.
5810      Debug.debugException(le);
5811    }
5812  }
5813
5814
5815
5816  /**
5817   * Adds the provided changelog entry to the data set, removing an old entry if
5818   * necessary to remain within the maximum allowed number of changes.  This
5819   * must only be called from a synchronized method, and the change number for
5820   * the changelog entry must have been obtained by calling
5821   * {@code lastChangeNumber.incrementAndGet()}.
5822   *
5823   * @param  e        The changelog entry to add to the data set.
5824   * @param  authzDN  The authorization DN for the change.
5825   */
5826  private void addChangeLogEntry(@NotNull final ChangeLogEntry e,
5827                                 @NotNull final DN authzDN)
5828  {
5829    // Construct the DN object to use for the entry and put it in the map.
5830    final long changeNumber = e.getChangeNumber();
5831    final Schema schema = schemaRef.get();
5832    final DN dn = new DN(
5833         new RDN("changeNumber", String.valueOf(changeNumber), schema),
5834         changeLogBaseDN);
5835
5836    final Entry entry = e.duplicate();
5837    if (generateOperationalAttributes)
5838    {
5839      final Date d = new Date();
5840      entry.addAttribute(new Attribute("entryDN",
5841           DistinguishedNameMatchingRule.getInstance(),
5842           dn.toNormalizedString()));
5843      entry.addAttribute(new Attribute("entryUUID",
5844           CryptoHelper.getRandomUUID().toString()));
5845      entry.addAttribute(new Attribute("subschemaSubentry",
5846           DistinguishedNameMatchingRule.getInstance(),
5847           subschemaSubentryDN.toString()));
5848      entry.addAttribute(new Attribute("creatorsName",
5849           DistinguishedNameMatchingRule.getInstance(),
5850           authzDN.toString()));
5851      entry.addAttribute(new Attribute("createTimestamp",
5852           GeneralizedTimeMatchingRule.getInstance(),
5853           StaticUtils.encodeGeneralizedTime(d)));
5854      entry.addAttribute(new Attribute("modifiersName",
5855           DistinguishedNameMatchingRule.getInstance(),
5856           authzDN.toString()));
5857      entry.addAttribute(new Attribute("modifyTimestamp",
5858           GeneralizedTimeMatchingRule.getInstance(),
5859           StaticUtils.encodeGeneralizedTime(d)));
5860    }
5861
5862    entryMap.put(dn, new ReadOnlyEntry(entry));
5863    indexAdd(entry);
5864
5865    // Update the first change number and/or trim the changelog if necessary.
5866    final long firstNumber = firstChangeNumber.get();
5867    if (changeNumber == 1L)
5868    {
5869      // It's the first change, so we need to set the first change number.
5870      firstChangeNumber.set(1);
5871    }
5872    else
5873    {
5874      // See if we need to trim an entry.
5875      final long numChangeLogEntries = changeNumber - firstNumber + 1;
5876      if (numChangeLogEntries > maxChangelogEntries)
5877      {
5878        // We need to delete the first changelog entry and increment the
5879        // first change number.
5880        firstChangeNumber.incrementAndGet();
5881        final Entry deletedEntry = entryMap.remove(new DN(
5882             new RDN("changeNumber", String.valueOf(firstNumber), schema),
5883             changeLogBaseDN));
5884        indexDelete(deletedEntry);
5885      }
5886    }
5887  }
5888
5889
5890
5891  /**
5892   * Checks to see if the provided control map includes a proxied authorization
5893   * control (v1 or v2) and if so then attempts to determine the appropriate
5894   * authorization identity to use for the operation.
5895   *
5896   * @param  m  The map of request controls, indexed by OID.
5897   *
5898   * @return  The DN of the authorized user, or the current authentication DN
5899   *          if the control map does not include a proxied authorization
5900   *          request control.
5901   *
5902   * @throws  LDAPException  If a problem is encountered while attempting to
5903   *                         determine the authorization DN.
5904   */
5905  @NotNull()
5906  private DN handleProxiedAuthControl(@NotNull final Map<String,Control> m)
5907          throws LDAPException
5908  {
5909    final ProxiedAuthorizationV1RequestControl p1 =
5910         (ProxiedAuthorizationV1RequestControl) m.get(
5911              ProxiedAuthorizationV1RequestControl.
5912                   PROXIED_AUTHORIZATION_V1_REQUEST_OID);
5913    if (p1 != null)
5914    {
5915      final DN authzDN = new DN(p1.getProxyDN(), schemaRef.get());
5916      if (authzDN.isNullDN() ||
5917          entryMap.containsKey(authzDN) ||
5918          additionalBindCredentials.containsKey(authzDN))
5919      {
5920        return authzDN;
5921      }
5922      else
5923      {
5924        throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
5925             ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get("dn:" + authzDN.toString()));
5926      }
5927    }
5928
5929    final ProxiedAuthorizationV2RequestControl p2 =
5930         (ProxiedAuthorizationV2RequestControl) m.get(
5931              ProxiedAuthorizationV2RequestControl.
5932                   PROXIED_AUTHORIZATION_V2_REQUEST_OID);
5933    if (p2 != null)
5934    {
5935      return getDNForAuthzID(p2.getAuthorizationID());
5936    }
5937
5938    return authenticatedDN;
5939  }
5940
5941
5942
5943  /**
5944   * Attempts to identify the DN of the user referenced by the provided
5945   * authorization ID string.  It may be "dn:" followed by the target DN, or
5946   * "u:" followed by the value of the uid attribute in the entry.  If it uses
5947   * the "dn:" form, then it may reference the DN of a regular entry or a DN
5948   * in the configured set of additional bind credentials.
5949   *
5950   * @param  authzID  The authorization ID to resolve to a user DN.
5951   *
5952   * @return  The DN identified for the provided authorization ID.
5953   *
5954   * @throws  LDAPException  If a problem prevents resolving the authorization
5955   *                         ID to a user DN.
5956   */
5957  @NotNull()
5958  public DN getDNForAuthzID(@NotNull final String authzID)
5959         throws LDAPException
5960  {
5961    synchronized (entryMap)
5962    {
5963      final String lowerAuthzID = StaticUtils.toLowerCase(authzID);
5964      if (lowerAuthzID.startsWith("dn:"))
5965      {
5966        if (lowerAuthzID.equals("dn:"))
5967        {
5968          return DN.NULL_DN;
5969        }
5970        else
5971        {
5972          final DN dn = new DN(authzID.substring(3), schemaRef.get());
5973          if (entryMap.containsKey(dn) ||
5974               additionalBindCredentials.containsKey(dn))
5975          {
5976            return dn;
5977          }
5978          else
5979          {
5980            throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
5981                 ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID));
5982          }
5983        }
5984      }
5985      else if (lowerAuthzID.startsWith("u:"))
5986      {
5987        final Filter f =
5988             Filter.createEqualityFilter("uid", authzID.substring(2));
5989        final List<ReadOnlyEntry> entryList = search("", SearchScope.SUB, f);
5990        if (entryList.size() == 1)
5991        {
5992          return entryList.get(0).getParsedDN();
5993        }
5994        else
5995        {
5996          throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
5997               ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID));
5998        }
5999      }
6000      else
6001      {
6002        throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
6003             ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID));
6004      }
6005    }
6006  }
6007
6008
6009
6010  /**
6011   * Checks to see if the provided control map includes an assertion request
6012   * control, and if so then checks to see whether the provided entry satisfies
6013   * the filter in that control.
6014   *
6015   * @param  m  The map of request controls, indexed by OID.
6016   * @param  e  The entry to examine against the assertion filter.
6017   *
6018   * @throws  LDAPException  If the control map includes an assertion request
6019   *                         control and the provided entry does not match the
6020   *                         filter contained in that control.
6021   */
6022  private static void handleAssertionRequestControl(
6023                           @NotNull final Map<String,Control> m,
6024                           @NotNull final Entry e)
6025          throws LDAPException
6026  {
6027    final AssertionRequestControl c = (AssertionRequestControl)
6028         m.get(AssertionRequestControl.ASSERTION_REQUEST_OID);
6029    if (c == null)
6030    {
6031      return;
6032    }
6033
6034    try
6035    {
6036      if (c.getFilter().matchesEntry(e))
6037      {
6038        return;
6039      }
6040    }
6041    catch (final LDAPException le)
6042    {
6043      Debug.debugException(le);
6044    }
6045
6046    // If we've gotten here, then the filter doesn't match.
6047    throw new LDAPException(ResultCode.ASSERTION_FAILED,
6048         ERR_MEM_HANDLER_ASSERTION_CONTROL_NOT_SATISFIED.get());
6049  }
6050
6051
6052
6053  /**
6054   * Checks to see if the provided control map includes a pre-read request
6055   * control, and if so then generates the appropriate response control that
6056   * should be returned to the client.
6057   *
6058   * @param  m  The map of request controls, indexed by OID.
6059   * @param  e  The entry as it appeared before the operation.
6060   *
6061   * @return  The pre-read response control that should be returned to the
6062   *          client, or {@code null} if there is none.
6063   */
6064  @Nullable()
6065  private PreReadResponseControl handlePreReadControl(
6066               @NotNull final Map<String,Control> m, @NotNull final Entry e)
6067  {
6068    final PreReadRequestControl c = (PreReadRequestControl)
6069         m.get(PreReadRequestControl.PRE_READ_REQUEST_OID);
6070    if (c == null)
6071    {
6072      return null;
6073    }
6074
6075    final SearchEntryParer parer = new SearchEntryParer(
6076         Arrays.asList(c.getAttributes()), schemaRef.get());
6077    final Entry trimmedEntry = parer.pareEntry(e);
6078    return new PreReadResponseControl(new ReadOnlyEntry(trimmedEntry));
6079  }
6080
6081
6082
6083  /**
6084   * Checks to see if the provided control map includes a post-read request
6085   * control, and if so then generates the appropriate response control that
6086   * should be returned to the client.
6087   *
6088   * @param  m  The map of request controls, indexed by OID.
6089   * @param  e  The entry as it appeared before the operation.
6090   *
6091   * @return  The post-read response control that should be returned to the
6092   *          client, or {@code null} if there is none.
6093   */
6094  @Nullable()
6095  private PostReadResponseControl handlePostReadControl(
6096               @NotNull final Map<String,Control> m, @NotNull final Entry e)
6097  {
6098    final PostReadRequestControl c = (PostReadRequestControl)
6099         m.get(PostReadRequestControl.POST_READ_REQUEST_OID);
6100    if (c == null)
6101    {
6102      return null;
6103    }
6104
6105    final SearchEntryParer parer = new SearchEntryParer(
6106         Arrays.asList(c.getAttributes()), schemaRef.get());
6107    final Entry trimmedEntry = parer.pareEntry(e);
6108    return new PostReadResponseControl(new ReadOnlyEntry(trimmedEntry));
6109  }
6110
6111
6112
6113  /**
6114   * Finds the smart referral entry which is hierarchically nearest the entry
6115   * with the given DN.
6116   *
6117   * @param  dn  The DN for which to find the hierarchically nearest smart
6118   *             referral entry.
6119   *
6120   * @return  The hierarchically nearest smart referral entry for the provided
6121   *          DN, or {@code null} if there are no smart referral entries with
6122   *          the provided DN or any of its ancestors.
6123   */
6124  @Nullable()
6125  private Entry findNearestReferral(@NotNull final DN dn)
6126  {
6127    DN d = dn;
6128    while (true)
6129    {
6130      final Entry e = entryMap.get(d);
6131      if (e == null)
6132      {
6133        d = d.getParent();
6134        if (d == null)
6135        {
6136          return null;
6137        }
6138      }
6139      else if (e.hasObjectClass("referral"))
6140      {
6141        return e;
6142      }
6143      else
6144      {
6145        return null;
6146      }
6147    }
6148  }
6149
6150
6151
6152  /**
6153   * Retrieves the referral URLs that should be used for the provided target DN
6154   * based on the given referral entry.
6155   *
6156   * @param  targetDN       The target DN from the associated operation.
6157   * @param  referralEntry  The entry containing the smart referral.
6158   *
6159   * @return  The referral URLs that should be returned.
6160   */
6161  @Nullable()
6162  private static List<String> getReferralURLs(@NotNull final DN targetDN,
6163                                   @NotNull final Entry referralEntry)
6164  {
6165    final String[] refs = referralEntry.getAttributeValues("ref");
6166    if (refs == null)
6167    {
6168      return null;
6169    }
6170
6171    final RDN[] retainRDNs;
6172    try
6173    {
6174      // If the target DN equals the referral entry DN, or if it's not
6175      // subordinate to the referral entry, then the URLs should be returned
6176      // as-is.
6177      final DN parsedEntryDN = referralEntry.getParsedDN();
6178      if (targetDN.equals(parsedEntryDN) ||
6179          (! targetDN.isDescendantOf(parsedEntryDN, true)))
6180      {
6181        return Arrays.asList(refs);
6182      }
6183
6184      final RDN[] targetRDNs   = targetDN.getRDNs();
6185      final RDN[] refEntryRDNs = referralEntry.getParsedDN().getRDNs();
6186      retainRDNs = new RDN[targetRDNs.length - refEntryRDNs.length];
6187      System.arraycopy(targetRDNs, 0, retainRDNs, 0, retainRDNs.length);
6188    }
6189    catch (final LDAPException le)
6190    {
6191      Debug.debugException(le);
6192      return Arrays.asList(refs);
6193    }
6194
6195    final List<String> refList = new ArrayList<>(refs.length);
6196    for (final String ref : refs)
6197    {
6198      try
6199      {
6200        final LDAPURL url = new LDAPURL(ref);
6201        final RDN[] refRDNs = url.getBaseDN().getRDNs();
6202        final RDN[] newRefRDNs = new RDN[retainRDNs.length + refRDNs.length];
6203        System.arraycopy(retainRDNs, 0, newRefRDNs, 0, retainRDNs.length);
6204        System.arraycopy(refRDNs, 0, newRefRDNs, retainRDNs.length,
6205             refRDNs.length);
6206        final DN newBaseDN = new DN(newRefRDNs);
6207
6208        final LDAPURL newURL = new LDAPURL(url.getScheme(), url.getHost(),
6209             url.getPort(), newBaseDN, null, null, null);
6210        refList.add(newURL.toString());
6211      }
6212      catch (final LDAPException le)
6213      {
6214        Debug.debugException(le);
6215        refList.add(ref);
6216      }
6217    }
6218
6219    return refList;
6220  }
6221
6222
6223
6224  /**
6225   * Indicates whether the specified entry exists in the server.
6226   *
6227   * @param  dn  The DN of the entry for which to make the determination.
6228   *
6229   * @return  {@code true} if the entry exists, or {@code false} if not.
6230   *
6231   * @throws  LDAPException  If a problem is encountered while trying to
6232   *                         communicate with the directory server.
6233   */
6234  public boolean entryExists(@NotNull final String dn)
6235         throws LDAPException
6236  {
6237    return (getEntry(dn) != null);
6238  }
6239
6240
6241
6242  /**
6243   * Indicates whether the specified entry exists in the server and matches the
6244   * given filter.
6245   *
6246   * @param  dn      The DN of the entry for which to make the determination.
6247   * @param  filter  The filter the entry is expected to match.
6248   *
6249   * @return  {@code true} if the entry exists and matches the specified filter,
6250   *          or {@code false} if not.
6251   *
6252   * @throws  LDAPException  If a problem is encountered while trying to
6253   *                         communicate with the directory server.
6254   */
6255  public boolean entryExists(@NotNull final String dn,
6256                             @NotNull final String filter)
6257         throws LDAPException
6258  {
6259    synchronized (entryMap)
6260    {
6261      final Entry e = getEntry(dn);
6262      if (e == null)
6263      {
6264        return false;
6265      }
6266
6267      final Filter f = Filter.create(filter);
6268      try
6269      {
6270        return f.matchesEntry(e, schemaRef.get());
6271      }
6272      catch (final LDAPException le)
6273      {
6274        Debug.debugException(le);
6275        return false;
6276      }
6277    }
6278  }
6279
6280
6281
6282  /**
6283   * Indicates whether the specified entry exists in the server.  This will
6284   * return {@code true} only if the target entry exists and contains all values
6285   * for all attributes of the provided entry.  The entry will be allowed to
6286   * have attribute values not included in the provided entry.
6287   *
6288   * @param  entry  The entry to compare against the directory server.
6289   *
6290   * @return  {@code true} if the entry exists in the server and is a superset
6291   *          of the provided entry, or {@code false} if not.
6292   *
6293   * @throws  LDAPException  If a problem is encountered while trying to
6294   *                         communicate with the directory server.
6295   */
6296  public boolean entryExists(@NotNull final Entry entry)
6297         throws LDAPException
6298  {
6299    synchronized (entryMap)
6300    {
6301      final Entry e = getEntry(entry.getDN());
6302      if (e == null)
6303      {
6304        return false;
6305      }
6306
6307      for (final Attribute a : entry.getAttributes())
6308      {
6309        for (final byte[] value : a.getValueByteArrays())
6310        {
6311          if (! e.hasAttributeValue(a.getName(), value))
6312          {
6313            return false;
6314          }
6315        }
6316      }
6317
6318      return true;
6319    }
6320  }
6321
6322
6323
6324  /**
6325   * Ensures that an entry with the provided DN exists in the directory.
6326   *
6327   * @param  dn  The DN of the entry for which to make the determination.
6328   *
6329   * @throws  LDAPException  If a problem is encountered while trying to
6330   *                         communicate with the directory server.
6331   *
6332   * @throws  AssertionError  If the target entry does not exist.
6333   */
6334  public void assertEntryExists(@NotNull final String dn)
6335         throws LDAPException, AssertionError
6336  {
6337    final Entry e = getEntry(dn);
6338    if (e == null)
6339    {
6340      throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6341    }
6342  }
6343
6344
6345
6346  /**
6347   * Ensures that an entry with the provided DN exists in the directory.
6348   *
6349   * @param  dn      The DN of the entry for which to make the determination.
6350   * @param  filter  A filter that the target entry must match.
6351   *
6352   * @throws  LDAPException  If a problem is encountered while trying to
6353   *                         communicate with the directory server.
6354   *
6355   * @throws  AssertionError  If the target entry does not exist or does not
6356   *                          match the provided filter.
6357   */
6358  public void assertEntryExists(@NotNull final String dn,
6359                                @NotNull final String filter)
6360         throws LDAPException, AssertionError
6361  {
6362    synchronized (entryMap)
6363    {
6364      final Entry e = getEntry(dn);
6365      if (e == null)
6366      {
6367        throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6368      }
6369
6370      final Filter f = Filter.create(filter);
6371      try
6372      {
6373        if (! f.matchesEntry(e, schemaRef.get()))
6374        {
6375          throw new AssertionError(
6376               ERR_MEM_HANDLER_TEST_ENTRY_DOES_NOT_MATCH_FILTER.get(dn,
6377                    filter));
6378        }
6379      }
6380      catch (final LDAPException le)
6381      {
6382        Debug.debugException(le);
6383        throw new AssertionError(
6384             ERR_MEM_HANDLER_TEST_ENTRY_DOES_NOT_MATCH_FILTER.get(dn, filter));
6385      }
6386    }
6387  }
6388
6389
6390
6391  /**
6392   * Ensures that an entry exists in the directory with the same DN and all
6393   * attribute values contained in the provided entry.  The server entry may
6394   * contain additional attributes and/or attribute values not included in the
6395   * provided entry.
6396   *
6397   * @param  entry  The entry expected to be present in the directory server.
6398   *
6399   * @throws  LDAPException  If a problem is encountered while trying to
6400   *                         communicate with the directory server.
6401   *
6402   * @throws  AssertionError  If the target entry does not exist or does not
6403   *                          match the provided filter.
6404   */
6405  public void assertEntryExists(@NotNull final Entry entry)
6406         throws LDAPException, AssertionError
6407  {
6408    synchronized (entryMap)
6409    {
6410      final Entry e = getEntry(entry.getDN());
6411      if (e == null)
6412      {
6413        throw new AssertionError(
6414             ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(entry.getDN()));
6415      }
6416
6417
6418      final Collection<Attribute> attrs = entry.getAttributes();
6419      final List<String> messages = new ArrayList<>(attrs.size());
6420
6421      final Schema schema = schemaRef.get();
6422      for (final Attribute a : entry.getAttributes())
6423      {
6424        final Filter presFilter = Filter.createPresenceFilter(a.getName());
6425        if (! presFilter.matchesEntry(e, schema))
6426        {
6427          messages.add(ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(entry.getDN(),
6428               a.getName()));
6429          continue;
6430        }
6431
6432        for (final byte[] value : a.getValueByteArrays())
6433        {
6434          final Filter eqFilter = Filter.createEqualityFilter(a.getName(),
6435               value);
6436          if (! eqFilter.matchesEntry(e, schema))
6437          {
6438            messages.add(ERR_MEM_HANDLER_TEST_VALUE_MISSING.get(entry.getDN(),
6439                 a.getName(), StaticUtils.toUTF8String(value)));
6440          }
6441        }
6442      }
6443
6444      if (! messages.isEmpty())
6445      {
6446        throw new AssertionError(StaticUtils.concatenateStrings(messages));
6447      }
6448    }
6449  }
6450
6451
6452
6453  /**
6454   * Retrieves a list containing the DNs of the entries which are missing from
6455   * the directory server.
6456   *
6457   * @param  dns  The DNs of the entries to try to find in the server.
6458   *
6459   * @return  A list containing all of the provided DNs that were not found in
6460   *          the server, or an empty list if all entries were found.
6461   *
6462   * @throws  LDAPException  If a problem is encountered while trying to
6463   *                         communicate with the directory server.
6464   */
6465  @NotNull()
6466  public List<String> getMissingEntryDNs(@NotNull final Collection<String> dns)
6467         throws LDAPException
6468  {
6469    synchronized (entryMap)
6470    {
6471      final List<String> missingDNs = new ArrayList<>(dns.size());
6472      for (final String dn : dns)
6473      {
6474        final Entry e = getEntry(dn);
6475        if (e == null)
6476        {
6477          missingDNs.add(dn);
6478        }
6479      }
6480
6481      return missingDNs;
6482    }
6483  }
6484
6485
6486
6487  /**
6488   * Ensures that all of the entries with the provided DNs exist in the
6489   * directory.
6490   *
6491   * @param  dns  The DNs of the entries for which to make the determination.
6492   *
6493   * @throws  LDAPException  If a problem is encountered while trying to
6494   *                         communicate with the directory server.
6495   *
6496   * @throws  AssertionError  If any of the target entries does not exist.
6497   */
6498  public void assertEntriesExist(@NotNull final Collection<String> dns)
6499         throws LDAPException, AssertionError
6500  {
6501    synchronized (entryMap)
6502    {
6503      final List<String> missingDNs = getMissingEntryDNs(dns);
6504      if (missingDNs.isEmpty())
6505      {
6506        return;
6507      }
6508
6509      final List<String> messages = new ArrayList<>(missingDNs.size());
6510      for (final String dn : missingDNs)
6511      {
6512        messages.add(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6513      }
6514
6515      throw new AssertionError(StaticUtils.concatenateStrings(messages));
6516    }
6517  }
6518
6519
6520
6521  /**
6522   * Retrieves a list containing all of the named attributes which do not exist
6523   * in the target entry.
6524   *
6525   * @param  dn              The DN of the entry to examine.
6526   * @param  attributeNames  The names of the attributes expected to be present
6527   *                         in the target entry.
6528   *
6529   * @return  A list containing the names of the attributes which were not
6530   *          present in the target entry, an empty list if all specified
6531   *          attributes were found in the entry, or {@code null} if the target
6532   *          entry does not exist.
6533   *
6534   * @throws  LDAPException  If a problem is encountered while trying to
6535   *                         communicate with the directory server.
6536   */
6537  @Nullable()
6538  public List<String> getMissingAttributeNames(@NotNull final String dn,
6539                           @NotNull final Collection<String> attributeNames)
6540         throws LDAPException
6541  {
6542    synchronized (entryMap)
6543    {
6544      final Entry e = getEntry(dn);
6545      if (e == null)
6546      {
6547        return null;
6548      }
6549
6550      final Schema schema = schemaRef.get();
6551      final List<String> missingAttrs =
6552           new ArrayList<>(attributeNames.size());
6553      for (final String attr : attributeNames)
6554      {
6555        final Filter f = Filter.createPresenceFilter(attr);
6556        if (! f.matchesEntry(e, schema))
6557        {
6558          missingAttrs.add(attr);
6559        }
6560      }
6561
6562      return missingAttrs;
6563    }
6564  }
6565
6566
6567
6568  /**
6569   * Ensures that the specified entry exists in the directory with all of the
6570   * specified attributes.
6571   *
6572   * @param  dn              The DN of the entry to examine.
6573   * @param  attributeNames  The names of the attributes that are expected to be
6574   *                         present in the provided entry.
6575   *
6576   * @throws  LDAPException  If a problem is encountered while trying to
6577   *                         communicate with the directory server.
6578   *
6579   * @throws  AssertionError  If the target entry does not exist or does not
6580   *                          contain all of the specified attributes.
6581   */
6582  public void assertAttributeExists(@NotNull final String dn,
6583                   @NotNull final Collection<String> attributeNames)
6584        throws LDAPException, AssertionError
6585  {
6586    synchronized (entryMap)
6587    {
6588      final List<String> missingAttrs =
6589           getMissingAttributeNames(dn, attributeNames);
6590      if (missingAttrs == null)
6591      {
6592        throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6593      }
6594      else if (missingAttrs.isEmpty())
6595      {
6596        return;
6597      }
6598
6599      final List<String> messages = new ArrayList<>(missingAttrs.size());
6600      for (final String attr : missingAttrs)
6601      {
6602        messages.add(ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(dn, attr));
6603      }
6604
6605      throw new AssertionError(StaticUtils.concatenateStrings(messages));
6606    }
6607  }
6608
6609
6610
6611  /**
6612   * Retrieves a list of all provided attribute values which are missing from
6613   * the specified entry.  The target attribute may or may not contain
6614   * additional values.
6615   *
6616   * @param  dn               The DN of the entry to examine.
6617   * @param  attributeName    The attribute expected to be present in the target
6618   *                          entry with the given values.
6619   * @param  attributeValues  The values expected to be present in the target
6620   *                          entry.
6621   *
6622   * @return  A list containing all of the provided values which were not found
6623   *          in the entry, an empty list if all provided attribute values were
6624   *          found, or {@code null} if the target entry does not exist.
6625   *
6626   * @throws  LDAPException  If a problem is encountered while trying to
6627   *                         communicate with the directory server.
6628   */
6629  @Nullable()
6630  public List<String> getMissingAttributeValues(@NotNull final String dn,
6631                           @NotNull final String attributeName,
6632                           @NotNull final Collection<String> attributeValues)
6633       throws LDAPException
6634  {
6635    synchronized (entryMap)
6636    {
6637      final Entry e = getEntry(dn);
6638      if (e == null)
6639      {
6640        return null;
6641      }
6642
6643      final Schema schema = schemaRef.get();
6644      final List<String> missingValues =
6645           new ArrayList<>(attributeValues.size());
6646      for (final String value : attributeValues)
6647      {
6648        final Filter f = Filter.createEqualityFilter(attributeName, value);
6649        if (! f.matchesEntry(e, schema))
6650        {
6651          missingValues.add(value);
6652        }
6653      }
6654
6655      return missingValues;
6656    }
6657  }
6658
6659
6660
6661  /**
6662   * Ensures that the specified entry exists in the directory with all of the
6663   * specified values for the given attribute.  The attribute may or may not
6664   * contain additional values.
6665   *
6666   * @param  dn               The DN of the entry to examine.
6667   * @param  attributeName    The name of the attribute to examine.
6668   * @param  attributeValues  The set of values which must exist for the given
6669   *                          attribute.
6670   *
6671   * @throws  LDAPException  If a problem is encountered while trying to
6672   *                         communicate with the directory server.
6673   *
6674   * @throws  AssertionError  If the target entry does not exist, does not
6675   *                          contain the specified attribute, or that attribute
6676   *                          does not have all of the specified values.
6677   */
6678  public void assertValueExists(@NotNull final String dn,
6679                   @NotNull final String attributeName,
6680                   @NotNull final Collection<String> attributeValues)
6681        throws LDAPException, AssertionError
6682  {
6683    synchronized (entryMap)
6684    {
6685      final List<String> missingValues =
6686           getMissingAttributeValues(dn, attributeName, attributeValues);
6687      if (missingValues == null)
6688      {
6689        throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6690      }
6691      else if (missingValues.isEmpty())
6692      {
6693        return;
6694      }
6695
6696      // See if the attribute exists at all in the entry.
6697      final Entry e = getEntry(dn);
6698      final Filter f = Filter.createPresenceFilter(attributeName);
6699      if (! f.matchesEntry(e,  schemaRef.get()))
6700      {
6701        throw new AssertionError(
6702             ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(dn, attributeName));
6703      }
6704
6705      final List<String> messages = new ArrayList<>(missingValues.size());
6706      for (final String value : missingValues)
6707      {
6708        messages.add(ERR_MEM_HANDLER_TEST_VALUE_MISSING.get(dn, attributeName,
6709             value));
6710      }
6711
6712      throw new AssertionError(StaticUtils.concatenateStrings(messages));
6713    }
6714  }
6715
6716
6717
6718  /**
6719   * Ensures that the specified entry does not exist in the directory.
6720   *
6721   * @param  dn  The DN of the entry expected to be missing.
6722   *
6723   * @throws  LDAPException  If a problem is encountered while trying to
6724   *                         communicate with the directory server.
6725   *
6726   * @throws  AssertionError  If the target entry is found in the server.
6727   */
6728  public void assertEntryMissing(@NotNull final String dn)
6729         throws LDAPException, AssertionError
6730  {
6731    final Entry e = getEntry(dn);
6732    if (e != null)
6733    {
6734      throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_EXISTS.get(dn));
6735    }
6736  }
6737
6738
6739
6740  /**
6741   * Ensures that the specified entry exists in the directory but does not
6742   * contain any of the specified attributes.
6743   *
6744   * @param  dn              The DN of the entry expected to be present.
6745   * @param  attributeNames  The names of the attributes expected to be missing
6746   *                         from the entry.
6747   *
6748   * @throws  LDAPException  If a problem is encountered while trying to
6749   *                         communicate with the directory server.
6750   *
6751   * @throws  AssertionError  If the target entry is missing from the server, or
6752   *                          if it contains any of the target attributes.
6753   */
6754  public void assertAttributeMissing(@NotNull final String dn,
6755                   @NotNull final Collection<String> attributeNames)
6756         throws LDAPException, AssertionError
6757  {
6758    synchronized (entryMap)
6759    {
6760      final Entry e = getEntry(dn);
6761      if (e == null)
6762      {
6763        throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6764      }
6765
6766      final Schema schema = schemaRef.get();
6767      final List<String> messages = new ArrayList<>(attributeNames.size());
6768      for (final String name : attributeNames)
6769      {
6770        final Filter f = Filter.createPresenceFilter(name);
6771        if (f.matchesEntry(e, schema))
6772        {
6773          messages.add(ERR_MEM_HANDLER_TEST_ATTR_EXISTS.get(dn, name));
6774        }
6775      }
6776
6777      if (! messages.isEmpty())
6778      {
6779        throw new AssertionError(StaticUtils.concatenateStrings(messages));
6780      }
6781    }
6782  }
6783
6784
6785
6786  /**
6787   * Ensures that the specified entry exists in the directory but does not
6788   * contain any of the specified attribute values.
6789   *
6790   * @param  dn               The DN of the entry expected to be present.
6791   * @param  attributeName    The name of the attribute to examine.
6792   * @param  attributeValues  The values expected to be missing from the target
6793   *                          entry.
6794   *
6795   * @throws  LDAPException  If a problem is encountered while trying to
6796   *                         communicate with the directory server.
6797   *
6798   * @throws  AssertionError  If the target entry is missing from the server, or
6799   *                          if it contains any of the target attribute values.
6800   */
6801  public void assertValueMissing(@NotNull final String dn,
6802                   @NotNull final String attributeName,
6803                   @NotNull final Collection<String> attributeValues)
6804         throws LDAPException, AssertionError
6805  {
6806    synchronized (entryMap)
6807    {
6808      final Entry e = getEntry(dn);
6809      if (e == null)
6810      {
6811        throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6812      }
6813
6814      final Schema schema = schemaRef.get();
6815      final List<String> messages = new ArrayList<>(attributeValues.size());
6816      for (final String value : attributeValues)
6817      {
6818        final Filter f = Filter.createEqualityFilter(attributeName, value);
6819        if (f.matchesEntry(e, schema))
6820        {
6821          messages.add(ERR_MEM_HANDLER_TEST_VALUE_EXISTS.get(dn, attributeName,
6822               value));
6823        }
6824      }
6825
6826      if (! messages.isEmpty())
6827      {
6828        throw new AssertionError(StaticUtils.concatenateStrings(messages));
6829      }
6830    }
6831  }
6832}