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