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