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