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