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