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