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