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