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