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