001    /*
002     * Copyright 2011-2017 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2011-2017 UnboundID Corp.
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     */
021    package com.unboundid.ldap.listener;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collection;
028    import java.util.Collections;
029    import java.util.Date;
030    import java.util.HashMap;
031    import java.util.Iterator;
032    import java.util.LinkedHashMap;
033    import java.util.LinkedHashSet;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.Set;
037    import java.util.SortedSet;
038    import java.util.TreeMap;
039    import java.util.TreeSet;
040    import java.util.UUID;
041    import java.util.concurrent.atomic.AtomicBoolean;
042    import java.util.concurrent.atomic.AtomicLong;
043    import java.util.concurrent.atomic.AtomicReference;
044    
045    import com.unboundid.asn1.ASN1Integer;
046    import com.unboundid.asn1.ASN1OctetString;
047    import com.unboundid.ldap.protocol.AddRequestProtocolOp;
048    import com.unboundid.ldap.protocol.AddResponseProtocolOp;
049    import com.unboundid.ldap.protocol.BindRequestProtocolOp;
050    import com.unboundid.ldap.protocol.BindResponseProtocolOp;
051    import com.unboundid.ldap.protocol.CompareRequestProtocolOp;
052    import com.unboundid.ldap.protocol.CompareResponseProtocolOp;
053    import com.unboundid.ldap.protocol.DeleteRequestProtocolOp;
054    import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
055    import com.unboundid.ldap.protocol.ExtendedRequestProtocolOp;
056    import com.unboundid.ldap.protocol.ExtendedResponseProtocolOp;
057    import com.unboundid.ldap.protocol.LDAPMessage;
058    import com.unboundid.ldap.protocol.ModifyRequestProtocolOp;
059    import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
060    import com.unboundid.ldap.protocol.ModifyDNRequestProtocolOp;
061    import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
062    import com.unboundid.ldap.protocol.ProtocolOp;
063    import com.unboundid.ldap.protocol.SearchRequestProtocolOp;
064    import com.unboundid.ldap.protocol.SearchResultDoneProtocolOp;
065    import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule;
066    import com.unboundid.ldap.matchingrules.GeneralizedTimeMatchingRule;
067    import com.unboundid.ldap.matchingrules.IntegerMatchingRule;
068    import com.unboundid.ldap.matchingrules.MatchingRule;
069    import com.unboundid.ldap.matchingrules.OctetStringMatchingRule;
070    import com.unboundid.ldap.protocol.SearchResultReferenceProtocolOp;
071    import com.unboundid.ldap.sdk.Attribute;
072    import com.unboundid.ldap.sdk.BindResult;
073    import com.unboundid.ldap.sdk.ChangeLogEntry;
074    import com.unboundid.ldap.sdk.Control;
075    import com.unboundid.ldap.sdk.DN;
076    import com.unboundid.ldap.sdk.Entry;
077    import com.unboundid.ldap.sdk.EntrySorter;
078    import com.unboundid.ldap.sdk.ExtendedRequest;
079    import com.unboundid.ldap.sdk.ExtendedResult;
080    import com.unboundid.ldap.sdk.Filter;
081    import com.unboundid.ldap.sdk.LDAPException;
082    import com.unboundid.ldap.sdk.LDAPURL;
083    import com.unboundid.ldap.sdk.Modification;
084    import com.unboundid.ldap.sdk.ModificationType;
085    import com.unboundid.ldap.sdk.OperationType;
086    import com.unboundid.ldap.sdk.RDN;
087    import com.unboundid.ldap.sdk.ReadOnlyEntry;
088    import com.unboundid.ldap.sdk.ResultCode;
089    import com.unboundid.ldap.sdk.SearchResultEntry;
090    import com.unboundid.ldap.sdk.SearchResultReference;
091    import com.unboundid.ldap.sdk.SearchScope;
092    import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
093    import com.unboundid.ldap.sdk.schema.DITContentRuleDefinition;
094    import com.unboundid.ldap.sdk.schema.DITStructureRuleDefinition;
095    import com.unboundid.ldap.sdk.schema.EntryValidator;
096    import com.unboundid.ldap.sdk.schema.MatchingRuleUseDefinition;
097    import com.unboundid.ldap.sdk.schema.NameFormDefinition;
098    import com.unboundid.ldap.sdk.schema.ObjectClassDefinition;
099    import com.unboundid.ldap.sdk.schema.Schema;
100    import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
101    import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
102    import com.unboundid.ldap.sdk.controls.AuthorizationIdentityResponseControl;
103    import com.unboundid.ldap.sdk.controls.DontUseCopyRequestControl;
104    import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
105    import com.unboundid.ldap.sdk.controls.PermissiveModifyRequestControl;
106    import com.unboundid.ldap.sdk.controls.PostReadRequestControl;
107    import com.unboundid.ldap.sdk.controls.PostReadResponseControl;
108    import com.unboundid.ldap.sdk.controls.PreReadRequestControl;
109    import com.unboundid.ldap.sdk.controls.PreReadResponseControl;
110    import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV1RequestControl;
111    import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
112    import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
113    import com.unboundid.ldap.sdk.controls.ServerSideSortResponseControl;
114    import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
115    import com.unboundid.ldap.sdk.controls.SortKey;
116    import com.unboundid.ldap.sdk.controls.SubentriesRequestControl;
117    import com.unboundid.ldap.sdk.controls.SubtreeDeleteRequestControl;
118    import com.unboundid.ldap.sdk.controls.TransactionSpecificationRequestControl;
119    import com.unboundid.ldap.sdk.controls.VirtualListViewRequestControl;
120    import com.unboundid.ldap.sdk.controls.VirtualListViewResponseControl;
121    import com.unboundid.ldap.sdk.experimental.
122                DraftZeilengaLDAPNoOp12RequestControl;
123    import com.unboundid.ldap.sdk.extensions.AbortedTransactionExtendedResult;
124    import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
125    import com.unboundid.ldif.LDIFAddChangeRecord;
126    import com.unboundid.ldif.LDIFDeleteChangeRecord;
127    import com.unboundid.ldif.LDIFException;
128    import com.unboundid.ldif.LDIFModifyChangeRecord;
129    import com.unboundid.ldif.LDIFModifyDNChangeRecord;
130    import com.unboundid.ldif.LDIFReader;
131    import com.unboundid.ldif.LDIFWriter;
132    import com.unboundid.util.Debug;
133    import com.unboundid.util.Mutable;
134    import com.unboundid.util.ObjectPair;
135    import com.unboundid.util.StaticUtils;
136    import com.unboundid.util.ThreadSafety;
137    import com.unboundid.util.ThreadSafetyLevel;
138    
139    import 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)
152    public 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          // Create a temporary list to hold all of the entries to be returned.
3127          // These entries will not have been pared down based on the requested
3128          // attributes.
3129          final List<Entry> fullEntryList = new ArrayList<Entry>(entryMap.size());
3130    
3131    findEntriesAndRefs:
3132          {
3133            // Check the scope.  If it is a base-level search, then we only need to
3134            // examine the base entry.  Otherwise, we'll have to scan the entire
3135            // entry map.
3136            final Filter filter = request.getFilter();
3137            final SearchScope scope = request.getScope();
3138            final boolean includeSubEntries = ((scope == SearchScope.BASE) ||
3139                 controlMap.containsKey(
3140                      SubentriesRequestControl.SUBENTRIES_REQUEST_OID));
3141            if (scope == SearchScope.BASE)
3142            {
3143              try
3144              {
3145                if (filter.matchesEntry(baseEntry, schema))
3146                {
3147                  processSearchEntry(baseEntry, includeSubEntries, includeChangeLog,
3148                       hasManageDsaIT, fullEntryList, referenceList);
3149                }
3150              }
3151              catch (final Exception e)
3152              {
3153                Debug.debugException(e);
3154              }
3155    
3156              break findEntriesAndRefs;
3157            }
3158    
3159            // If the search uses a single-level scope and the base DN is the root
3160            // DSE, then we will only examine the defined base entries for the data
3161            // set.
3162            if ((scope == SearchScope.ONE) && baseDN.isNullDN())
3163            {
3164              for (final DN dn : baseDNs)
3165              {
3166                final Entry e = entryMap.get(dn);
3167                if (e != null)
3168                {
3169                  try
3170                  {
3171                    if (filter.matchesEntry(e, schema))
3172                    {
3173                      processSearchEntry(e, includeSubEntries, includeChangeLog,
3174                           hasManageDsaIT, fullEntryList, referenceList);
3175                    }
3176                  }
3177                  catch (final Exception ex)
3178                  {
3179                    Debug.debugException(ex);
3180                  }
3181                }
3182              }
3183    
3184              break findEntriesAndRefs;
3185            }
3186    
3187    
3188            // Try to use indexes to process the request.  If we can't use any
3189            // indexes to get a candidate list, then just iterate over all the
3190            // entries.  It's not necessary to consider the root DSE for non-base
3191            // scopes.
3192            final Set<DN> candidateDNs = indexSearch(filter);
3193            if (candidateDNs == null)
3194            {
3195              for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet())
3196              {
3197                final DN dn = me.getKey();
3198                final Entry entry = me.getValue();
3199                try
3200                {
3201                  if (dn.matchesBaseAndScope(baseDN, scope) &&
3202                       filter.matchesEntry(entry, schema))
3203                  {
3204                    processSearchEntry(entry, includeSubEntries, includeChangeLog,
3205                         hasManageDsaIT, fullEntryList, referenceList);
3206                  }
3207                }
3208                catch (final Exception e)
3209                {
3210                  Debug.debugException(e);
3211                }
3212              }
3213            }
3214            else
3215            {
3216              for (final DN dn : candidateDNs)
3217              {
3218                try
3219                {
3220                  if (! dn.matchesBaseAndScope(baseDN, scope))
3221                  {
3222                    continue;
3223                  }
3224    
3225                  final Entry entry = entryMap.get(dn);
3226                  if (filter.matchesEntry(entry, schema))
3227                  {
3228                    processSearchEntry(entry, includeSubEntries, includeChangeLog,
3229                         hasManageDsaIT, fullEntryList, referenceList);
3230                  }
3231                }
3232                catch (final Exception e)
3233                {
3234                  Debug.debugException(e);
3235                }
3236              }
3237            }
3238          }
3239    
3240    
3241          // If the request included the server-side sort request control, then sort
3242          // the matching entries appropriately.
3243          final ServerSideSortRequestControl sortRequestControl =
3244               (ServerSideSortRequestControl) controlMap.get(
3245                    ServerSideSortRequestControl.SERVER_SIDE_SORT_REQUEST_OID);
3246          if (sortRequestControl != null)
3247          {
3248            final EntrySorter entrySorter = new EntrySorter(false, schema,
3249                 sortRequestControl.getSortKeys());
3250            final SortedSet<Entry> sortedEntrySet = entrySorter.sort(fullEntryList);
3251            fullEntryList.clear();
3252            fullEntryList.addAll(sortedEntrySet);
3253    
3254            responseControls.add(new ServerSideSortResponseControl(
3255                 ResultCode.SUCCESS, null, false));
3256          }
3257    
3258    
3259          // If the request included the simple paged results control, then handle
3260          // it.
3261          final SimplePagedResultsControl pagedResultsControl =
3262               (SimplePagedResultsControl)
3263                    controlMap.get(SimplePagedResultsControl.PAGED_RESULTS_OID);
3264          if (pagedResultsControl != null)
3265          {
3266            final int totalSize = fullEntryList.size();
3267            final int pageSize = pagedResultsControl.getSize();
3268            final ASN1OctetString cookie = pagedResultsControl.getCookie();
3269    
3270            final int offset;
3271            if ((cookie == null) || (cookie.getValueLength() == 0))
3272            {
3273              // This is the first request in the series, so start at the beginning
3274              // of the list.
3275              offset = 0;
3276            }
3277            else
3278            {
3279              // The cookie value will simply be an integer representation of the
3280              // offset within the result list at which to start the next batch.
3281              try
3282              {
3283                final ASN1Integer offsetInteger =
3284                     ASN1Integer.decodeAsInteger(cookie.getValue());
3285                offset = offsetInteger.intValue();
3286              }
3287              catch (final Exception e)
3288              {
3289                Debug.debugException(e);
3290                return new LDAPMessage(messageID,
3291                     new SearchResultDoneProtocolOp(
3292                          ResultCode.PROTOCOL_ERROR_INT_VALUE, null,
3293                          ERR_MEM_HANDLER_MALFORMED_PAGED_RESULTS_COOKIE.get(),
3294                          null),
3295                     responseControls);
3296              }
3297            }
3298    
3299            // Create an iterator that will be used to remove entries from the
3300            // result set that are outside of the requested page of results.
3301            int pos = 0;
3302            final Iterator<Entry> iterator = fullEntryList.iterator();
3303    
3304            // First, remove entries at the beginning of the list until we hit the
3305            // offset.
3306            while (iterator.hasNext() && (pos < offset))
3307            {
3308              iterator.next();
3309              iterator.remove();
3310              pos++;
3311            }
3312    
3313            // Next, skip over the entries that should be returned.
3314            int keptEntries = 0;
3315            while (iterator.hasNext() && (keptEntries < pageSize))
3316            {
3317              iterator.next();
3318              pos++;
3319              keptEntries++;
3320            }
3321    
3322            // If there are still entries left, then remove them and create a cookie
3323            // to include in the response.  Otherwise, use an empty cookie.
3324            if (iterator.hasNext())
3325            {
3326              responseControls.add(new SimplePagedResultsControl(totalSize,
3327                   new ASN1OctetString(new ASN1Integer(pos).encode()), false));
3328              while (iterator.hasNext())
3329              {
3330                iterator.next();
3331                iterator.remove();
3332              }
3333            }
3334            else
3335            {
3336              responseControls.add(new SimplePagedResultsControl(totalSize,
3337                   new ASN1OctetString(), false));
3338            }
3339          }
3340    
3341    
3342          // If the request includes the virtual list view request control, then
3343          // handle it.
3344          final VirtualListViewRequestControl vlvRequest =
3345               (VirtualListViewRequestControl) controlMap.get(
3346                    VirtualListViewRequestControl.VIRTUAL_LIST_VIEW_REQUEST_OID);
3347          if (vlvRequest != null)
3348          {
3349            final int totalEntries = fullEntryList.size();
3350            final ASN1OctetString assertionValue = vlvRequest.getAssertionValue();
3351    
3352            // Figure out the position of the target entry in the list.
3353            int offset = vlvRequest.getTargetOffset();
3354            if (assertionValue == null)
3355            {
3356              // The offset is one-based, so we need to adjust it for the list's
3357              // zero-based offset.  Also, make sure to put it within the bounds of
3358              // the list.
3359              offset--;
3360              offset = Math.max(0, offset);
3361              offset = Math.min(fullEntryList.size(), offset);
3362            }
3363            else
3364            {
3365              final SortKey primarySortKey = sortRequestControl.getSortKeys()[0];
3366    
3367              final Entry testEntry = new Entry("cn=test", schema,
3368                   new Attribute(primarySortKey.getAttributeName(),
3369                        assertionValue));
3370    
3371              final EntrySorter entrySorter =
3372                   new EntrySorter(false, schema, primarySortKey);
3373    
3374              offset = fullEntryList.size();
3375              for (int i=0; i < fullEntryList.size(); i++)
3376              {
3377                if (entrySorter.compare(fullEntryList.get(i), testEntry) >= 0)
3378                {
3379                  offset = i;
3380                  break;
3381                }
3382              }
3383            }
3384    
3385            // Get the start and end positions based on the before and after counts.
3386            final int beforeCount = Math.max(0, vlvRequest.getBeforeCount());
3387            final int afterCount  = Math.max(0, vlvRequest.getAfterCount());
3388    
3389            final int start = Math.max(0, (offset - beforeCount));
3390            final int end =
3391                 Math.min(fullEntryList.size(), (offset + afterCount + 1));
3392    
3393            // Create an iterator to use to alter the list so that it only contains
3394            // the appropriate set of entries.
3395            int pos = 0;
3396            final Iterator<Entry> iterator = fullEntryList.iterator();
3397            while (iterator.hasNext())
3398            {
3399              iterator.next();
3400              if ((pos < start) || (pos >= end))
3401              {
3402                iterator.remove();
3403              }
3404              pos++;
3405            }
3406    
3407            // Create the appropriate response control.
3408            responseControls.add(new VirtualListViewResponseControl((offset+1),
3409                 totalEntries, ResultCode.SUCCESS, null));
3410          }
3411    
3412    
3413          // Process the set of requested attributes so that we can pare down the
3414          // entries.
3415          final AtomicBoolean allUserAttrs = new AtomicBoolean(false);
3416          final AtomicBoolean allOpAttrs = new AtomicBoolean(false);
3417          final Map<String,List<List<String>>> returnAttrs =
3418               processRequestedAttributes(request.getAttributes(), allUserAttrs,
3419                    allOpAttrs);
3420    
3421          final int sizeLimit;
3422          if (request.getSizeLimit() > 0)
3423          {
3424            sizeLimit = Math.min(request.getSizeLimit(), maxSizeLimit);
3425          }
3426          else
3427          {
3428            sizeLimit = maxSizeLimit;
3429          }
3430    
3431          int entryCount = 0;
3432          for (final Entry e : fullEntryList)
3433          {
3434            entryCount++;
3435            if (entryCount > sizeLimit)
3436            {
3437              return new LDAPMessage(messageID,
3438                   new SearchResultDoneProtocolOp(
3439                        ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE, null,
3440                        ERR_MEM_HANDLER_SEARCH_SIZE_LIMIT_EXCEEDED.get(), null),
3441                   responseControls);
3442            }
3443    
3444            final Entry trimmedEntry = trimForRequestedAttributes(e,
3445                 allUserAttrs.get(), allOpAttrs.get(), returnAttrs);
3446            if (request.typesOnly())
3447            {
3448              final Entry typesOnlyEntry = new Entry(trimmedEntry.getDN(), schema);
3449              for (final Attribute a : trimmedEntry.getAttributes())
3450              {
3451                typesOnlyEntry.addAttribute(new Attribute(a.getName()));
3452              }
3453              entryList.add(new SearchResultEntry(typesOnlyEntry));
3454            }
3455            else
3456            {
3457              entryList.add(new SearchResultEntry(trimmedEntry));
3458            }
3459          }
3460    
3461          return new LDAPMessage(messageID,
3462               new SearchResultDoneProtocolOp(ResultCode.SUCCESS_INT_VALUE, null,
3463                    null, null),
3464               responseControls);
3465        }
3466      }
3467    
3468    
3469    
3470      /**
3471       * Performs any necessary index processing to add the provided entry.
3472       *
3473       * @param  entry  The entry that has been added.
3474       */
3475      private void indexAdd(final Entry entry)
3476      {
3477        for (final InMemoryDirectoryServerEqualityAttributeIndex i :
3478             equalityIndexes.values())
3479        {
3480          try
3481          {
3482            i.processAdd(entry);
3483          }
3484          catch (final LDAPException le)
3485          {
3486            Debug.debugException(le);
3487          }
3488        }
3489      }
3490    
3491    
3492    
3493      /**
3494       * Performs any necessary index processing to delete the provided entry.
3495       *
3496       * @param  entry  The entry that has been deleted.
3497       */
3498      private void indexDelete(final Entry entry)
3499      {
3500        for (final InMemoryDirectoryServerEqualityAttributeIndex i :
3501             equalityIndexes.values())
3502        {
3503          try
3504          {
3505            i.processDelete(entry);
3506          }
3507          catch (final LDAPException le)
3508          {
3509            Debug.debugException(le);
3510          }
3511        }
3512      }
3513    
3514    
3515    
3516      /**
3517       * Attempts to use indexes to obtain a candidate list for the provided filter.
3518       *
3519       * @param  filter  The filter to be processed.
3520       *
3521       * @return  The DNs of entries which may match the given filter, or
3522       *          {@code null} if the filter is not indexed.
3523       */
3524      private Set<DN> indexSearch(final Filter filter)
3525      {
3526        switch (filter.getFilterType())
3527        {
3528          case Filter.FILTER_TYPE_AND:
3529            Filter[] comps = filter.getComponents();
3530            if (comps.length == 0)
3531            {
3532              return null;
3533            }
3534            else if (comps.length == 1)
3535            {
3536              return indexSearch(comps[0]);
3537            }
3538            else
3539            {
3540              Set<DN> candidateSet = null;
3541              for (final Filter f : comps)
3542              {
3543                final Set<DN> dnSet = indexSearch(f);
3544                if (dnSet != null)
3545                {
3546                  if (candidateSet == null)
3547                  {
3548                    candidateSet = new TreeSet<DN>(dnSet);
3549                  }
3550                  else
3551                  {
3552                    candidateSet.retainAll(dnSet);
3553                  }
3554                }
3555              }
3556              return candidateSet;
3557            }
3558    
3559          case Filter.FILTER_TYPE_OR:
3560            comps = filter.getComponents();
3561            if (comps.length == 0)
3562            {
3563              return Collections.emptySet();
3564            }
3565            else if (comps.length == 1)
3566            {
3567              return indexSearch(comps[0]);
3568            }
3569            else
3570            {
3571              Set<DN> candidateSet = null;
3572              for (final Filter f : comps)
3573              {
3574                final Set<DN> dnSet = indexSearch(f);
3575                if (dnSet == null)
3576                {
3577                  return null;
3578                }
3579    
3580                if (candidateSet == null)
3581                {
3582                  candidateSet = new TreeSet<DN>(dnSet);
3583                }
3584                else
3585                {
3586                  candidateSet.addAll(dnSet);
3587                }
3588              }
3589              return candidateSet;
3590            }
3591    
3592          case Filter.FILTER_TYPE_EQUALITY:
3593            final Schema schema = schemaRef.get();
3594            if (schema == null)
3595            {
3596              return null;
3597            }
3598            final AttributeTypeDefinition at =
3599                 schema.getAttributeType(filter.getAttributeName());
3600            if (at == null)
3601            {
3602              return null;
3603            }
3604            final InMemoryDirectoryServerEqualityAttributeIndex i =
3605                 equalityIndexes.get(at);
3606            if (i == null)
3607            {
3608              return null;
3609            }
3610            try
3611            {
3612              return i.getMatchingEntries(filter.getRawAssertionValue());
3613            }
3614            catch (final Exception e)
3615            {
3616              Debug.debugException(e);
3617              return null;
3618            }
3619    
3620          default:
3621            return null;
3622        }
3623      }
3624    
3625    
3626    
3627      /**
3628       * Determines whether the provided set of controls includes a transaction
3629       * specification request control.  If so, then it will verify that it
3630       * references a valid transaction for the client.  If the request is part of a
3631       * valid transaction, then the transaction specification request control will
3632       * be removed and the request will be stashed in the client connection state
3633       * so that it can be retrieved and processed when the transaction is
3634       * committed.
3635       *
3636       * @param  messageID  The message ID for the request to be processed.
3637       * @param  request    The protocol op for the request to be processed.
3638       * @param  controls   The set of controls for the request to be processed.
3639       *
3640       * @return  The transaction ID for the associated transaction, or {@code null}
3641       *          if the request is not part of any transaction.
3642       *
3643       * @throws  LDAPException  If the transaction specification request control is
3644       *                         present but does not refer to a valid transaction
3645       *                         for the associated client connection.
3646       */
3647      @SuppressWarnings("unchecked")
3648      private ASN1OctetString processTransactionRequest(final int messageID,
3649                                   final ProtocolOp request,
3650                                   final Map<String,Control> controls)
3651              throws LDAPException
3652      {
3653        final TransactionSpecificationRequestControl txnControl =
3654             (TransactionSpecificationRequestControl)
3655             controls.remove(TransactionSpecificationRequestControl.
3656                  TRANSACTION_SPECIFICATION_REQUEST_OID);
3657        if (txnControl == null)
3658        {
3659          return null;
3660        }
3661    
3662        // See if the client has an active transaction.  If not, then fail.
3663        final ASN1OctetString txnID = txnControl.getTransactionID();
3664        final ObjectPair<ASN1OctetString,List<LDAPMessage>> txnInfo =
3665             (ObjectPair<ASN1OctetString,List<LDAPMessage>>) connectionState.get(
3666                  TransactionExtendedOperationHandler.STATE_VARIABLE_TXN_INFO);
3667        if (txnInfo == null)
3668        {
3669          throw new LDAPException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
3670               ERR_MEM_HANDLER_TXN_CONTROL_WITHOUT_TXN.get(txnID.stringValue()));
3671        }
3672    
3673    
3674        // Make sure that the active transaction has a transaction ID that matches
3675        // the transaction ID from the control.  If not, then abort the existing
3676        // transaction and fail.
3677        final ASN1OctetString existingTxnID = txnInfo.getFirst();
3678        if (! txnID.stringValue().equals(existingTxnID.stringValue()))
3679        {
3680          connectionState.remove(
3681               TransactionExtendedOperationHandler.STATE_VARIABLE_TXN_INFO);
3682          connection.sendUnsolicitedNotification(
3683               new AbortedTransactionExtendedResult(existingTxnID,
3684                    ResultCode.CONSTRAINT_VIOLATION,
3685                    ERR_MEM_HANDLER_TXN_ABORTED_BY_CONTROL_TXN_ID_MISMATCH.get(
3686                         existingTxnID.stringValue(), txnID.stringValue()),
3687                    null, null, null));
3688          throw new LDAPException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
3689               ERR_MEM_HANDLER_TXN_CONTROL_ID_MISMATCH.get(txnID.stringValue(),
3690                    existingTxnID.stringValue()));
3691        }
3692    
3693    
3694        // Stash the request in the transaction state information so that it will
3695        // be processed when the transaction is committed.
3696        txnInfo.getSecond().add(new LDAPMessage(messageID, request,
3697             new ArrayList<Control>(controls.values())));
3698    
3699        return txnID;
3700      }
3701    
3702    
3703    
3704      /**
3705       * Sleeps for a period of time (if appropriate) before beginning processing
3706       * for an operation.
3707       */
3708      private void sleepBeforeProcessing()
3709      {
3710        final long delay = processingDelayMillis.get();
3711        if (delay > 0)
3712        {
3713          try
3714          {
3715            Thread.sleep(delay);
3716          }
3717          catch (final Exception e)
3718          {
3719            Debug.debugException(e);
3720    
3721            if (e instanceof InterruptedException)
3722            {
3723              Thread.currentThread().interrupt();
3724            }
3725          }
3726        }
3727      }
3728    
3729    
3730    
3731      /**
3732       * Retrieves the number of entries currently held in the server.
3733       *
3734       * @param  includeChangeLog  Indicates whether to include entries that are
3735       *                           part of the changelog in the count.
3736       *
3737       * @return  The number of entries currently held in the server.
3738       */
3739      public int countEntries(final boolean includeChangeLog)
3740      {
3741        synchronized (entryMap)
3742        {
3743          if (includeChangeLog || (maxChangelogEntries == 0))
3744          {
3745            return entryMap.size();
3746          }
3747          else
3748          {
3749            int count = 0;
3750    
3751            for (final DN dn : entryMap.keySet())
3752            {
3753              if (! dn.isDescendantOf(changeLogBaseDN, true))
3754              {
3755                count++;
3756              }
3757            }
3758    
3759            return count;
3760          }
3761        }
3762      }
3763    
3764    
3765    
3766      /**
3767       * Retrieves the number of entries currently held in the server whose DN
3768       * matches or is subordinate to the provided base DN.
3769       *
3770       * @param  baseDN  The base DN to use for the determination.
3771       *
3772       * @return  The number of entries currently held in the server whose DN
3773       *          matches or is subordinate to the provided base DN.
3774       *
3775       * @throws  LDAPException  If the provided string cannot be parsed as a valid
3776       *                         DN.
3777       */
3778      public int countEntriesBelow(final String baseDN)
3779             throws LDAPException
3780      {
3781        synchronized (entryMap)
3782        {
3783          final DN parsedBaseDN = new DN(baseDN, schemaRef.get());
3784    
3785          int count = 0;
3786          for (final DN dn : entryMap.keySet())
3787          {
3788            if (dn.isDescendantOf(parsedBaseDN, true))
3789            {
3790              count++;
3791            }
3792          }
3793    
3794          return count;
3795        }
3796      }
3797    
3798    
3799    
3800      /**
3801       * Removes all entries currently held in the server.  If a changelog is
3802       * enabled, then all changelog entries will also be cleared but the base
3803       * "cn=changelog" entry will be retained.
3804       */
3805      public void clear()
3806      {
3807        synchronized (entryMap)
3808        {
3809          restoreSnapshot(initialSnapshot);
3810        }
3811      }
3812    
3813    
3814    
3815      /**
3816       * Reads entries from the provided LDIF reader and adds them to the server,
3817       * optionally clearing any existing entries before beginning to add the new
3818       * entries.  If an error is encountered while adding entries from LDIF then
3819       * the server will remain populated with the data it held before the import
3820       * attempt (even if the {@code clear} is given with a value of {@code true}).
3821       *
3822       * @param  clear       Indicates whether to remove all existing entries prior
3823       *                     to adding entries read from LDIF.
3824       * @param  ldifReader  The LDIF reader to use to obtain the entries to be
3825       *                     imported.
3826       *
3827       * @return  The number of entries read from LDIF and added to the server.
3828       *
3829       * @throws  LDAPException  If a problem occurs while reading entries or adding
3830       *                         them to the server.
3831       */
3832      public int importFromLDIF(final boolean clear, final LDIFReader ldifReader)
3833             throws LDAPException
3834      {
3835        synchronized (entryMap)
3836        {
3837          final InMemoryDirectoryServerSnapshot snapshot = createSnapshot();
3838          boolean restoreSnapshot = true;
3839    
3840          try
3841          {
3842            if (clear)
3843            {
3844              restoreSnapshot(initialSnapshot);
3845            }
3846    
3847            int entriesAdded = 0;
3848            while (true)
3849            {
3850              final Entry entry;
3851              try
3852              {
3853                entry = ldifReader.readEntry();
3854                if (entry == null)
3855                {
3856                  restoreSnapshot = false;
3857                  return entriesAdded;
3858                }
3859              }
3860              catch (final LDIFException le)
3861              {
3862                Debug.debugException(le);
3863                throw new LDAPException(ResultCode.LOCAL_ERROR,
3864                     ERR_MEM_HANDLER_INIT_FROM_LDIF_READ_ERROR.get(le.getMessage()),
3865                     le);
3866              }
3867              catch (final Exception e)
3868              {
3869                Debug.debugException(e);
3870                throw new LDAPException(ResultCode.LOCAL_ERROR,
3871                     ERR_MEM_HANDLER_INIT_FROM_LDIF_READ_ERROR.get(
3872                          StaticUtils.getExceptionMessage(e)),
3873                     e);
3874              }
3875    
3876              addEntry(entry, true);
3877              entriesAdded++;
3878            }
3879          }
3880          finally
3881          {
3882            try
3883            {
3884              ldifReader.close();
3885            }
3886            catch (final Exception e)
3887            {
3888              Debug.debugException(e);
3889            }
3890    
3891            if (restoreSnapshot)
3892            {
3893              restoreSnapshot(snapshot);
3894            }
3895          }
3896        }
3897      }
3898    
3899    
3900    
3901      /**
3902       * Writes all entries contained in the server to LDIF using the provided
3903       * writer.
3904       *
3905       * @param  ldifWriter             The LDIF writer to use when writing the
3906       *                                entries.  It must not be {@code null}.
3907       * @param  excludeGeneratedAttrs  Indicates whether to exclude automatically
3908       *                                generated operational attributes like
3909       *                                entryUUID, entryDN, creatorsName, etc.
3910       * @param  excludeChangeLog       Indicates whether to exclude entries
3911       *                                contained in the changelog.
3912       * @param  closeWriter            Indicates whether the LDIF writer should be
3913       *                                closed after all entries have been written.
3914       *
3915       * @return  The number of entries written to LDIF.
3916       *
3917       * @throws  LDAPException  If a problem is encountered while attempting to
3918       *                         write an entry to LDIF.
3919       */
3920      public int exportToLDIF(final LDIFWriter ldifWriter,
3921                              final boolean excludeGeneratedAttrs,
3922                              final boolean excludeChangeLog,
3923                              final boolean closeWriter)
3924             throws LDAPException
3925      {
3926        synchronized (entryMap)
3927        {
3928          boolean exceptionThrown = false;
3929    
3930          try
3931          {
3932            int entriesWritten = 0;
3933    
3934            for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet())
3935            {
3936              final DN dn = me.getKey();
3937              if (excludeChangeLog && dn.isDescendantOf(changeLogBaseDN, true))
3938              {
3939                continue;
3940              }
3941    
3942              final Entry entry;
3943              if (excludeGeneratedAttrs)
3944              {
3945                entry = me.getValue().duplicate();
3946                entry.removeAttribute("entryDN");
3947                entry.removeAttribute("entryUUID");
3948                entry.removeAttribute("subschemaSubentry");
3949                entry.removeAttribute("creatorsName");
3950                entry.removeAttribute("createTimestamp");
3951                entry.removeAttribute("modifiersName");
3952                entry.removeAttribute("modifyTimestamp");
3953              }
3954              else
3955              {
3956                entry = me.getValue();
3957              }
3958    
3959              try
3960              {
3961                ldifWriter.writeEntry(entry);
3962                entriesWritten++;
3963              }
3964              catch (final Exception e)
3965              {
3966                Debug.debugException(e);
3967                exceptionThrown = true;
3968                throw new LDAPException(ResultCode.LOCAL_ERROR,
3969                     ERR_MEM_HANDLER_LDIF_WRITE_ERROR.get(entry.getDN(),
3970                          StaticUtils.getExceptionMessage(e)),
3971                     e);
3972              }
3973            }
3974    
3975            return entriesWritten;
3976          }
3977          finally
3978          {
3979            if (closeWriter)
3980            {
3981              try
3982              {
3983                ldifWriter.close();
3984              }
3985              catch (final Exception e)
3986              {
3987                Debug.debugException(e);
3988                if (! exceptionThrown)
3989                {
3990                  throw new LDAPException(ResultCode.LOCAL_ERROR,
3991                       ERR_MEM_HANDLER_LDIF_WRITE_CLOSE_ERROR.get(
3992                            StaticUtils.getExceptionMessage(e)),
3993                       e);
3994                }
3995              }
3996            }
3997          }
3998        }
3999      }
4000    
4001    
4002    
4003      /**
4004       * Attempts to add the provided entry to the in-memory data set.  The attempt
4005       * will fail if any of the following conditions is true:
4006       * <UL>
4007       *   <LI>The provided entry has a malformed DN.</LI>
4008       *   <LI>The provided entry has the null DN.</LI>
4009       *   <LI>The provided entry has a DN that is the same as or subordinate to the
4010       *       subschema subentry.</LI>
4011       *   <LI>An entry already exists with the same DN as the entry in the provided
4012       *       request.</LI>
4013       *   <LI>The entry is outside the set of base DNs for the server.</LI>
4014       *   <LI>The entry is below one of the defined base DNs but the immediate
4015       *       parent entry does not exist.</LI>
4016       *   <LI>If a schema was provided, and the entry is not valid according to the
4017       *       constraints of that schema.</LI>
4018       * </UL>
4019       *
4020       * @param  entry                     The entry to be added.  It must not be
4021       *                                   {@code null}.
4022       * @param  ignoreNoUserModification  Indicates whether to ignore constraints
4023       *                                   normally imposed by the
4024       *                                   NO-USER-MODIFICATION element in attribute
4025       *                                   type definitions.
4026       *
4027       * @throws  LDAPException  If a problem occurs while attempting to add the
4028       *                         provided entry.
4029       */
4030      public void addEntry(final Entry entry,
4031                           final boolean ignoreNoUserModification)
4032             throws LDAPException
4033      {
4034        final List<Control> controls;
4035        if (ignoreNoUserModification)
4036        {
4037          controls = new ArrayList<Control>(1);
4038          controls.add(new Control(OID_INTERNAL_OPERATION_REQUEST_CONTROL, false));
4039        }
4040        else
4041        {
4042          controls = Collections.emptyList();
4043        }
4044    
4045        final AddRequestProtocolOp addRequest = new AddRequestProtocolOp(
4046             entry.getDN(), new ArrayList<Attribute>(entry.getAttributes()));
4047    
4048        final LDAPMessage resultMessage =
4049             processAddRequest(-1, addRequest, controls);
4050    
4051        final AddResponseProtocolOp addResponse =
4052             resultMessage.getAddResponseProtocolOp();
4053        if (addResponse.getResultCode() != ResultCode.SUCCESS_INT_VALUE)
4054        {
4055          throw new LDAPException(ResultCode.valueOf(addResponse.getResultCode()),
4056               addResponse.getDiagnosticMessage(), addResponse.getMatchedDN(),
4057               stringListToArray(addResponse.getReferralURLs()));
4058        }
4059      }
4060    
4061    
4062    
4063      /**
4064       * Attempts to add all of the provided entries to the server.  If an error is
4065       * encountered during processing, then the contents of the server will be the
4066       * same as they were before this method was called.
4067       *
4068       * @param  entries  The collection of entries to be added.
4069       *
4070       * @throws  LDAPException  If a problem was encountered while attempting to
4071       *                         add any of the entries to the server.
4072       */
4073      public void addEntries(final List<? extends Entry> entries)
4074             throws LDAPException
4075      {
4076        synchronized (entryMap)
4077        {
4078          final InMemoryDirectoryServerSnapshot snapshot = createSnapshot();
4079          boolean restoreSnapshot = true;
4080    
4081          try
4082          {
4083            for (final Entry e : entries)
4084            {
4085              addEntry(e, false);
4086            }
4087            restoreSnapshot = false;
4088          }
4089          finally
4090          {
4091            if (restoreSnapshot)
4092            {
4093              restoreSnapshot(snapshot);
4094            }
4095          }
4096        }
4097      }
4098    
4099    
4100    
4101      /**
4102       * Removes the entry with the specified DN and any subordinate entries it may
4103       * have.
4104       *
4105       * @param  baseDN  The DN of the entry to be deleted.  It must not be
4106       *                 {@code null} or represent the null DN.
4107       *
4108       * @return  The number of entries actually removed, or zero if the specified
4109       *          base DN does not represent an entry in the server.
4110       *
4111       * @throws  LDAPException  If the provided base DN is not a valid DN, or is
4112       *                         the DN of an entry that cannot be deleted (e.g.,
4113       *                         the null DN).
4114       */
4115      public int deleteSubtree(final String baseDN)
4116             throws LDAPException
4117      {
4118        synchronized (entryMap)
4119        {
4120          final DN dn = new DN(baseDN, schemaRef.get());
4121          if (dn.isNullDN())
4122          {
4123            throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
4124                 ERR_MEM_HANDLER_DELETE_ROOT_DSE.get());
4125          }
4126    
4127          int numDeleted = 0;
4128    
4129          final Iterator<Map.Entry<DN,ReadOnlyEntry>> iterator =
4130               entryMap.entrySet().iterator();
4131          while (iterator.hasNext())
4132          {
4133            final Map.Entry<DN,ReadOnlyEntry> e = iterator.next();
4134            if (e.getKey().isDescendantOf(dn, true))
4135            {
4136              iterator.remove();
4137              numDeleted++;
4138            }
4139          }
4140    
4141          return numDeleted;
4142        }
4143      }
4144    
4145    
4146    
4147      /**
4148       * Attempts to apply the provided set of modifications to the specified entry.
4149       * The attempt will fail if any of the following conditions is true:
4150       * <UL>
4151       *   <LI>The target DN is malformed.</LI>
4152       *   <LI>The target entry is the root DSE.</LI>
4153       *   <LI>The target entry is the subschema subentry.</LI>
4154       *   <LI>The target entry does not exist.</LI>
4155       *   <LI>Any of the modifications cannot be applied to the entry.</LI>
4156       *   <LI>If a schema was provided, and the entry violates any of the
4157       *       constraints of that schema.</LI>
4158       * </UL>
4159       *
4160       * @param  dn    The DN of the entry to be modified.
4161       * @param  mods  The set of modifications to be applied to the entry.
4162       *
4163       * @throws  LDAPException  If a problem is encountered while attempting to
4164       *                         update the specified entry.
4165       */
4166      public void modifyEntry(final String dn, final List<Modification> mods)
4167             throws LDAPException
4168      {
4169        final ModifyRequestProtocolOp modifyRequest =
4170             new ModifyRequestProtocolOp(dn, mods);
4171    
4172        final LDAPMessage resultMessage = processModifyRequest(-1, modifyRequest,
4173             Collections.<Control>emptyList());
4174    
4175        final ModifyResponseProtocolOp modifyResponse =
4176             resultMessage.getModifyResponseProtocolOp();
4177        if (modifyResponse.getResultCode() != ResultCode.SUCCESS_INT_VALUE)
4178        {
4179          throw new LDAPException(
4180               ResultCode.valueOf(modifyResponse.getResultCode()),
4181               modifyResponse.getDiagnosticMessage(), modifyResponse.getMatchedDN(),
4182               stringListToArray(modifyResponse.getReferralURLs()));
4183        }
4184      }
4185    
4186    
4187    
4188      /**
4189       * Retrieves a read-only representation the entry with the specified DN, if
4190       * it exists.
4191       *
4192       * @param  dn  The DN of the entry to retrieve.
4193       *
4194       * @return  The requested entry, or {@code null} if no entry exists with the
4195       *          given DN.
4196       *
4197       * @throws  LDAPException  If the provided DN is malformed.
4198       */
4199      public ReadOnlyEntry getEntry(final String dn)
4200             throws LDAPException
4201      {
4202        return getEntry(new DN(dn, schemaRef.get()));
4203      }
4204    
4205    
4206    
4207      /**
4208       * Retrieves a read-only representation the entry with the specified DN, if
4209       * it exists.
4210       *
4211       * @param  dn  The DN of the entry to retrieve.
4212       *
4213       * @return  The requested entry, or {@code null} if no entry exists with the
4214       *          given DN.
4215       */
4216      public ReadOnlyEntry getEntry(final DN dn)
4217      {
4218        synchronized (entryMap)
4219        {
4220          if (dn.isNullDN())
4221          {
4222            return generateRootDSE();
4223          }
4224          else if (dn.equals(subschemaSubentryDN))
4225          {
4226            return subschemaSubentryRef.get();
4227          }
4228          else
4229          {
4230            final Entry e = entryMap.get(dn);
4231            if (e == null)
4232            {
4233              return null;
4234            }
4235            else
4236            {
4237              return new ReadOnlyEntry(e);
4238            }
4239          }
4240        }
4241      }
4242    
4243    
4244    
4245      /**
4246       * Retrieves a list of all entries in the server which match the given
4247       * search criteria.
4248       *
4249       * @param  baseDN  The base DN to use for the search.  It must not be
4250       *                 {@code null}.
4251       * @param  scope   The scope to use for the search.  It must not be
4252       *                 {@code null}.
4253       * @param  filter  The filter to use for the search.  It must not be
4254       *                 {@code null}.
4255       *
4256       * @return  A list of the entries that matched the provided search criteria.
4257       *
4258       * @throws  LDAPException  If a problem is encountered while performing the
4259       *                         search.
4260       */
4261      public List<ReadOnlyEntry> search(final String baseDN,
4262                                        final SearchScope scope,
4263                                        final Filter filter)
4264             throws LDAPException
4265      {
4266        synchronized (entryMap)
4267        {
4268          final DN parsedDN;
4269          final Schema schema = schemaRef.get();
4270          try
4271          {
4272            parsedDN = new DN(baseDN, schema);
4273          }
4274          catch (final LDAPException le)
4275          {
4276            Debug.debugException(le);
4277            throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
4278                 ERR_MEM_HANDLER_SEARCH_MALFORMED_BASE.get(baseDN, le.getMessage()),
4279                 le);
4280          }
4281    
4282          final ReadOnlyEntry baseEntry;
4283          if (parsedDN.isNullDN())
4284          {
4285            baseEntry = generateRootDSE();
4286          }
4287          else if (parsedDN.equals(subschemaSubentryDN))
4288          {
4289            baseEntry = subschemaSubentryRef.get();
4290          }
4291          else
4292          {
4293            final Entry e = entryMap.get(parsedDN);
4294            if (e == null)
4295            {
4296              throw new LDAPException(ResultCode.NO_SUCH_OBJECT,
4297                   ERR_MEM_HANDLER_SEARCH_BASE_DOES_NOT_EXIST.get(baseDN),
4298                   getMatchedDNString(parsedDN), null);
4299            }
4300    
4301            baseEntry = new ReadOnlyEntry(e);
4302          }
4303    
4304          if (scope == SearchScope.BASE)
4305          {
4306            final List<ReadOnlyEntry> entryList = new ArrayList<ReadOnlyEntry>(1);
4307    
4308            try
4309            {
4310              if (filter.matchesEntry(baseEntry, schema))
4311              {
4312                entryList.add(baseEntry);
4313              }
4314            }
4315            catch (final LDAPException le)
4316            {
4317              Debug.debugException(le);
4318            }
4319    
4320            return Collections.unmodifiableList(entryList);
4321          }
4322    
4323          if ((scope == SearchScope.ONE) && parsedDN.isNullDN())
4324          {
4325            final List<ReadOnlyEntry> entryList =
4326                 new ArrayList<ReadOnlyEntry>(baseDNs.size());
4327    
4328            try
4329            {
4330              for (final DN dn : baseDNs)
4331              {
4332                final Entry e = entryMap.get(dn);
4333                if ((e != null) && filter.matchesEntry(e, schema))
4334                {
4335                  entryList.add(new ReadOnlyEntry(e));
4336                }
4337              }
4338            }
4339            catch (final LDAPException le)
4340            {
4341              Debug.debugException(le);
4342            }
4343    
4344            return Collections.unmodifiableList(entryList);
4345          }
4346    
4347          final List<ReadOnlyEntry> entryList = new ArrayList<ReadOnlyEntry>(10);
4348          for (final Map.Entry<DN,ReadOnlyEntry> me : entryMap.entrySet())
4349          {
4350            final DN dn = me.getKey();
4351            if (dn.matchesBaseAndScope(parsedDN, scope))
4352            {
4353              // We don't want to return changelog entries searches based at the
4354              // root DSE.
4355              if (parsedDN.isNullDN() && dn.isDescendantOf(changeLogBaseDN, true))
4356              {
4357                continue;
4358              }
4359    
4360              try
4361              {
4362                final Entry entry = me.getValue();
4363                if (filter.matchesEntry(entry, schema))
4364                {
4365                  entryList.add(new ReadOnlyEntry(entry));
4366                }
4367              }
4368              catch (final LDAPException le)
4369              {
4370                Debug.debugException(le);
4371              }
4372            }
4373          }
4374    
4375          return Collections.unmodifiableList(entryList);
4376        }
4377      }
4378    
4379    
4380    
4381      /**
4382       * Generates an entry to use as the server root DSE.
4383       *
4384       * @return  The generated root DSE entry.
4385       */
4386      private ReadOnlyEntry generateRootDSE()
4387      {
4388        final ReadOnlyEntry rootDSEFromCfg = config.getRootDSEEntry();
4389        if (rootDSEFromCfg != null)
4390        {
4391          return rootDSEFromCfg;
4392        }
4393    
4394        final Entry rootDSEEntry = new Entry(DN.NULL_DN, schemaRef.get());
4395        rootDSEEntry.addAttribute("objectClass", "top", "ds-root-dse");
4396        rootDSEEntry.addAttribute(new Attribute("supportedLDAPVersion",
4397             IntegerMatchingRule.getInstance(), "3"));
4398    
4399        final String vendorName = config.getVendorName();
4400        if (vendorName != null)
4401        {
4402          rootDSEEntry.addAttribute("vendorName", vendorName);
4403        }
4404    
4405        final String vendorVersion = config.getVendorVersion();
4406        if (vendorVersion != null)
4407        {
4408          rootDSEEntry.addAttribute("vendorVersion", vendorVersion);
4409        }
4410    
4411        rootDSEEntry.addAttribute(new Attribute("subschemaSubentry",
4412             DistinguishedNameMatchingRule.getInstance(),
4413             subschemaSubentryDN.toString()));
4414        rootDSEEntry.addAttribute(new Attribute("entryDN",
4415             DistinguishedNameMatchingRule.getInstance(), ""));
4416        rootDSEEntry.addAttribute("entryUUID", UUID.randomUUID().toString());
4417    
4418        rootDSEEntry.addAttribute("supportedFeatures",
4419             "1.3.6.1.4.1.4203.1.5.1",  // All operational attributes
4420             "1.3.6.1.4.1.4203.1.5.2",  // Request attributes by object class
4421             "1.3.6.1.4.1.4203.1.5.3",  // LDAP absolute true and false filters
4422             "1.3.6.1.1.14");           // Increment modification type
4423    
4424        final TreeSet<String> ctlSet = new TreeSet<String>();
4425    
4426        ctlSet.add(AssertionRequestControl.ASSERTION_REQUEST_OID);
4427        ctlSet.add(AuthorizationIdentityRequestControl.
4428             AUTHORIZATION_IDENTITY_REQUEST_OID);
4429        ctlSet.add(DontUseCopyRequestControl.DONT_USE_COPY_REQUEST_OID);
4430        ctlSet.add(ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID);
4431        ctlSet.add(DraftZeilengaLDAPNoOp12RequestControl.NO_OP_REQUEST_OID);
4432        ctlSet.add(PermissiveModifyRequestControl.PERMISSIVE_MODIFY_REQUEST_OID);
4433        ctlSet.add(PostReadRequestControl.POST_READ_REQUEST_OID);
4434        ctlSet.add(PreReadRequestControl.PRE_READ_REQUEST_OID);
4435        ctlSet.add(ProxiedAuthorizationV1RequestControl.
4436             PROXIED_AUTHORIZATION_V1_REQUEST_OID);
4437        ctlSet.add(ProxiedAuthorizationV2RequestControl.
4438             PROXIED_AUTHORIZATION_V2_REQUEST_OID);
4439        ctlSet.add(ServerSideSortRequestControl.SERVER_SIDE_SORT_REQUEST_OID);
4440        ctlSet.add(SimplePagedResultsControl.PAGED_RESULTS_OID);
4441        ctlSet.add(SubentriesRequestControl.SUBENTRIES_REQUEST_OID);
4442        ctlSet.add(SubtreeDeleteRequestControl.SUBTREE_DELETE_REQUEST_OID);
4443        ctlSet.add(TransactionSpecificationRequestControl.
4444             TRANSACTION_SPECIFICATION_REQUEST_OID);
4445        ctlSet.add(VirtualListViewRequestControl.VIRTUAL_LIST_VIEW_REQUEST_OID);
4446    
4447        final String[] controlOIDs = new String[ctlSet.size()];
4448        rootDSEEntry.addAttribute("supportedControl", ctlSet.toArray(controlOIDs));
4449    
4450    
4451        if (! extendedRequestHandlers.isEmpty())
4452        {
4453          final String[] oidArray = new String[extendedRequestHandlers.size()];
4454          rootDSEEntry.addAttribute("supportedExtension",
4455               extendedRequestHandlers.keySet().toArray(oidArray));
4456    
4457          for (final InMemoryListenerConfig c : config.getListenerConfigs())
4458          {
4459            if (c.getStartTLSSocketFactory() != null)
4460            {
4461              rootDSEEntry.addAttribute("supportedExtension",
4462                   StartTLSExtendedRequest.STARTTLS_REQUEST_OID);
4463              break;
4464            }
4465          }
4466        }
4467    
4468        if (! saslBindHandlers.isEmpty())
4469        {
4470          final String[] mechanismArray = new String[saslBindHandlers.size()];
4471          rootDSEEntry.addAttribute("supportedSASLMechanisms",
4472               saslBindHandlers.keySet().toArray(mechanismArray));
4473        }
4474    
4475        int pos = 0;
4476        final String[] baseDNStrings = new String[baseDNs.size()];
4477        for (final DN baseDN : baseDNs)
4478        {
4479          baseDNStrings[pos++] = baseDN.toString();
4480        }
4481        rootDSEEntry.addAttribute(new Attribute("namingContexts",
4482             DistinguishedNameMatchingRule.getInstance(), baseDNStrings));
4483    
4484        if (maxChangelogEntries > 0)
4485        {
4486          rootDSEEntry.addAttribute(new Attribute("changeLog",
4487               DistinguishedNameMatchingRule.getInstance(),
4488               changeLogBaseDN.toString()));
4489          rootDSEEntry.addAttribute(new Attribute("firstChangeNumber",
4490               IntegerMatchingRule.getInstance(), firstChangeNumber.toString()));
4491          rootDSEEntry.addAttribute(new Attribute("lastChangeNumber",
4492               IntegerMatchingRule.getInstance(), lastChangeNumber.toString()));
4493        }
4494    
4495        return new ReadOnlyEntry(rootDSEEntry);
4496      }
4497    
4498    
4499    
4500      /**
4501       * Generates a subschema subentry from the provided schema object.
4502       *
4503       * @param  schema  The schema to use to generate the subschema subentry.  It
4504       *                 may be {@code null} if a minimal default entry should be
4505       *                 generated.
4506       *
4507       * @return  The generated subschema subentry.
4508       */
4509      private static ReadOnlyEntry generateSubschemaSubentry(final Schema schema)
4510      {
4511        final Entry e;
4512    
4513        if (schema == null)
4514        {
4515          e = new Entry("cn=schema", schema);
4516    
4517          e.addAttribute("objectClass", "namedObject", "ldapSubEntry",
4518               "subschema");
4519          e.addAttribute("cn", "schema");
4520        }
4521        else
4522        {
4523          e = schema.getSchemaEntry().duplicate();
4524        }
4525    
4526        try
4527        {
4528          e.addAttribute("entryDN", DN.normalize(e.getDN(), schema));
4529        }
4530        catch (final LDAPException le)
4531        {
4532          // This should never happen.
4533          Debug.debugException(le);
4534          e.setAttribute("entryDN", StaticUtils.toLowerCase(e.getDN()));
4535        }
4536    
4537    
4538        e.addAttribute("entryUUID", UUID.randomUUID().toString());
4539        return new ReadOnlyEntry(e);
4540      }
4541    
4542    
4543    
4544      /**
4545       * Processes the set of requested attributes from the given search request.
4546       *
4547       * @param  attrList      The list of requested attributes to examine.
4548       * @param  allUserAttrs  Indicates whether to return all user attributes.  It
4549       *                       should have an initial value of {@code false}.
4550       * @param  allOpAttrs    Indicates whether to return all operational
4551       *                       attributes.  It should have an initial value of
4552       *                       {@code false}.
4553       *
4554       * @return  A map of specific attribute types to be returned.  The keys of the
4555       *          map will be the lowercase OID and names of the attribute types,
4556       *          and the values will be a list of option sets for the associated
4557       *          attribute type.
4558       */
4559      private Map<String,List<List<String>>> processRequestedAttributes(
4560                   final List<String> attrList, final AtomicBoolean allUserAttrs,
4561                   final AtomicBoolean allOpAttrs)
4562      {
4563        if (attrList.isEmpty())
4564        {
4565          allUserAttrs.set(true);
4566          return Collections.emptyMap();
4567        }
4568    
4569        final Schema schema = schemaRef.get();
4570        final HashMap<String,List<List<String>>> m =
4571             new HashMap<String,List<List<String>>>(attrList.size() * 2);
4572        for (final String s : attrList)
4573        {
4574          if (s.equals("*"))
4575          {
4576            // All user attributes.
4577            allUserAttrs.set(true);
4578          }
4579          else if (s.equals("+"))
4580          {
4581            // All operational attributes.
4582            allOpAttrs.set(true);
4583          }
4584          else if (s.startsWith("@"))
4585          {
4586            // Return attributes by object class.  This can only be supported if a
4587            // schema has been defined.
4588            if (schema != null)
4589            {
4590              final String ocName = s.substring(1);
4591              final ObjectClassDefinition oc = schema.getObjectClass(ocName);
4592              if (oc != null)
4593              {
4594                for (final AttributeTypeDefinition at :
4595                     oc.getRequiredAttributes(schema, true))
4596                {
4597                  addAttributeOIDAndNames(at, m, Collections.<String>emptyList());
4598                }
4599                for (final AttributeTypeDefinition at :
4600                     oc.getOptionalAttributes(schema, true))
4601                {
4602                  addAttributeOIDAndNames(at, m, Collections.<String>emptyList());
4603                }
4604              }
4605            }
4606          }
4607          else
4608          {
4609            final ObjectPair<String,List<String>> nameWithOptions =
4610                 getNameWithOptions(s);
4611            if (nameWithOptions == null)
4612            {
4613              continue;
4614            }
4615    
4616            final String name = nameWithOptions.getFirst();
4617            final List<String> options = nameWithOptions.getSecond();
4618    
4619            if (schema == null)
4620            {
4621              // Just use the name as provided.
4622              List<List<String>> optionLists = m.get(name);
4623              if (optionLists == null)
4624              {
4625                optionLists = new ArrayList<List<String>>(1);
4626                m.put(name, optionLists);
4627              }
4628              optionLists.add(options);
4629            }
4630            else
4631            {
4632              // If the attribute type is defined in the schema, then use it to get
4633              // all names and the OID.  Otherwise, just use the name as provided.
4634              final AttributeTypeDefinition at = schema.getAttributeType(name);
4635              if (at == null)
4636              {
4637                List<List<String>> optionLists = m.get(name);
4638                if (optionLists == null)
4639                {
4640                  optionLists = new ArrayList<List<String>>(1);
4641                  m.put(name, optionLists);
4642                }
4643                optionLists.add(options);
4644              }
4645              else
4646              {
4647                addAttributeOIDAndNames(at, m, options);
4648              }
4649            }
4650          }
4651        }
4652    
4653        return m;
4654      }
4655    
4656    
4657    
4658      /**
4659       * Parses the provided string into an attribute type and set of options.
4660       *
4661       * @param  s  The string to be parsed.
4662       *
4663       * @return  An {@code ObjectPair} in which the first element is the attribute
4664       *          type name and the second is the list of options (or an empty
4665       *          list if there are no options).  Alternately, a value of
4666       *          {@code null} may be returned if the provided string does not
4667       *          represent a valid attribute type description.
4668       */
4669      private static ObjectPair<String,List<String>> getNameWithOptions(
4670                                                          final String s)
4671      {
4672        if (! Attribute.nameIsValid(s, true))
4673        {
4674          return null;
4675        }
4676    
4677        final String l = StaticUtils.toLowerCase(s);
4678    
4679        int semicolonPos = l.indexOf(';');
4680        if (semicolonPos < 0)
4681        {
4682          return new ObjectPair<String,List<String>>(l,
4683               Collections.<String>emptyList());
4684        }
4685    
4686        final String name = l.substring(0, semicolonPos);
4687        final ArrayList<String> optionList = new ArrayList<String>(1);
4688        while (true)
4689        {
4690          final int nextSemicolonPos = l.indexOf(';', semicolonPos+1);
4691          if (nextSemicolonPos < 0)
4692          {
4693            optionList.add(l.substring(semicolonPos+1));
4694            break;
4695          }
4696          else
4697          {
4698            optionList.add(l.substring(semicolonPos+1, nextSemicolonPos));
4699            semicolonPos = nextSemicolonPos;
4700          }
4701        }
4702    
4703        return new ObjectPair<String,List<String>>(name, optionList);
4704      }
4705    
4706    
4707    
4708      /**
4709       * Adds all-lowercase versions of the OID and all names for the provided
4710       * attribute type definition to the given map with the given options.
4711       *
4712       * @param  d  The attribute type definition to process.
4713       * @param  m  The map to which the OID and names should be added.
4714       * @param  o  The array of attribute options to use in the map.  It should be
4715       *            empty if no options are needed, and must not be {@code null}.
4716       */
4717      private void addAttributeOIDAndNames(final AttributeTypeDefinition d,
4718                                           final Map<String,List<List<String>>> m,
4719                                           final List<String> o)
4720      {
4721        if (d == null)
4722        {
4723          return;
4724        }
4725    
4726        final String lowerOID = StaticUtils.toLowerCase(d.getOID());
4727        if (lowerOID != null)
4728        {
4729          List<List<String>> l = m.get(lowerOID);
4730          if (l == null)
4731          {
4732            l = new ArrayList<List<String>>(1);
4733            m.put(lowerOID, l);
4734          }
4735    
4736          l.add(o);
4737        }
4738    
4739        for (final String name : d.getNames())
4740        {
4741          final String lowerName = StaticUtils.toLowerCase(name);
4742          List<List<String>> l = m.get(lowerName);
4743          if (l == null)
4744          {
4745            l = new ArrayList<List<String>>(1);
4746            m.put(lowerName, l);
4747          }
4748    
4749          l.add(o);
4750        }
4751    
4752        // If a schema is available, then see if the attribute type has any
4753        // subordinate types.  If so, then add them.
4754        final Schema schema = schemaRef.get();
4755        if (schema != null)
4756        {
4757          for (final AttributeTypeDefinition subordinateType :
4758               schema.getSubordinateAttributeTypes(d))
4759          {
4760            addAttributeOIDAndNames(subordinateType, m, o);
4761          }
4762        }
4763      }
4764    
4765    
4766    
4767      /**
4768       * Performs the necessary processing to determine whether the given entry
4769       * should be returned as a search result entry or reference, or if it should
4770       * not be returned at all.
4771       *
4772       * @param  entry              The entry to be processed.
4773       * @param  includeSubEntries  Indicates whether LDAP subentries should be
4774       *                            returned to the client.
4775       * @param  includeChangeLog   Indicates whether entries within the changelog
4776       *                            should be returned to the client.
4777       * @param  hasManageDsaIT     Indicates whether the request includes the
4778       *                            ManageDsaIT control, which can change how smart
4779       *                            referrals should be handled.
4780       * @param  entryList          The list to which the entry should be added if
4781       *                            it should be returned to the client as a search
4782       *                            result entry.
4783       * @param  referenceList      The list that should be updated if the provided
4784       *                            entry represents a smart referral that should be
4785       *                            returned as a search result reference.
4786       */
4787      private void processSearchEntry(final Entry entry,
4788                        final boolean includeSubEntries,
4789                        final boolean includeChangeLog,
4790                        final boolean hasManageDsaIT,
4791                        final List<Entry> entryList,
4792                        final List<SearchResultReference> referenceList)
4793      {
4794        // See if the entry should be suppressed as an LDAP subentry.
4795        if ((! includeSubEntries) &&
4796            (entry.hasObjectClass("ldapSubEntry") ||
4797             entry.hasObjectClass("inheritableLDAPSubEntry")))
4798        {
4799          return;
4800        }
4801    
4802        // See if the entry should be suppressed as a changelog entry.
4803        try
4804        {
4805          if ((! includeChangeLog) &&
4806               (entry.getParsedDN().isDescendantOf(changeLogBaseDN, true)))
4807          {
4808            return;
4809          }
4810        }
4811        catch (final Exception e)
4812        {
4813          // This should never happen.
4814          Debug.debugException(e);
4815        }
4816    
4817        // See if the entry is a referral and should result in a reference rather
4818        // than an entry.
4819        if ((! hasManageDsaIT) && entry.hasObjectClass("referral") &&
4820            entry.hasAttribute("ref"))
4821        {
4822          referenceList.add(new SearchResultReference(
4823               entry.getAttributeValues("ref"), NO_CONTROLS));
4824          return;
4825        }
4826    
4827        entryList.add(entry);
4828      }
4829    
4830    
4831    
4832      /**
4833       * Retrieves a copy of the provided entry that includes only the appropriate
4834       * set of requested attributes.
4835       *
4836       * @param  entry         The entry to be returned.
4837       * @param  allUserAttrs  Indicates whether to return all user attributes.
4838       * @param  allOpAttrs    Indicates whether to return all operational
4839       *                       attributes.
4840       * @param  returnAttrs   A map with information about the specific attribute
4841       *                       types to return.
4842       *
4843       * @return  A copy of the provided entry that includes only the appropriate
4844       *          set of requested attributes.
4845       */
4846      private Entry trimForRequestedAttributes(final Entry entry,
4847                         final boolean allUserAttrs, final boolean allOpAttrs,
4848                         final Map<String,List<List<String>>> returnAttrs)
4849      {
4850        // See if we can return the entry without paring it down.
4851        final Schema schema = schemaRef.get();
4852        if (allUserAttrs)
4853        {
4854          if (allOpAttrs || (schema == null))
4855          {
4856            return entry;
4857          }
4858        }
4859    
4860    
4861        // If we've gotten here, then we may only need to return a partial entry.
4862        final Entry copy = new Entry(entry.getDN(), schema);
4863    
4864        for (final Attribute a : entry.getAttributes())
4865        {
4866          final ObjectPair<String,List<String>> nameWithOptions =
4867               getNameWithOptions(a.getName());
4868          final String name = nameWithOptions.getFirst();
4869          final List<String> options = nameWithOptions.getSecond();
4870    
4871          // If there is a schema, then see if it is an operational attribute, since
4872          // that needs to be handled in a manner different from user attributes
4873          if (schema != null)
4874          {
4875            final AttributeTypeDefinition at = schema.getAttributeType(name);
4876            if ((at != null) && at.isOperational())
4877            {
4878              if (allOpAttrs)
4879              {
4880                copy.addAttribute(a);
4881                continue;
4882              }
4883    
4884              final List<List<String>> optionLists = returnAttrs.get(name);
4885              if (optionLists == null)
4886              {
4887                continue;
4888              }
4889    
4890              for (final List<String> optionList : optionLists)
4891              {
4892                boolean matchAll = true;
4893                for (final String option : optionList)
4894                {
4895                  if (! options.contains(option))
4896                  {
4897                    matchAll = false;
4898                    break;
4899                  }
4900                }
4901    
4902                if (matchAll)
4903                {
4904                  copy.addAttribute(a);
4905                  break;
4906                }
4907              }
4908              continue;
4909            }
4910          }
4911    
4912          // We'll assume that it's a user attribute, and we'll look for an exact
4913          // match on the base name.
4914          if (allUserAttrs)
4915          {
4916            copy.addAttribute(a);
4917            continue;
4918          }
4919    
4920          final List<List<String>> optionLists = returnAttrs.get(name);
4921          if (optionLists == null)
4922          {
4923            continue;
4924          }
4925    
4926          for (final List<String> optionList : optionLists)
4927          {
4928            boolean matchAll = true;
4929            for (final String option : optionList)
4930            {
4931              if (! options.contains(option))
4932              {
4933                matchAll = false;
4934                break;
4935              }
4936            }
4937    
4938            if (matchAll)
4939            {
4940              copy.addAttribute(a);
4941              break;
4942            }
4943          }
4944        }
4945    
4946        return copy;
4947      }
4948    
4949    
4950    
4951      /**
4952       * Retrieves the DN of the existing entry which is the closest hierarchical
4953       * match to the provided DN.
4954       *
4955       * @param  dn  The DN for which to retrieve the appropriate matched DN.
4956       *
4957       * @return  The appropriate matched DN value, or {@code null} if there is
4958       *          none.
4959       */
4960      private String getMatchedDNString(final DN dn)
4961      {
4962        DN parentDN = dn.getParent();
4963        while (parentDN != null)
4964        {
4965          if (entryMap.containsKey(parentDN))
4966          {
4967            return parentDN.toString();
4968          }
4969    
4970          parentDN = parentDN.getParent();
4971        }
4972    
4973        return null;
4974      }
4975    
4976    
4977    
4978      /**
4979       * Converts the provided string list to an array.
4980       *
4981       * @param  l  The possibly null list to be converted.
4982       *
4983       * @return  The string array with the same elements as the given list in the
4984       *          same order, or {@code null} if the given list was null.
4985       */
4986      private static String[] stringListToArray(final List<String> l)
4987      {
4988        if (l == null)
4989        {
4990          return null;
4991        }
4992        else
4993        {
4994          final String[] a = new String[l.size()];
4995          return l.toArray(a);
4996        }
4997      }
4998    
4999    
5000    
5001      /**
5002       * Creates a changelog entry from the information in the provided add request
5003       * and adds it to the server changelog.
5004       *
5005       * @param  addRequest  The add request to use to construct the changelog
5006       *                     entry.
5007       * @param  authzDN     The authorization DN for the change.
5008       */
5009      private void addChangeLogEntry(final AddRequestProtocolOp addRequest,
5010                                     final DN authzDN)
5011      {
5012        // If the changelog is disabled, then don't do anything.
5013        if (maxChangelogEntries <= 0)
5014        {
5015          return;
5016        }
5017    
5018        final long changeNumber = lastChangeNumber.incrementAndGet();
5019        final LDIFAddChangeRecord changeRecord = new LDIFAddChangeRecord(
5020             addRequest.getDN(), addRequest.getAttributes());
5021        try
5022        {
5023          addChangeLogEntry(
5024               ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord),
5025               authzDN);
5026        }
5027        catch (final LDAPException le)
5028        {
5029          // This should not happen.
5030          Debug.debugException(le);
5031        }
5032      }
5033    
5034    
5035    
5036      /**
5037       * Creates a changelog entry from the information in the provided delete
5038       * request and adds it to the server changelog.
5039       *
5040       * @param  e        The entry to be deleted.
5041       * @param  authzDN  The authorization DN for the change.
5042       */
5043      private void addDeleteChangeLogEntry(final Entry e, final DN authzDN)
5044      {
5045        // If the changelog is disabled, then don't do anything.
5046        if (maxChangelogEntries <= 0)
5047        {
5048          return;
5049        }
5050    
5051        final long changeNumber = lastChangeNumber.incrementAndGet();
5052        final LDIFDeleteChangeRecord changeRecord =
5053             new LDIFDeleteChangeRecord(e.getDN());
5054    
5055        // Create the changelog entry.
5056        try
5057        {
5058          final ChangeLogEntry cle = ChangeLogEntry.constructChangeLogEntry(
5059               changeNumber, changeRecord);
5060    
5061          // Add a set of deleted entry attributes, which is simply an LDIF-encoded
5062          // representation of the entry, excluding the first line since it contains
5063          // the DN.
5064          final StringBuilder deletedEntryAttrsBuffer = new StringBuilder();
5065          final String[] ldifLines = e.toLDIF(0);
5066          for (int i=1; i < ldifLines.length; i++)
5067          {
5068            deletedEntryAttrsBuffer.append(ldifLines[i]);
5069            deletedEntryAttrsBuffer.append(StaticUtils.EOL);
5070          }
5071    
5072          final Entry copy = cle.duplicate();
5073          copy.addAttribute(ChangeLogEntry.ATTR_DELETED_ENTRY_ATTRS,
5074               deletedEntryAttrsBuffer.toString());
5075          addChangeLogEntry(new ChangeLogEntry(copy), authzDN);
5076        }
5077        catch (final LDAPException le)
5078        {
5079          // This should never happen.
5080          Debug.debugException(le);
5081        }
5082      }
5083    
5084    
5085    
5086      /**
5087       * Creates a changelog entry from the information in the provided modify
5088       * request and adds it to the server changelog.
5089       *
5090       * @param  modifyRequest  The modify request to use to construct the changelog
5091       *                        entry.
5092       * @param  authzDN        The authorization DN for the change.
5093       */
5094      private void addChangeLogEntry(final ModifyRequestProtocolOp modifyRequest,
5095                                     final DN authzDN)
5096      {
5097        // If the changelog is disabled, then don't do anything.
5098        if (maxChangelogEntries <= 0)
5099        {
5100          return;
5101        }
5102    
5103        final long changeNumber = lastChangeNumber.incrementAndGet();
5104        final LDIFModifyChangeRecord changeRecord =
5105             new LDIFModifyChangeRecord(modifyRequest.getDN(),
5106                  modifyRequest.getModifications());
5107        try
5108        {
5109          addChangeLogEntry(
5110               ChangeLogEntry.constructChangeLogEntry(changeNumber, changeRecord),
5111               authzDN);
5112        }
5113        catch (final LDAPException le)
5114        {
5115          // This should not happen.
5116          Debug.debugException(le);
5117        }
5118      }
5119    
5120    
5121    
5122      /**
5123       * Creates a changelog entry from the information in the provided modify DN
5124       * request and adds it to the server changelog.
5125       *
5126       * @param  modifyDNRequest  The modify DN request to use to construct the
5127       *                          changelog entry.
5128       * @param  authzDN          The authorization DN for the change.
5129       */
5130      private void addChangeLogEntry(
5131                        final ModifyDNRequestProtocolOp modifyDNRequest,
5132                        final DN authzDN)
5133      {
5134        // If the changelog is disabled, then don't do anything.
5135        if (maxChangelogEntries <= 0)
5136        {
5137          return;
5138        }
5139    
5140        final long changeNumber = lastChangeNumber.incrementAndGet();
5141        final LDIFModifyDNChangeRecord changeRecord =
5142             new LDIFModifyDNChangeRecord(modifyDNRequest.getDN(),
5143                  modifyDNRequest.getNewRDN(), modifyDNRequest.deleteOldRDN(),
5144                  modifyDNRequest.getNewSuperiorDN());
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       * Adds the provided changelog entry to the data set, removing an old entry if
5162       * necessary to remain within the maximum allowed number of changes.  This
5163       * must only be called from a synchronized method, and the change number for
5164       * the changelog entry must have been obtained by calling
5165       * {@code lastChangeNumber.incrementAndGet()}.
5166       *
5167       * @param  e        The changelog entry to add to the data set.
5168       * @param  authzDN  The authorization DN for the change.
5169       */
5170      private void addChangeLogEntry(final ChangeLogEntry e, final DN authzDN)
5171      {
5172        // Construct the DN object to use for the entry and put it in the map.
5173        final long changeNumber = e.getChangeNumber();
5174        final Schema schema = schemaRef.get();
5175        final DN dn = new DN(
5176             new RDN("changeNumber", String.valueOf(changeNumber), schema),
5177             changeLogBaseDN);
5178    
5179        final Entry entry = e.duplicate();
5180        if (generateOperationalAttributes)
5181        {
5182          final Date d = new Date();
5183          entry.addAttribute(new Attribute("entryDN",
5184               DistinguishedNameMatchingRule.getInstance(),
5185               dn.toNormalizedString()));
5186          entry.addAttribute(new Attribute("entryUUID",
5187               UUID.randomUUID().toString()));
5188          entry.addAttribute(new Attribute("subschemaSubentry",
5189               DistinguishedNameMatchingRule.getInstance(),
5190               subschemaSubentryDN.toString()));
5191          entry.addAttribute(new Attribute("creatorsName",
5192               DistinguishedNameMatchingRule.getInstance(),
5193               authzDN.toString()));
5194          entry.addAttribute(new Attribute("createTimestamp",
5195               GeneralizedTimeMatchingRule.getInstance(),
5196               StaticUtils.encodeGeneralizedTime(d)));
5197          entry.addAttribute(new Attribute("modifiersName",
5198               DistinguishedNameMatchingRule.getInstance(),
5199               authzDN.toString()));
5200          entry.addAttribute(new Attribute("modifyTimestamp",
5201               GeneralizedTimeMatchingRule.getInstance(),
5202               StaticUtils.encodeGeneralizedTime(d)));
5203        }
5204    
5205        entryMap.put(dn, new ReadOnlyEntry(entry));
5206        indexAdd(entry);
5207    
5208        // Update the first change number and/or trim the changelog if necessary.
5209        final long firstNumber = firstChangeNumber.get();
5210        if (changeNumber == 1L)
5211        {
5212          // It's the first change, so we need to set the first change number.
5213          firstChangeNumber.set(1);
5214        }
5215        else
5216        {
5217          // See if we need to trim an entry.
5218          final long numChangeLogEntries = changeNumber - firstNumber + 1;
5219          if (numChangeLogEntries > maxChangelogEntries)
5220          {
5221            // We need to delete the first changelog entry and increment the
5222            // first change number.
5223            firstChangeNumber.incrementAndGet();
5224            final Entry deletedEntry = entryMap.remove(new DN(
5225                 new RDN("changeNumber", String.valueOf(firstNumber), schema),
5226                 changeLogBaseDN));
5227            indexDelete(deletedEntry);
5228          }
5229        }
5230      }
5231    
5232    
5233    
5234      /**
5235       * Checks to see if the provided control map includes a proxied authorization
5236       * control (v1 or v2) and if so then attempts to determine the appropriate
5237       * authorization identity to use for the operation.
5238       *
5239       * @param  m  The map of request controls, indexed by OID.
5240       *
5241       * @return  The DN of the authorized user, or the current authentication DN
5242       *          if the control map does not include a proxied authorization
5243       *          request control.
5244       *
5245       * @throws  LDAPException  If a problem is encountered while attempting to
5246       *                         determine the authorization DN.
5247       */
5248      private DN handleProxiedAuthControl(final Map<String,Control> m)
5249              throws LDAPException
5250      {
5251        final ProxiedAuthorizationV1RequestControl p1 =
5252             (ProxiedAuthorizationV1RequestControl) m.get(
5253                  ProxiedAuthorizationV1RequestControl.
5254                       PROXIED_AUTHORIZATION_V1_REQUEST_OID);
5255        if (p1 != null)
5256        {
5257          final DN authzDN = new DN(p1.getProxyDN(), schemaRef.get());
5258          if (authzDN.isNullDN() ||
5259              entryMap.containsKey(authzDN) ||
5260              additionalBindCredentials.containsKey(authzDN))
5261          {
5262            return authzDN;
5263          }
5264          else
5265          {
5266            throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
5267                 ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get("dn:" + authzDN.toString()));
5268          }
5269        }
5270    
5271        final ProxiedAuthorizationV2RequestControl p2 =
5272             (ProxiedAuthorizationV2RequestControl) m.get(
5273                  ProxiedAuthorizationV2RequestControl.
5274                       PROXIED_AUTHORIZATION_V2_REQUEST_OID);
5275        if (p2 != null)
5276        {
5277          return getDNForAuthzID(p2.getAuthorizationID());
5278        }
5279    
5280        return authenticatedDN;
5281      }
5282    
5283    
5284    
5285      /**
5286       * Attempts to identify the DN of the user referenced by the provided
5287       * authorization ID string.  It may be "dn:" followed by the target DN, or
5288       * "u:" followed by the value of the uid attribute in the entry.  If it uses
5289       * the "dn:" form, then it may reference the DN of a regular entry or a DN
5290       * in the configured set of additional bind credentials.
5291       *
5292       * @param  authzID  The authorization ID to resolve to a user DN.
5293       *
5294       * @return  The DN identified for the provided authorization ID.
5295       *
5296       * @throws  LDAPException  If a problem prevents resolving the authorization
5297       *                         ID to a user DN.
5298       */
5299      public DN getDNForAuthzID(final String authzID)
5300             throws LDAPException
5301      {
5302        synchronized (entryMap)
5303        {
5304          final String lowerAuthzID = StaticUtils.toLowerCase(authzID);
5305          if (lowerAuthzID.startsWith("dn:"))
5306          {
5307            if (lowerAuthzID.equals("dn:"))
5308            {
5309              return DN.NULL_DN;
5310            }
5311            else
5312            {
5313              final DN dn = new DN(authzID.substring(3), schemaRef.get());
5314              if (entryMap.containsKey(dn) ||
5315                   additionalBindCredentials.containsKey(dn))
5316              {
5317                return dn;
5318              }
5319              else
5320              {
5321                throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
5322                     ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID));
5323              }
5324            }
5325          }
5326          else if (lowerAuthzID.startsWith("u:"))
5327          {
5328            final Filter f =
5329                 Filter.createEqualityFilter("uid", authzID.substring(2));
5330            final List<ReadOnlyEntry> entryList = search("", SearchScope.SUB, f);
5331            if (entryList.size() == 1)
5332            {
5333              return entryList.get(0).getParsedDN();
5334            }
5335            else
5336            {
5337              throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
5338                   ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID));
5339            }
5340          }
5341          else
5342          {
5343            throw new LDAPException(ResultCode.AUTHORIZATION_DENIED,
5344                 ERR_MEM_HANDLER_NO_SUCH_IDENTITY.get(authzID));
5345          }
5346        }
5347      }
5348    
5349    
5350    
5351      /**
5352       * Checks to see if the provided control map includes an assertion request
5353       * control, and if so then checks to see whether the provided entry satisfies
5354       * the filter in that control.
5355       *
5356       * @param  m  The map of request controls, indexed by OID.
5357       * @param  e  The entry to examine against the assertion filter.
5358       *
5359       * @throws  LDAPException  If the control map includes an assertion request
5360       *                         control and the provided entry does not match the
5361       *                         filter contained in that control.
5362       */
5363      private static void handleAssertionRequestControl(final Map<String,Control> m,
5364                                                        final Entry e)
5365              throws LDAPException
5366      {
5367        final AssertionRequestControl c = (AssertionRequestControl)
5368             m.get(AssertionRequestControl.ASSERTION_REQUEST_OID);
5369        if (c == null)
5370        {
5371          return;
5372        }
5373    
5374        try
5375        {
5376          if (c.getFilter().matchesEntry(e))
5377          {
5378            return;
5379          }
5380        }
5381        catch (final LDAPException le)
5382        {
5383          Debug.debugException(le);
5384        }
5385    
5386        // If we've gotten here, then the filter doesn't match.
5387        throw new LDAPException(ResultCode.ASSERTION_FAILED,
5388             ERR_MEM_HANDLER_ASSERTION_CONTROL_NOT_SATISFIED.get());
5389      }
5390    
5391    
5392    
5393      /**
5394       * Checks to see if the provided control map includes a pre-read request
5395       * control, and if so then generates the appropriate response control that
5396       * should be returned to the client.
5397       *
5398       * @param  m  The map of request controls, indexed by OID.
5399       * @param  e  The entry as it appeared before the operation.
5400       *
5401       * @return  The pre-read response control that should be returned to the
5402       *          client, or {@code null} if there is none.
5403       */
5404      private PreReadResponseControl handlePreReadControl(
5405                   final Map<String,Control> m, final Entry e)
5406      {
5407        final PreReadRequestControl c = (PreReadRequestControl)
5408             m.get(PreReadRequestControl.PRE_READ_REQUEST_OID);
5409        if (c == null)
5410        {
5411          return null;
5412        }
5413    
5414        final AtomicBoolean allUserAttrs = new AtomicBoolean(false);
5415        final AtomicBoolean allOpAttrs = new AtomicBoolean(false);
5416        final Map<String,List<List<String>>> returnAttrs =
5417             processRequestedAttributes(Arrays.asList(c.getAttributes()),
5418                  allUserAttrs, allOpAttrs);
5419    
5420        final Entry trimmedEntry = trimForRequestedAttributes(e, allUserAttrs.get(),
5421             allOpAttrs.get(), returnAttrs);
5422        return new PreReadResponseControl(new ReadOnlyEntry(trimmedEntry));
5423      }
5424    
5425    
5426    
5427      /**
5428       * Checks to see if the provided control map includes a post-read request
5429       * control, and if so then generates the appropriate response control that
5430       * should be returned to the client.
5431       *
5432       * @param  m  The map of request controls, indexed by OID.
5433       * @param  e  The entry as it appeared before the operation.
5434       *
5435       * @return  The post-read response control that should be returned to the
5436       *          client, or {@code null} if there is none.
5437       */
5438      private PostReadResponseControl handlePostReadControl(
5439                   final Map<String,Control> m, final Entry e)
5440      {
5441        final PostReadRequestControl c = (PostReadRequestControl)
5442             m.get(PostReadRequestControl.POST_READ_REQUEST_OID);
5443        if (c == null)
5444        {
5445          return null;
5446        }
5447    
5448        final AtomicBoolean allUserAttrs = new AtomicBoolean(false);
5449        final AtomicBoolean allOpAttrs = new AtomicBoolean(false);
5450        final Map<String,List<List<String>>> returnAttrs =
5451             processRequestedAttributes(Arrays.asList(c.getAttributes()),
5452                  allUserAttrs, allOpAttrs);
5453    
5454        final Entry trimmedEntry = trimForRequestedAttributes(e, allUserAttrs.get(),
5455             allOpAttrs.get(), returnAttrs);
5456        return new PostReadResponseControl(new ReadOnlyEntry(trimmedEntry));
5457      }
5458    
5459    
5460    
5461      /**
5462       * Finds the smart referral entry which is hierarchically nearest the entry
5463       * with the given DN.
5464       *
5465       * @param  dn  The DN for which to find the hierarchically nearest smart
5466       *             referral entry.
5467       *
5468       * @return  The hierarchically nearest smart referral entry for the provided
5469       *          DN, or {@code null} if there are no smart referral entries with
5470       *          the provided DN or any of its ancestors.
5471       */
5472      private Entry findNearestReferral(final DN dn)
5473      {
5474        DN d = dn;
5475        while (true)
5476        {
5477          final Entry e = entryMap.get(d);
5478          if (e == null)
5479          {
5480            d = d.getParent();
5481            if (d == null)
5482            {
5483              return null;
5484            }
5485          }
5486          else if (e.hasObjectClass("referral"))
5487          {
5488            return e;
5489          }
5490          else
5491          {
5492            return null;
5493          }
5494        }
5495      }
5496    
5497    
5498    
5499      /**
5500       * Retrieves the referral URLs that should be used for the provided target DN
5501       * based on the given referral entry.
5502       *
5503       * @param  targetDN       The target DN from the associated operation.
5504       * @param  referralEntry  The entry containing the smart referral.
5505       *
5506       * @return  The referral URLs that should be returned.
5507       */
5508      private static List<String> getReferralURLs(final DN targetDN,
5509                                                  final Entry referralEntry)
5510      {
5511        final String[] refs = referralEntry.getAttributeValues("ref");
5512        if (refs == null)
5513        {
5514          return null;
5515        }
5516    
5517        final RDN[] retainRDNs;
5518        try
5519        {
5520          // If the target DN equals the referral entry DN, or if it's not
5521          // subordinate to the referral entry, then the URLs should be returned
5522          // as-is.
5523          final DN parsedEntryDN = referralEntry.getParsedDN();
5524          if (targetDN.equals(parsedEntryDN) ||
5525              (! targetDN.isDescendantOf(parsedEntryDN, true)))
5526          {
5527            return Arrays.asList(refs);
5528          }
5529    
5530          final RDN[] targetRDNs   = targetDN.getRDNs();
5531          final RDN[] refEntryRDNs = referralEntry.getParsedDN().getRDNs();
5532          retainRDNs = new RDN[targetRDNs.length - refEntryRDNs.length];
5533          System.arraycopy(targetRDNs, 0, retainRDNs, 0, retainRDNs.length);
5534        }
5535        catch (final LDAPException le)
5536        {
5537          Debug.debugException(le);
5538          return Arrays.asList(refs);
5539        }
5540    
5541        final List<String> refList = new ArrayList<String>(refs.length);
5542        for (final String ref : refs)
5543        {
5544          try
5545          {
5546            final LDAPURL url = new LDAPURL(ref);
5547            final RDN[] refRDNs = url.getBaseDN().getRDNs();
5548            final RDN[] newRefRDNs = new RDN[retainRDNs.length + refRDNs.length];
5549            System.arraycopy(retainRDNs, 0, newRefRDNs, 0, retainRDNs.length);
5550            System.arraycopy(refRDNs, 0, newRefRDNs, retainRDNs.length,
5551                 refRDNs.length);
5552            final DN newBaseDN = new DN(newRefRDNs);
5553    
5554            final LDAPURL newURL = new LDAPURL(url.getScheme(), url.getHost(),
5555                 url.getPort(), newBaseDN, null, null, null);
5556            refList.add(newURL.toString());
5557          }
5558          catch (final LDAPException le)
5559          {
5560            Debug.debugException(le);
5561            refList.add(ref);
5562          }
5563        }
5564    
5565        return refList;
5566      }
5567    
5568    
5569    
5570      /**
5571       * Indicates whether the specified entry exists in the server.
5572       *
5573       * @param  dn  The DN of the entry for which to make the determination.
5574       *
5575       * @return  {@code true} if the entry exists, or {@code false} if not.
5576       *
5577       * @throws  LDAPException  If a problem is encountered while trying to
5578       *                         communicate with the directory server.
5579       */
5580      public boolean entryExists(final String dn)
5581             throws LDAPException
5582      {
5583        return (getEntry(dn) != null);
5584      }
5585    
5586    
5587    
5588      /**
5589       * Indicates whether the specified entry exists in the server and matches the
5590       * given filter.
5591       *
5592       * @param  dn      The DN of the entry for which to make the determination.
5593       * @param  filter  The filter the entry is expected to match.
5594       *
5595       * @return  {@code true} if the entry exists and matches the specified filter,
5596       *          or {@code false} if not.
5597       *
5598       * @throws  LDAPException  If a problem is encountered while trying to
5599       *                         communicate with the directory server.
5600       */
5601      public boolean entryExists(final String dn, final String filter)
5602             throws LDAPException
5603      {
5604        synchronized (entryMap)
5605        {
5606          final Entry e = getEntry(dn);
5607          if (e == null)
5608          {
5609            return false;
5610          }
5611    
5612          final Filter f = Filter.create(filter);
5613          try
5614          {
5615            return f.matchesEntry(e, schemaRef.get());
5616          }
5617          catch (final LDAPException le)
5618          {
5619            Debug.debugException(le);
5620            return false;
5621          }
5622        }
5623      }
5624    
5625    
5626    
5627      /**
5628       * Indicates whether the specified entry exists in the server.  This will
5629       * return {@code true} only if the target entry exists and contains all values
5630       * for all attributes of the provided entry.  The entry will be allowed to
5631       * have attribute values not included in the provided entry.
5632       *
5633       * @param  entry  The entry to compare against the directory server.
5634       *
5635       * @return  {@code true} if the entry exists in the server and is a superset
5636       *          of the provided entry, or {@code false} if not.
5637       *
5638       * @throws  LDAPException  If a problem is encountered while trying to
5639       *                         communicate with the directory server.
5640       */
5641      public boolean entryExists(final Entry entry)
5642             throws LDAPException
5643      {
5644        synchronized (entryMap)
5645        {
5646          final Entry e = getEntry(entry.getDN());
5647          if (e == null)
5648          {
5649            return false;
5650          }
5651    
5652          for (final Attribute a : entry.getAttributes())
5653          {
5654            for (final byte[] value : a.getValueByteArrays())
5655            {
5656              if (! e.hasAttributeValue(a.getName(), value))
5657              {
5658                return false;
5659              }
5660            }
5661          }
5662    
5663          return true;
5664        }
5665      }
5666    
5667    
5668    
5669      /**
5670       * Ensures that an entry with the provided DN exists in the directory.
5671       *
5672       * @param  dn  The DN of the entry for which to make the determination.
5673       *
5674       * @throws  LDAPException  If a problem is encountered while trying to
5675       *                         communicate with the directory server.
5676       *
5677       * @throws  AssertionError  If the target entry does not exist.
5678       */
5679      public void assertEntryExists(final String dn)
5680             throws LDAPException, AssertionError
5681      {
5682        final Entry e = getEntry(dn);
5683        if (e == null)
5684        {
5685          throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
5686        }
5687      }
5688    
5689    
5690    
5691      /**
5692       * Ensures that an entry with the provided DN exists in the directory.
5693       *
5694       * @param  dn      The DN of the entry for which to make the determination.
5695       * @param  filter  A filter that the target entry must match.
5696       *
5697       * @throws  LDAPException  If a problem is encountered while trying to
5698       *                         communicate with the directory server.
5699       *
5700       * @throws  AssertionError  If the target entry does not exist or does not
5701       *                          match the provided filter.
5702       */
5703      public void assertEntryExists(final String dn, final String filter)
5704             throws LDAPException, AssertionError
5705      {
5706        synchronized (entryMap)
5707        {
5708          final Entry e = getEntry(dn);
5709          if (e == null)
5710          {
5711            throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
5712          }
5713    
5714          final Filter f = Filter.create(filter);
5715          try
5716          {
5717            if (! f.matchesEntry(e, schemaRef.get()))
5718            {
5719              throw new AssertionError(
5720                   ERR_MEM_HANDLER_TEST_ENTRY_DOES_NOT_MATCH_FILTER.get(dn,
5721                        filter));
5722            }
5723          }
5724          catch (final LDAPException le)
5725          {
5726            Debug.debugException(le);
5727            throw new AssertionError(
5728                 ERR_MEM_HANDLER_TEST_ENTRY_DOES_NOT_MATCH_FILTER.get(dn, filter));
5729          }
5730        }
5731      }
5732    
5733    
5734    
5735      /**
5736       * Ensures that an entry exists in the directory with the same DN and all
5737       * attribute values contained in the provided entry.  The server entry may
5738       * contain additional attributes and/or attribute values not included in the
5739       * provided entry.
5740       *
5741       * @param  entry  The entry expected to be present in the directory server.
5742       *
5743       * @throws  LDAPException  If a problem is encountered while trying to
5744       *                         communicate with the directory server.
5745       *
5746       * @throws  AssertionError  If the target entry does not exist or does not
5747       *                          match the provided filter.
5748       */
5749      public void assertEntryExists(final Entry entry)
5750             throws LDAPException, AssertionError
5751      {
5752        synchronized (entryMap)
5753        {
5754          final Entry e = getEntry(entry.getDN());
5755          if (e == null)
5756          {
5757            throw new AssertionError(
5758                 ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(entry.getDN()));
5759          }
5760    
5761    
5762          final Collection<Attribute> attrs = entry.getAttributes();
5763          final List<String> messages = new ArrayList<String>(attrs.size());
5764    
5765          final Schema schema = schemaRef.get();
5766          for (final Attribute a : entry.getAttributes())
5767          {
5768            final Filter presFilter = Filter.createPresenceFilter(a.getName());
5769            if (! presFilter.matchesEntry(e, schema))
5770            {
5771              messages.add(ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(entry.getDN(),
5772                   a.getName()));
5773              continue;
5774            }
5775    
5776            for (final byte[] value : a.getValueByteArrays())
5777            {
5778              final Filter eqFilter = Filter.createEqualityFilter(a.getName(),
5779                   value);
5780              if (! eqFilter.matchesEntry(e, schema))
5781              {
5782                messages.add(ERR_MEM_HANDLER_TEST_VALUE_MISSING.get(entry.getDN(),
5783                     a.getName(), StaticUtils.toUTF8String(value)));
5784              }
5785            }
5786          }
5787    
5788          if (! messages.isEmpty())
5789          {
5790            throw new AssertionError(StaticUtils.concatenateStrings(messages));
5791          }
5792        }
5793      }
5794    
5795    
5796    
5797      /**
5798       * Retrieves a list containing the DNs of the entries which are missing from
5799       * the directory server.
5800       *
5801       * @param  dns  The DNs of the entries to try to find in the server.
5802       *
5803       * @return  A list containing all of the provided DNs that were not found in
5804       *          the server, or an empty list if all entries were found.
5805       *
5806       * @throws  LDAPException  If a problem is encountered while trying to
5807       *                         communicate with the directory server.
5808       */
5809      public List<String> getMissingEntryDNs(final Collection<String> dns)
5810             throws LDAPException
5811      {
5812        synchronized (entryMap)
5813        {
5814          final List<String> missingDNs = new ArrayList<String>(dns.size());
5815          for (final String dn : dns)
5816          {
5817            final Entry e = getEntry(dn);
5818            if (e == null)
5819            {
5820              missingDNs.add(dn);
5821            }
5822          }
5823    
5824          return missingDNs;
5825        }
5826      }
5827    
5828    
5829    
5830      /**
5831       * Ensures that all of the entries with the provided DNs exist in the
5832       * directory.
5833       *
5834       * @param  dns  The DNs of the entries for which to make the determination.
5835       *
5836       * @throws  LDAPException  If a problem is encountered while trying to
5837       *                         communicate with the directory server.
5838       *
5839       * @throws  AssertionError  If any of the target entries does not exist.
5840       */
5841      public void assertEntriesExist(final Collection<String> dns)
5842             throws LDAPException, AssertionError
5843      {
5844        synchronized (entryMap)
5845        {
5846          final List<String> missingDNs = getMissingEntryDNs(dns);
5847          if (missingDNs.isEmpty())
5848          {
5849            return;
5850          }
5851    
5852          final List<String> messages = new ArrayList<String>(missingDNs.size());
5853          for (final String dn : missingDNs)
5854          {
5855            messages.add(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
5856          }
5857    
5858          throw new AssertionError(StaticUtils.concatenateStrings(messages));
5859        }
5860      }
5861    
5862    
5863    
5864      /**
5865       * Retrieves a list containing all of the named attributes which do not exist
5866       * in the target entry.
5867       *
5868       * @param  dn              The DN of the entry to examine.
5869       * @param  attributeNames  The names of the attributes expected to be present
5870       *                         in the target entry.
5871       *
5872       * @return  A list containing the names of the attributes which were not
5873       *          present in the target entry, an empty list if all specified
5874       *          attributes were found in the entry, or {@code null} if the target
5875       *          entry does not exist.
5876       *
5877       * @throws  LDAPException  If a problem is encountered while trying to
5878       *                         communicate with the directory server.
5879       */
5880      public List<String> getMissingAttributeNames(final String dn,
5881                               final Collection<String> attributeNames)
5882             throws LDAPException
5883      {
5884        synchronized (entryMap)
5885        {
5886          final Entry e = getEntry(dn);
5887          if (e == null)
5888          {
5889            return null;
5890          }
5891    
5892          final Schema schema = schemaRef.get();
5893          final List<String> missingAttrs =
5894               new ArrayList<String>(attributeNames.size());
5895          for (final String attr : attributeNames)
5896          {
5897            final Filter f = Filter.createPresenceFilter(attr);
5898            if (! f.matchesEntry(e, schema))
5899            {
5900              missingAttrs.add(attr);
5901            }
5902          }
5903    
5904          return missingAttrs;
5905        }
5906      }
5907    
5908    
5909    
5910      /**
5911       * Ensures that the specified entry exists in the directory with all of the
5912       * specified attributes.
5913       *
5914       * @param  dn              The DN of the entry to examine.
5915       * @param  attributeNames  The names of the attributes that are expected to be
5916       *                         present in the provided entry.
5917       *
5918       * @throws  LDAPException  If a problem is encountered while trying to
5919       *                         communicate with the directory server.
5920       *
5921       * @throws  AssertionError  If the target entry does not exist or does not
5922       *                          contain all of the specified attributes.
5923       */
5924      public void assertAttributeExists(final String dn,
5925                                        final Collection<String> attributeNames)
5926            throws LDAPException, AssertionError
5927      {
5928        synchronized (entryMap)
5929        {
5930          final List<String> missingAttrs =
5931               getMissingAttributeNames(dn, attributeNames);
5932          if (missingAttrs == null)
5933          {
5934            throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
5935          }
5936          else if (missingAttrs.isEmpty())
5937          {
5938            return;
5939          }
5940    
5941          final List<String> messages = new ArrayList<String>(missingAttrs.size());
5942          for (final String attr : missingAttrs)
5943          {
5944            messages.add(ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(dn, attr));
5945          }
5946    
5947          throw new AssertionError(StaticUtils.concatenateStrings(messages));
5948        }
5949      }
5950    
5951    
5952    
5953      /**
5954       * Retrieves a list of all provided attribute values which are missing from
5955       * the specified entry.  The target attribute may or may not contain
5956       * additional values.
5957       *
5958       * @param  dn               The DN of the entry to examine.
5959       * @param  attributeName    The attribute expected to be present in the target
5960       *                          entry with the given values.
5961       * @param  attributeValues  The values expected to be present in the target
5962       *                          entry.
5963       *
5964       * @return  A list containing all of the provided values which were not found
5965       *          in the entry, an empty list if all provided attribute values were
5966       *          found, or {@code null} if the target entry does not exist.
5967       *
5968       * @throws  LDAPException  If a problem is encountered while trying to
5969       *                         communicate with the directory server.
5970       */
5971      public List<String> getMissingAttributeValues(final String dn,
5972                               final String attributeName,
5973                               final Collection<String> attributeValues)
5974           throws LDAPException
5975      {
5976        synchronized (entryMap)
5977        {
5978          final Entry e = getEntry(dn);
5979          if (e == null)
5980          {
5981            return null;
5982          }
5983    
5984          final Schema schema = schemaRef.get();
5985          final List<String> missingValues =
5986               new ArrayList<String>(attributeValues.size());
5987          for (final String value : attributeValues)
5988          {
5989            final Filter f = Filter.createEqualityFilter(attributeName, value);
5990            if (! f.matchesEntry(e, schema))
5991            {
5992              missingValues.add(value);
5993            }
5994          }
5995    
5996          return missingValues;
5997        }
5998      }
5999    
6000    
6001    
6002      /**
6003       * Ensures that the specified entry exists in the directory with all of the
6004       * specified values for the given attribute.  The attribute may or may not
6005       * contain additional values.
6006       *
6007       * @param  dn               The DN of the entry to examine.
6008       * @param  attributeName    The name of the attribute to examine.
6009       * @param  attributeValues  The set of values which must exist for the given
6010       *                          attribute.
6011       *
6012       * @throws  LDAPException  If a problem is encountered while trying to
6013       *                         communicate with the directory server.
6014       *
6015       * @throws  AssertionError  If the target entry does not exist, does not
6016       *                          contain the specified attribute, or that attribute
6017       *                          does not have all of the specified values.
6018       */
6019      public void assertValueExists(final String dn,
6020                                    final String attributeName,
6021                                    final Collection<String> attributeValues)
6022            throws LDAPException, AssertionError
6023      {
6024        synchronized (entryMap)
6025        {
6026          final List<String> missingValues =
6027               getMissingAttributeValues(dn, attributeName, attributeValues);
6028          if (missingValues == null)
6029          {
6030            throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6031          }
6032          else if (missingValues.isEmpty())
6033          {
6034            return;
6035          }
6036    
6037          // See if the attribute exists at all in the entry.
6038          final Entry e = getEntry(dn);
6039          final Filter f = Filter.createPresenceFilter(attributeName);
6040          if (! f.matchesEntry(e,  schemaRef.get()))
6041          {
6042            throw new AssertionError(
6043                 ERR_MEM_HANDLER_TEST_ATTR_MISSING.get(dn, attributeName));
6044          }
6045    
6046          final List<String> messages = new ArrayList<String>(missingValues.size());
6047          for (final String value : missingValues)
6048          {
6049            messages.add(ERR_MEM_HANDLER_TEST_VALUE_MISSING.get(dn, attributeName,
6050                 value));
6051          }
6052    
6053          throw new AssertionError(StaticUtils.concatenateStrings(messages));
6054        }
6055      }
6056    
6057    
6058    
6059      /**
6060       * Ensures that the specified entry does not exist in the directory.
6061       *
6062       * @param  dn  The DN of the entry expected to be missing.
6063       *
6064       * @throws  LDAPException  If a problem is encountered while trying to
6065       *                         communicate with the directory server.
6066       *
6067       * @throws  AssertionError  If the target entry is found in the server.
6068       */
6069      public void assertEntryMissing(final String dn)
6070             throws LDAPException, AssertionError
6071      {
6072        final Entry e = getEntry(dn);
6073        if (e != null)
6074        {
6075          throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_EXISTS.get(dn));
6076        }
6077      }
6078    
6079    
6080    
6081      /**
6082       * Ensures that the specified entry exists in the directory but does not
6083       * contain any of the specified attributes.
6084       *
6085       * @param  dn              The DN of the entry expected to be present.
6086       * @param  attributeNames  The names of the attributes expected to be missing
6087       *                         from the entry.
6088       *
6089       * @throws  LDAPException  If a problem is encountered while trying to
6090       *                         communicate with the directory server.
6091       *
6092       * @throws  AssertionError  If the target entry is missing from the server, or
6093       *                          if it contains any of the target attributes.
6094       */
6095      public void assertAttributeMissing(final String dn,
6096                                         final Collection<String> attributeNames)
6097             throws LDAPException, AssertionError
6098      {
6099        synchronized (entryMap)
6100        {
6101          final Entry e = getEntry(dn);
6102          if (e == null)
6103          {
6104            throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6105          }
6106    
6107          final Schema schema = schemaRef.get();
6108          final List<String> messages =
6109               new ArrayList<String>(attributeNames.size());
6110          for (final String name : attributeNames)
6111          {
6112            final Filter f = Filter.createPresenceFilter(name);
6113            if (f.matchesEntry(e, schema))
6114            {
6115              messages.add(ERR_MEM_HANDLER_TEST_ATTR_EXISTS.get(dn, name));
6116            }
6117          }
6118    
6119          if (! messages.isEmpty())
6120          {
6121            throw new AssertionError(StaticUtils.concatenateStrings(messages));
6122          }
6123        }
6124      }
6125    
6126    
6127    
6128      /**
6129       * Ensures that the specified entry exists in the directory but does not
6130       * contain any of the specified attribute values.
6131       *
6132       * @param  dn               The DN of the entry expected to be present.
6133       * @param  attributeName    The name of the attribute to examine.
6134       * @param  attributeValues  The values expected to be missing from the target
6135       *                          entry.
6136       *
6137       * @throws  LDAPException  If a problem is encountered while trying to
6138       *                         communicate with the directory server.
6139       *
6140       * @throws  AssertionError  If the target entry is missing from the server, or
6141       *                          if it contains any of the target attribute values.
6142       */
6143      public void assertValueMissing(final String dn,
6144                                     final String attributeName,
6145                                     final Collection<String> attributeValues)
6146             throws LDAPException, AssertionError
6147      {
6148        synchronized (entryMap)
6149        {
6150          final Entry e = getEntry(dn);
6151          if (e == null)
6152          {
6153            throw new AssertionError(ERR_MEM_HANDLER_TEST_ENTRY_MISSING.get(dn));
6154          }
6155    
6156          final Schema schema = schemaRef.get();
6157          final List<String> messages =
6158               new ArrayList<String>(attributeValues.size());
6159          for (final String value : attributeValues)
6160          {
6161            final Filter f = Filter.createEqualityFilter(attributeName, value);
6162            if (f.matchesEntry(e, schema))
6163            {
6164              messages.add(ERR_MEM_HANDLER_TEST_VALUE_EXISTS.get(dn, attributeName,
6165                   value));
6166            }
6167          }
6168    
6169          if (! messages.isEmpty())
6170          {
6171            throw new AssertionError(StaticUtils.concatenateStrings(messages));
6172          }
6173        }
6174      }
6175    }