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