001/*
002 * Copyright 2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2019 Ping Identity Corporation
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 */
021package com.unboundid.util;
022
023
024
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.Iterator;
029import java.util.List;
030import java.util.SortedMap;
031import java.util.SortedSet;
032import java.util.TreeMap;
033import java.util.TreeSet;
034import java.util.concurrent.atomic.AtomicLong;
035import java.util.concurrent.atomic.AtomicReference;
036
037import com.unboundid.asn1.ASN1OctetString;
038import com.unboundid.ldap.sdk.AbstractConnectionPool;
039import com.unboundid.ldap.sdk.Control;
040import com.unboundid.ldap.sdk.DeleteRequest;
041import com.unboundid.ldap.sdk.DereferencePolicy;
042import com.unboundid.ldap.sdk.DN;
043import com.unboundid.ldap.sdk.ExtendedRequest;
044import com.unboundid.ldap.sdk.ExtendedResult;
045import com.unboundid.ldap.sdk.Filter;
046import com.unboundid.ldap.sdk.LDAPConnection;
047import com.unboundid.ldap.sdk.LDAPException;
048import com.unboundid.ldap.sdk.LDAPInterface;
049import com.unboundid.ldap.sdk.LDAPResult;
050import com.unboundid.ldap.sdk.LDAPSearchException;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.ldap.sdk.RootDSE;
053import com.unboundid.ldap.sdk.SearchRequest;
054import com.unboundid.ldap.sdk.SearchResult;
055import com.unboundid.ldap.sdk.SearchScope;
056import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
057import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
058import com.unboundid.ldap.sdk.controls.SubentriesRequestControl;
059import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedRequest;
060import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedResult;
061import com.unboundid.ldap.sdk.unboundidds.controls.HardDeleteRequestControl;
062import com.unboundid.ldap.sdk.unboundidds.controls.
063            PermitUnindexedSearchRequestControl;
064import com.unboundid.ldap.sdk.unboundidds.controls.
065            ReturnConflictEntriesRequestControl;
066import com.unboundid.ldap.sdk.unboundidds.controls.
067            SoftDeletedEntryAccessRequestControl;
068import com.unboundid.ldap.sdk.unboundidds.extensions.
069            SetSubtreeAccessibilityExtendedRequest;
070
071import static com.unboundid.util.UtilityMessages.*;
072
073
074
075/**
076 * This class provides a utility that can delete all entries below a specified
077 * base DN (including the base entry itself by default, although it can be
078 * preserved if desired) in an LDAP directory server.  It accomplishes this
079 * through a combination of search and delete operations.  Ideally, it will
080 * first perform a search to find all entries below the target base DN, but in
081 * some cases, it may be necessary to intertwine search and delete operations
082 * if it is not possible to retrieve all entries in the target subtree in
083 * advance.
084 * <BR><BR>
085 * The subtree deleter can optionally take advantage of a number of server
086 * features to aid in processing, but does not require them.  Some of these
087 * features include:
088 * <UL>
089 *   <LI>
090 *     Set Subtree Accessibility Extended Operation -- A proprietary extended
091 *     operation supported by the Ping Identity, UnboundID, and
092 *     Nokia/Alcatel-Lucent 8661 Directory Server products.  This operation can
093 *     restrict access to a specified subtree to all but a specified user.  If
094 *     this is to be used, then the "Who Am I?" extended operation will first be
095 *     used to identify the user that is authenticated on the provided
096 *     connection, and then the set subtree accessibility extended operation
097 *     will be used to make the target subtree hidden and read-only for all
098 *     users except the user identified by the "Who Am I?" operation.  As far as
099 *     all other clients are concerned, this will make the target subtree
100 *     immediately disappear.  The subtree deleter will then be able to search
101 *     for the entries to delete, and then delete those entries, without
102 *     exposing other clients to its in-progress state.
103 *     <BR><BR>
104 *     The set subtree accessibility extended operation will not automatically
105 *     be used.  If the
106 *     {@link #setUseSetSubtreeAccessibilityOperationIfAvailable} method is
107 *     called with a value of {@code true}, then this extended operation will be
108 *     used if the server root DSE advertises support for both this operation
109 *     and the LDAP "Who Am I?" extended operation.
110 *     <BR><BR>
111 *   </LI>
112 *   <LI>
113 *     Simple Paged Results Request Control -- A standard request control that
114 *     is supported by several types of directory servers.  This control allows
115 *     a search to be broken up into pages to limit the number of entries that
116 *     are returned in any single operation (which can help an authorized
117 *     client circumvent search size limit restrictions).  It can also help
118 *     ensure that if the server can return entries faster than the client can
119 *     consume them, it will not result in a large backlog on the server.
120 *     <BR><BR>
121 *     The simple paged results request control will be used by default if the
122 *     server root DSE advertises support for it, with a default page size of
123 *     100 entries.
124 *     <BR><BR>
125 *   </LI>
126 *   <LI>
127 *     Manage DSA IT Request Control -- A standard request control that is
128 *     supported by several types of directory servers.  This control indicates
129 *     that any referral entries (that is, entries that contain the "referral"
130 *     object class and a "ref" attribute) should be treated as regular entries
131 *     rather than triggering a referral result or a search result reference.
132 *     The subtree deleter will not make any attempt to follow referrals, and
133 *     if any referral or search result reference results are returned during
134 *     processing, then it may not be possible to completely remove all entries
135 *     in the target subtree.
136 *     <BR><BR>
137 *     The manage DSA IT request control will be used by default if the server
138 *     root DSE advertises support for it.
139 *     <BR><BR>
140 *   </LI>
141 *   <LI>
142 *     Permit Unindexed Search Request Control -- A proprietary request
143 *     control supported by the Ping Identity, UnboundID, and
144 *     Nokia/Alcatel-Lucent 8661 Directory Server products.  This control
145 *     indicates that the client wishes to process the search even if it is
146 *     unindexed.
147 *     <BR><BR>
148 *     The permit unindexed search request control will not automatically be
149 *     used.  It may not needed if the requester has the unindexed-search
150 *     privilege, and the permit unindexed search request control requires that
151 *     the caller have either the unindexed-search or
152 *     unindexed-search-with-control privilege.  If the
153 *     {@link #setUsePermitUnindexedSearchControlIfAvailable} method is called
154 *     with a value of {@code true}, then this control will be used if the
155 *     server root DSE advertises support for it.
156 *     <BR><BR>
157 *   </LI>
158 *   <LI>
159 *     LDAP Subentries Request Control -- A standard request control that is
160 *     supported by several types of directory servers.  It allows the client
161 *     to request a search that retrieves entries with the "ldapSubentry"
162 *     object class, which are normally excluded from search results.  Note that
163 *     because of the nature of this control, if it is to be used, then two
164 *     separate sets of searches will be used:  one that retrieves only
165 *     LDAP subentries, and a second that retrieves other types of entries.
166 *     <BR><BR>
167 *     The LDAP subentries request control will be used by default if the server
168 *     root DSE advertises support for it.
169 *     <BR><BR>
170 *   </LI>
171 *   <LI>
172 *     Return Conflict Entries Request Control -- A proprietary request control
173 *     that is supported by the Ping Identity, UnboundID, and
174 *     Nokia/Alcatel-Lucent 8661 Directory Server products.  This control
175 *     indicates that the server should return replication conflict entries,
176 *     which are normally excluded from search results.
177 *     <BR><BR>
178 *     The return conflict entries request control will be used by default if
179 *     the server root DSE advertises support for it.
180 *     <BR><BR>
181 *   </LI>
182 *   <LI>
183 *     Soft-Deleted Entry Access Request Control -- A proprietary request
184 *     control that is supported by the Ping Identity, UnboundID, and
185 *     Nokia/Alcatel-Lucent 8661 Directory Server products.  This control
186 *     indicates that the server should return soft-deleted entries, which are
187 *     normally excluded from search results.
188 *     <BR><BR>
189 *     The soft-deleted entry access request control will be used by default if
190 *     the server root DSE advertises support for it.
191 *     <BR><BR>
192 *   <LI>
193 *     Hard Delete Request Control -- A proprietary request control that is
194 *     supported by the Ping Identity, UnboundID, and Nokia/Alcatel-Lucent 8661
195 *     Directory Server products.  This control indicates that the server
196 *     should process a delete operation as a hard delete, even if a
197 *     soft-delete policy would have otherwise converted it into a soft delete.
198 *     A subtree cannot be deleted if it contains soft-deleted entries, so this
199 *     should be used if the server is configured with such a soft-delete
200 *     policy.
201 *     <BR><BR>
202 *     The hard delete request control will be used by default if the server
203 *     root DSE advertises support for it.
204 *     <BR><BR>
205 *   </LI>
206 * </UL>
207 */
208@Mutable()
209@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
210public final class SubtreeDeleter
211{
212  // Indicates whether to delete the base entry itself, or only its
213  // subordinates.
214  private boolean deleteBaseEntry = true;
215
216  // Indicates whether to include the hard delete request control in delete
217  // requests, if the server root DSE advertises support for it.
218  private boolean useHardDeleteControlIfAvailable = true;
219
220  // Indicates whether to include the manage DSA IT request control in search
221  // and delete requests, if the server root DSE advertises support for it.
222  private boolean useManageDSAITControlIfAvailable = true;
223
224  // Indicates whether to include the permit unindexed search request control in
225  // search requests, if the server root DSE advertises support for it.
226  private boolean usePermitUnindexedSearchControlIfAvailable = false;
227
228  // Indicates whether to include the return conflict entries request control
229  // in search requests, if the server root DSE advertises support for it.
230  private boolean useReturnConflictEntriesRequestControlIfAvailable = true;
231
232  // Indicates whether to use the simple paged results control in the course of
233  // finding the entries to delete, if the server root DSE advertises support
234  // for it.
235  private boolean useSimplePagedResultsControlIfAvailable = true;
236
237  // Indicates whether to include the soft-deleted entry access request control
238  // in search requests, if the server root DSE advertises support for it.
239  private boolean useSoftDeletedEntryAccessControlIfAvailable = true;
240
241  // Indicates whether to use the subentries request control to search for LDAP
242  // subentries if the server root DSE advertises support for it.
243  private boolean useSubentriesControlIfAvailable = true;
244
245  // Indicates whether to use the set subtree accessibility extended operation
246  // to made the target subtree inaccessible, if the server root DSE advertises
247  // support for it.
248  private boolean useSetSubtreeAccessibilityOperationIfAvailable = false;
249
250  // The maximum number of entries to return from any single search operation.
251  private int searchRequestSizeLimit = 0;
252
253  // The page size to use in conjunction with the simple paged results request
254  // control.
255  private int simplePagedResultsPageSize = 100;
256
257  // The fixed-rate barrier that will be used to limit the rate at which delete
258  // operations will be attempted.
259  private FixedRateBarrier deleteRateLimiter = null;
260
261  // A list of additional controls that should be included in search requests
262  // used to find the entries to delete.
263  private List<Control> additionalSearchControls = Collections.emptyList();
264
265  // A list of additional controls that should be included in delete requests
266  // used to
267  private List<Control> additionalDeleteControls = Collections.emptyList();
268
269
270
271  /**
272   * Creates a new instance of this subtree deleter with the default settings.
273   */
274  public SubtreeDeleter()
275  {
276    // No implementation is required.
277  }
278
279
280
281  /**
282   * Indicates whether the base entry itself should be deleted along with all of
283   * its subordinates.  This method returns {@code true} by default.
284   *
285   * @return  {@code true} if the base entry should be deleted in addition to
286   *          its subordinates, or {@code false} if the base entry should not
287   *          be deleted but all of its subordinates should be.
288   */
289  public boolean deleteBaseEntry()
290  {
291    return deleteBaseEntry;
292  }
293
294
295
296  /**
297   * Specifies whether the base entry itself should be deleted along with all of
298   * its subordinates.
299   *
300   * @param  deleteBaseEntry
301   *              {@code true} to indicate that the base entry should be deleted
302   *              in addition to its subordinates, or {@code false} if only the
303   *              subordinates of the base entry should be removed.
304   */
305  public void setDeleteBaseEntry(final boolean deleteBaseEntry)
306  {
307    this.deleteBaseEntry = deleteBaseEntry;
308  }
309
310
311
312  /**
313   * Indicates whether to use the {@link SetSubtreeAccessibilityExtendedRequest}
314   * to make the target subtree hidden before starting to search for entries to
315   * delete if the server root DSE advertises support for both that extended
316   * request and the "Who Am I?" extended request.  In servers that support it,
317   * this extended operation can make the target subtree hidden and read-only to
318   * clients other than those authenticated as the user that issued the set
319   * subtree accessibility request.
320   * <BR><BR>
321   * This method returns {@code true} by default.  Its value will be ignored if
322   * the server root DSE does not indicate that it supports both the set subtree
323   * accessibility extended operation and the "Who Am I?" extended operation.
324   *
325   * @return  {@code true} if the set subtree accessibility extended operation
326   *          should be used to make the target subtree hidden and read-only
327   *          before attempting to search for entries to delete if the server
328   *          root DSE advertises support for it, or {@code false} if the
329   *          operation should not be used.
330   */
331  public boolean useSetSubtreeAccessibilityOperationIfAvailable()
332  {
333    return useSetSubtreeAccessibilityOperationIfAvailable;
334  }
335
336
337
338  /**
339   * Specifies whether to use the {@link SetSubtreeAccessibilityExtendedRequest}
340   * to make the target subtree hidden before starting to search for entries to
341   * delete if the server root DSE advertises support for both that extended
342   * request and the "Who Am I?" extended request.  In servers that support it,
343   * this extended operation can make the target subtree hidden and read-only to
344   * clients other than those authenticated as the user that issued the set
345   * subtree accessibility request.
346   *
347   * @param  useSetSubtreeAccessibilityOperationIfAvailable
348   *              {@code true} to indicate that the set subtree accessibility
349   *              extended operation should be used to make the target subtree
350   *              hidden and read-only before starting to search for entries
351   *              to delete, or {@code false} if not.  This value will be
352   *              ignored if the server root DSE does not advertise support for
353   *              both the set subtree accessibility extended operation and the
354   *              "Who Am I?" extended operation.
355   */
356  public void setUseSetSubtreeAccessibilityOperationIfAvailable(
357                   final boolean useSetSubtreeAccessibilityOperationIfAvailable)
358  {
359    this.useSetSubtreeAccessibilityOperationIfAvailable =
360         useSetSubtreeAccessibilityOperationIfAvailable;
361  }
362
363
364
365  /**
366   * Indicates whether to use the {@link SimplePagedResultsControl} when
367   * searching for entries to delete if the server advertises support for it.
368   * Using this control can help avoid problems from running into the search
369   * size limit, and can also prevent the server from trying to return entries
370   * faster than the client can consume them.
371   * <BR><BR>
372   * This method returns {@code true} by default.  Its value will be ignored if
373   * the server root DSE does not indicate that it supports the simple paged
374   * results control.
375   *
376   * @return   {@code true} if the simple paged results control should be used
377   *           when searching for entries to delete if the server root DSE
378   *           advertises support for it, or {@code false} if the control should
379   *           not be used.
380   */
381  public boolean useSimplePagedResultsControlIfAvailable()
382  {
383    return useSimplePagedResultsControlIfAvailable;
384  }
385
386
387
388  /**
389   * Specifies whether to use the {@link SimplePagedResultsControl} when
390   * searching for entries to delete if the server advertises support for it.
391   * Using this control can help avoid problems from running into the search
392   * size limit, and can also prevent the server from trying to return entries
393   * faster than the client can consume them.
394   *
395   * @param  useSimplePagedResultsControlIfAvailable
396   *              {@code true} to indicate that the simple paged results control
397   *              should be used when searching for entries to delete, or
398   *              {@code false} if not.  This value will be ignored if the
399   *              server root DSE does not advertise support for the simple
400   *              paged results control.
401   */
402  public void setUseSimplePagedResultsControlIfAvailable(
403                   final boolean useSimplePagedResultsControlIfAvailable)
404  {
405    this.useSimplePagedResultsControlIfAvailable =
406         useSimplePagedResultsControlIfAvailable;
407  }
408
409
410
411  /**
412   * Retrieves the maximum number of entries that should be returned in each
413   * page of results when using the simple paged results control.  This value
414   * will only be used if {@link #useSimplePagedResultsControlIfAvailable()}
415   * returns {@code true} and the server root DSE indicates that it supports the
416   * simple paged results control.
417   * <BR><BR>
418   * This method returns {@code 100} by default.  Its value will be ignored if
419   * the server root DSE does not indicate that it supports the simple paged
420   * results control.
421   *
422   * @return  The maximum number of entries that should be returned in each page
423   *          of results when using the simple paged results control.
424   */
425  public int getSimplePagedResultsPageSize()
426  {
427    return simplePagedResultsPageSize;
428  }
429
430
431
432  /**
433   * Specifies the maximum number of entries that should be returned in each
434   * page of results when using the simple paged results control. This value
435   * will only be used if {@link #useSimplePagedResultsControlIfAvailable()}
436   * returns {@code true} and the server root DSE indicates that it supports the
437   * simple paged results control.
438   *
439   * @param  simplePagedResultsPageSize
440   *              The maximum number of entries that should be returned in each
441   *              page of results when using the simple paged results control.
442   *              The value must be greater than or equal to one.
443   */
444  public void setSimplePagedResultsPageSize(
445                   final int simplePagedResultsPageSize)
446  {
447    Validator.ensureTrue((simplePagedResultsPageSize >= 1),
448         "SubtreeDeleter.simplePagedResultsPageSize must be greater than " +
449              "or equal to 1.");
450    this.simplePagedResultsPageSize = simplePagedResultsPageSize;
451  }
452
453
454
455  /**
456   * Indicates whether to include the {@link ManageDsaITRequestControl} in
457   * search and delete requests if the server root DSE advertises support for
458   * it.  The manage DSA IT request control tells the server that it should
459   * return referral entries as regular entries rather than returning them as
460   * search result references when processing a search operation, or returning a
461   * referral result when attempting a delete.  If any referrals are
462   * encountered during processing and this control is not used, then it may
463   * not be possible to completely delete the entire subtree.
464   * <BR><BR>
465   * This method returns {@code true} by default.  Its value will be ignored if
466   * the server root DSE does not indicate that it supports the manage DSA IT
467   * request control.
468   *
469   * @return  {@code true} if the manage DSA IT request control should be
470   *          included in search and delete requests if the server root DSE
471   *          advertises support for it, or {@code false} if not.
472   */
473  public boolean useManageDSAITControlIfAvailable()
474  {
475    return useManageDSAITControlIfAvailable;
476  }
477
478
479
480  /**
481   * Specifies whether to include the {@link ManageDsaITRequestControl} in
482   * search and delete requests if the server root DSE advertises support for
483   * it.  The manage DSA IT request control tells the server that it should
484   * return referral entries as regular entries rather than returning them as
485   * search result references when processing a search operation, or returning a
486   * referral result when attempting a delete.  If any referrals are
487   * encountered during processing and this control is not used, then it may
488   * not be possible to completely delete the entire subtree.
489   *
490   * @param  useManageDSAITControlIfAvailable
491   *              {@code true} to indicate that the manage DSA IT request
492   *              control should be included in search and delete requests,
493   *              or {@code false} if not.  This value will be ignored if the
494   *              server root DSE does not advertise support for the manage DSA
495   *              IT request control.
496   */
497  public void setUseManageDSAITControlIfAvailable(
498                   final boolean useManageDSAITControlIfAvailable)
499  {
500    this.useManageDSAITControlIfAvailable = useManageDSAITControlIfAvailable;
501  }
502
503
504
505  /**
506   * Indicates whether to include the
507   * {@link PermitUnindexedSearchRequestControl} in search requests used to
508   * identify the entries to be deleted if the server root DSE advertises
509   * support for it.  The permit unindexed search request control may allow
510   * appropriately authorized clients to explicitly indicate that the server
511   * should process an unindexed search that would normally be rejected.
512   * <BR><BR>
513   * This method returns {@code true} by default.  Its value will be ignored if
514   * the server root DSE does not indicate that it supports the permit unindexed
515   * search request control.
516   *
517   * @return  {@code true} if search requests should include the permit
518   *          unindexed search request control if the server root DSE advertises
519   *          support for it, or {@code false} if not.
520   */
521  public boolean usePermitUnindexedSearchControlIfAvailable()
522  {
523    return usePermitUnindexedSearchControlIfAvailable;
524  }
525
526
527
528  /**
529   * Specifies whether to include the
530   * {@link PermitUnindexedSearchRequestControl} in search request used to
531   * identify the entries to be deleted if the server root DSE advertises
532   * support for it.  The permit unindexed search request control may allow
533   * appropriately authorized clients to explicitly indicate that the server
534   * should process an unindexed search that would normally be rejected.
535   *
536   * @param  usePermitUnindexedSearchControlIfAvailable
537   *              {@code true} to indicate that the permit unindexed search
538   *              request control should be included in search requests, or
539   *              {@code false} if not.  This value will be ignored if the
540   *              server root DSE does not advertise support for the permit
541   *              unindexed search request control.
542   */
543  public void setUsePermitUnindexedSearchControlIfAvailable(
544                   final boolean usePermitUnindexedSearchControlIfAvailable)
545  {
546    this.usePermitUnindexedSearchControlIfAvailable =
547         usePermitUnindexedSearchControlIfAvailable;
548  }
549
550
551
552  /**
553   * Indicates whether to use the {@link SubentriesRequestControl} when
554   * searching for entries to delete if the server root DSE advertises support
555   * for it.  The subentries request control allows LDAP subentries to be
556   * included in search results.  These entries are normally excluded from
557   * search results.
558   * <BR><BR>
559   * This method returns {@code true} by default.  Its value will be ignored if
560   * the server root DSE does not indicate that it supports the subentries
561   * request control.
562   *
563   * @return  {@code true} if the subentries request control should be used
564   *          to retrieve LDAP subentries if the server root DSE advertises
565   *          support for it, or {@code false} if not.
566   */
567  public boolean useSubentriesControlIfAvailable()
568  {
569    return useSubentriesControlIfAvailable;
570  }
571
572
573
574  /**
575   * Specifies whether to use the {@link SubentriesRequestControl} when
576   * searching for entries to delete if the server root DSE advertises support
577   * for it.  The subentries request control allows LDAP subentries to be
578   * included in search results.  These entries are normally excluded from
579   * search results.
580   *
581   * @param  useSubentriesControlIfAvailable
582   *              [@code true} to indicate that the subentries request control
583   *              should be used to retrieve LDAP subentries, or {@code false}
584   *              if not.  This value will be ignored if the server root DSE
585   *              does not advertise support for the subentries request
586   *              control.
587   */
588  public void setUseSubentriesControlIfAvailable(
589                   final boolean useSubentriesControlIfAvailable)
590  {
591    this.useSubentriesControlIfAvailable = useSubentriesControlIfAvailable;
592  }
593
594
595
596  /**
597   * Indicates whether to use the {@link ReturnConflictEntriesRequestControl}
598   * when searching for entries to delete if the server root DSE advertises
599   * support for it.  The return conflict entries request control allows
600   * replication conflict entries to be included in search results.  These
601   * entries are normally excluded from search results.
602   * <BR><BR>
603   * This method returns {@code true} by default.  Its value will be ignored if
604   * the server root DSE does not indicate that it supports the return
605   * conflict entries request control.
606   *
607   * @return  {@code true} if the return conflict entries request control
608   *          should be used to retrieve replication conflict entries if the
609   *          server root DSE advertises support for it, or {@code false} if
610   *          not.
611   */
612  public boolean useReturnConflictEntriesRequestControlIfAvailable()
613  {
614    return useReturnConflictEntriesRequestControlIfAvailable;
615  }
616
617
618
619  /**
620   * Specifies whether to use the {@link ReturnConflictEntriesRequestControl}
621   * when searching for entries to delete if the server root DSE advertises
622   * support for it.  The return conflict entries request control allows
623   * replication conflict entries to be included in search results.  These
624   * entries are normally excluded from search results.
625   *
626   * @param  useReturnConflictEntriesRequestControlIfAvailable
627   *              {@code true} to indicate that the return conflict entries
628   *              request control should be used to retrieve replication
629   *              conflict entries, or {@code false} if not.  This value will be
630   *              ignored if the server root DSE does not advertise support for
631   *              the return conflict entries request control.
632   */
633  public void setUseReturnConflictEntriesRequestControlIfAvailable(
634       final boolean useReturnConflictEntriesRequestControlIfAvailable)
635  {
636    this.useReturnConflictEntriesRequestControlIfAvailable =
637         useReturnConflictEntriesRequestControlIfAvailable;
638  }
639
640
641
642  /**
643   * Indicates whether to use the {@link SoftDeletedEntryAccessRequestControl}
644   * when searching for entries to delete if the server root DSE advertises
645   * support for it.  The soft-deleted entry access request control allows
646   * soft-deleted entries to be included in search results.  These entries are
647   * normally excluded from search results.
648   * <BR><BR>
649   * This method returns {@code true} by default.  Its value will be ignored if
650   * the server root DSE does not indicate that it supports the soft-deleted
651   * entry access request control.
652   *
653   * @return  {@code true} if the soft-deleted entry access request control
654   *          should be used to retrieve soft-deleted entries if the server
655   *          root DSE advertises support for it, or {@code false} if not.
656   */
657  public boolean useSoftDeletedEntryAccessControlIfAvailable()
658  {
659    return useSoftDeletedEntryAccessControlIfAvailable;
660  }
661
662
663
664  /**
665   * Specifies whether to use the {@link SoftDeletedEntryAccessRequestControl}
666   * when searching for entries to delete if the server root DSE advertises
667   * support for it.  The soft-deleted entry access request control allows
668   * soft-deleted entries to be included in search results.  These entries are
669   * normally excluded from search results.
670   *
671   * @param  useSoftDeletedEntryAccessControlIfAvailable
672   *              {@code true} to indicate that the soft-deleted entry access
673   *              request control should be used to retrieve soft-deleted
674   *              entries, or {@code false} if not.  This value will be ignored
675   *              if the server root DSE does not advertise support for the
676   *              soft-deleted entry access request control.
677   */
678  public void setUseSoftDeletedEntryAccessControlIfAvailable(
679                   final boolean useSoftDeletedEntryAccessControlIfAvailable)
680  {
681    this.useSoftDeletedEntryAccessControlIfAvailable =
682         useSoftDeletedEntryAccessControlIfAvailable;
683  }
684
685
686
687  /**
688   * Indicates whether to include the {@link HardDeleteRequestControl} in
689   * delete requests if the server root DSE advertises support for it.  The
690   * hard delete request control indicates that the server should treat a delete
691   * operation as a hard delete even if it would have normally been processed as
692   * a soft delete because it matches the criteria in a configured soft delete
693   * policy.
694   * <BR><BR>
695   * This method returns {@code true} by default.  Its value will be ignored if
696   * the server root DSE does not indicate that it supports the hard delete
697   * request control.
698   *
699   * @return  {@code true} if the hard delete request control should be included
700   *          in delete requests if the server root DSE advertises support for
701   *          it, or {@code false} if not.
702   */
703  public boolean useHardDeleteControlIfAvailable()
704  {
705    return useHardDeleteControlIfAvailable;
706  }
707
708
709
710  /**
711   * Specifies whether to include the {@link HardDeleteRequestControl} in
712   * delete requests if the server root DSE advertises support for it.  The
713   * hard delete request control indicates that the server should treat a delete
714   * operation as a hard delete even if it would have normally been processed as
715   * a soft delete because it matches the criteria in a configured soft delete
716   * policy.
717   *
718   * @param  useHardDeleteControlIfAvailable
719   *              {@code true} to indicate that the hard delete request control
720   *              should be included in delete requests, or {@code false} if
721   *              not.  This value will be ignored if the server root DSE does
722   *              not advertise support for the hard delete request control.
723   */
724  public void setUseHardDeleteControlIfAvailable(
725                   final boolean useHardDeleteControlIfAvailable)
726  {
727    this.useHardDeleteControlIfAvailable = useHardDeleteControlIfAvailable;
728  }
729
730
731
732  /**
733   * Retrieves an unmodifiable list of additional controls that should be
734   * included in search requests used to identify entries to delete.
735   * <BR><BR>
736   * This method returns an empty list by default.
737   *
738   * @return  An unmodifiable list of additional controls that should be
739   *          included in search requests used to identify entries to delete.
740   */
741  public List<Control> getAdditionalSearchControls()
742  {
743    return additionalSearchControls;
744  }
745
746
747
748  /**
749   * Specifies a list of additional controls that should be included in search
750   * requests used to identify entries to delete.
751   *
752   * @param  additionalSearchControls
753   *              A list of additional controls that should be included in
754   *              search requests used to identify entries to delete.  This must
755   *              not be {@code null} but may be empty.
756   */
757  public void setAdditionalSearchControls(
758                   final Control... additionalSearchControls)
759  {
760    setAdditionalSearchControls(Arrays.asList(additionalSearchControls));
761  }
762
763
764
765  /**
766   * Specifies a list of additional controls that should be included in search
767   * requests used to identify entries to delete.
768   *
769   * @param  additionalSearchControls
770   *              A list of additional controls that should be included in
771   *              search requests used to identify entries to delete.  This must
772   *              not be {@code null} but may be empty.
773   */
774  public void setAdditionalSearchControls(
775                   final List<Control> additionalSearchControls)
776  {
777    this.additionalSearchControls = Collections.unmodifiableList(
778         new ArrayList<>(additionalSearchControls));
779  }
780
781
782
783  /**
784   * Retrieves an unmodifiable list of additional controls that should be
785   * included in delete requests.
786   * <BR><BR>
787   * This method returns an empty list by default.
788   *
789   * @return  An unmodifiable list of additional controls that should be
790   *          included in delete requests.
791   */
792  public List<Control> getAdditionalDeleteControls()
793  {
794    return additionalDeleteControls;
795  }
796
797
798
799  /**
800   * Specifies a list of additional controls that should be included in delete
801   * requests.
802   *
803   * @param  additionalDeleteControls
804   *              A list of additional controls that should be included in
805   *              delete requests.  This must not be {@code null} but may be
806   *              empty.
807   */
808  public void setAdditionalDeleteControls(
809                   final Control... additionalDeleteControls)
810  {
811    setAdditionalDeleteControls(Arrays.asList(additionalDeleteControls));
812  }
813
814
815
816  /**
817   * Specifies a list of additional controls that should be included in delete
818   * requests.
819   *
820   * @param  additionalDeleteControls
821   *              A list of additional controls that should be included in
822   *              delete requests.  This must not be {@code null} but may be
823   *              empty.
824   */
825  public void setAdditionalDeleteControls(
826                   final List<Control> additionalDeleteControls)
827  {
828    this.additionalDeleteControls = Collections.unmodifiableList(
829         new ArrayList<>(additionalDeleteControls));
830  }
831
832
833
834  /**
835   * Retrieves the size limit that should be used in each search request to
836   * specify the maximum number of entries to return in response to that
837   * request.  If a search request matches more than this number of entries,
838   * then the server may return a subset of the results and a search result
839   * done message with a result code of {@link ResultCode#SIZE_LIMIT_EXCEEDED}.
840   * <BR><BR>
841   * This method returns a value of zero by default, which indicates that the
842   * client does not want to impose any limit on the number of entries that may
843   * be returned in response to any single search operation (although the server
844   * may still impose a limit).
845   *
846   * @return  The size limit that should be used in each search request to
847   *          specify the maximum number of entries to return in response to
848   *          that request, or zero to indicate that the client does not want to
849   *          impose any size limit.
850   */
851  public int getSearchRequestSizeLimit()
852  {
853    return searchRequestSizeLimit;
854  }
855
856
857
858  /**
859   * Specifies the size limit that should be used in each search request to
860   * specify the maximum number of entries to return in response to that
861   * request.  If a search request matches more than this number of entries,
862   * then the server may return a subset of the results and a search result
863   * done message with a result code of {@link ResultCode#SIZE_LIMIT_EXCEEDED}.
864   * A value that is less than or equal to zero indicates that the client does
865   * not want to impose any limit on the number of entries that may be returned
866   * in response to any single search operation (although the server may still
867   * impose a limit).
868   *
869   * @param  searchRequestSizeLimit
870   *              The size limit that should be used in each search request to
871   *              specify the maximum number of entries to return in response
872   *              to that request.  A value that is less than or equal to zero
873   *              indicates that the client does not want to impose any size
874   *              limit.
875   */
876  public void setSearchRequestSizeLimit(final int searchRequestSizeLimit)
877  {
878    if (searchRequestSizeLimit <= 0)
879    {
880      this.searchRequestSizeLimit = 0;
881    }
882    else
883    {
884      this.searchRequestSizeLimit = searchRequestSizeLimit;
885    }
886  }
887
888
889
890  /**
891   * Retrieves the fixed-rate barrier that may be used to impose a rate limit on
892   * delete operations, if defined.
893   * <BR><BR>
894   * This method returns {@code null} by default, to indicate that no delete
895   * rate limit will be imposed.
896   *
897   * @return  The fixed-rate barrier that may be used to impose a rate limit on
898   *          delete operations, or {@code null} if no rate limit should be
899   *          imposed.
900   */
901  public FixedRateBarrier getDeleteRateLimiter()
902  {
903    return deleteRateLimiter;
904  }
905
906
907
908  /**
909   * Provides a fixed-rate barrier that may be used to impose a rate limit on
910   * delete operations.
911   *
912   * @param  deleteRateLimiter
913   *              A fixed-rate barrier that may be used to impose a rate limit
914   *              on delete operations.  It may be {@code null} if no delete
915   *              rate limit should be imposed.
916   */
917  public void setDeleteRateLimiter(final FixedRateBarrier deleteRateLimiter)
918  {
919    this.deleteRateLimiter = deleteRateLimiter;
920  }
921
922
923
924  /**
925   * Attempts to delete the specified subtree using the current settings.
926   *
927   * @param  connection
928   *              The {@link LDAPInterface} instance to use to communicate with
929   *              the directory server.  While this may be an individual
930   *              {@link LDAPConnection}, it may be better as a connection
931   *              pool with automatic retry enabled so that it's more likely to
932   *              succeed in the event that a connection becomes invalid or an
933   *              operation experiences a transient failure.  It must not be
934   *              {@code null}.
935   * @param  baseDN
936   *              The base DN for the subtree to delete.  It must not be
937   *              {@code null}.
938   *
939   * @return  An object with information about the results of the subtree
940   *          delete processing.
941   *
942   * @throws  LDAPException  If the provided base DN cannot be parsed as a valid
943   *                         DN.
944   */
945  public SubtreeDeleterResult delete(final LDAPInterface connection,
946                                     final String baseDN)
947         throws LDAPException
948  {
949    return delete(connection, new DN(baseDN));
950  }
951
952
953
954  /**
955   * Attempts to delete the specified subtree using the current settings.
956   *
957   * @param  connection
958   *              The {@link LDAPInterface} instance to use to communicate with
959   *              the directory server.  While this may be an individual
960   *              {@link LDAPConnection}, it may be better as a connection
961   *              pool with automatic retry enabled so that it's more likely to
962   *              succeed in the event that a connection becomes invalid or an
963   *              operation experiences a transient failure.  It must not be
964   *              {@code null}.
965   * @param  baseDN
966   *              The base DN for the subtree to delete.  It must not be
967   *              {@code null}.
968   *
969   * @return  An object with information about the results of the subtree
970   *          delete processing.
971   */
972  public SubtreeDeleterResult delete(final LDAPInterface connection,
973                                     final DN baseDN)
974  {
975    final AtomicReference<RootDSE> rootDSE = new AtomicReference<>();
976    final boolean useSetSubtreeAccessibility =
977         useSetSubtreeAccessibilityOperationIfAvailable &&
978              supportsExtendedRequest(connection, rootDSE,
979                   SetSubtreeAccessibilityExtendedRequest.
980                        SET_SUBTREE_ACCESSIBILITY_REQUEST_OID) &&
981              supportsExtendedRequest(connection, rootDSE,
982                   WhoAmIExtendedRequest.WHO_AM_I_REQUEST_OID);
983
984    final boolean usePagedResults = useSimplePagedResultsControlIfAvailable &&
985         supportsControl(connection, rootDSE,
986              SimplePagedResultsControl.PAGED_RESULTS_OID);
987
988    final boolean useSubentries = useSubentriesControlIfAvailable &&
989         supportsControl(connection, rootDSE,
990              SubentriesRequestControl.SUBENTRIES_REQUEST_OID);
991
992    final List<Control> searchControls = new ArrayList<>(10);
993    searchControls.addAll(additionalSearchControls);
994
995    final List<Control> deleteControls = new ArrayList<>(10);
996    deleteControls.addAll(additionalDeleteControls);
997
998    if (useHardDeleteControlIfAvailable &&
999       supportsControl(connection, rootDSE,
1000            HardDeleteRequestControl.HARD_DELETE_REQUEST_OID))
1001    {
1002      deleteControls.add(new HardDeleteRequestControl(false));
1003    }
1004
1005    if (useManageDSAITControlIfAvailable &&
1006       supportsControl(connection, rootDSE,
1007            ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID))
1008    {
1009      final ManageDsaITRequestControl c =
1010           new ManageDsaITRequestControl(false);
1011      searchControls.add(c);
1012      deleteControls.add(c);
1013    }
1014
1015    if (usePermitUnindexedSearchControlIfAvailable &&
1016       supportsControl(connection, rootDSE,
1017            PermitUnindexedSearchRequestControl.
1018                 PERMIT_UNINDEXED_SEARCH_REQUEST_OID))
1019    {
1020      searchControls.add(new PermitUnindexedSearchRequestControl(false));
1021    }
1022
1023    if (useReturnConflictEntriesRequestControlIfAvailable &&
1024       supportsControl(connection, rootDSE,
1025            ReturnConflictEntriesRequestControl.
1026                 RETURN_CONFLICT_ENTRIES_REQUEST_OID))
1027    {
1028      searchControls.add(new ReturnConflictEntriesRequestControl(false));
1029    }
1030
1031    if (useSoftDeletedEntryAccessControlIfAvailable &&
1032       supportsControl(connection, rootDSE,
1033            SoftDeletedEntryAccessRequestControl.
1034                 SOFT_DELETED_ENTRY_ACCESS_REQUEST_OID))
1035    {
1036      searchControls.add(new SoftDeletedEntryAccessRequestControl(false,
1037           true, false));
1038    }
1039
1040    return delete(connection, baseDN, deleteBaseEntry,
1041         useSetSubtreeAccessibility, usePagedResults, searchRequestSizeLimit,
1042         simplePagedResultsPageSize, useSubentries, searchControls,
1043         deleteControls, deleteRateLimiter);
1044  }
1045
1046
1047
1048  /**
1049   * Attempts to delete the specified subtree using the current settings.
1050   *
1051   * @param  connection
1052   *              The {@link LDAPInterface} instance to use to communicate with
1053   *              the directory server.  While this may be an individual
1054   *              {@link LDAPConnection}, it may be better as a connection
1055   *              pool with automatic retry enabled so that it's more likely to
1056   *              succeed in the event that a connection becomes invalid or an
1057   *              operation experiences a transient failure.  It must not be
1058   *              {@code null}.
1059   * @param  baseDN
1060   *              The base DN for the subtree to delete.  It must not be
1061   *              {@code null}.
1062   * @param  deleteBaseEntry
1063   *              Indicates whether the base entry itself should be deleted
1064   *              along with its subordinates (if {@code true}), or if only the
1065   *              subordinates of the base entry should be deleted but the base
1066   *              entry itself should remain (if {@code false}).
1067   * @param  useSetSubtreeAccessibilityOperation
1068   *              Indicates whether to use the
1069   *              {@link SetSubtreeAccessibilityExtendedRequest} to make the
1070   *              target subtree hidden and read-only before beginning to search
1071   *              for entries to delete.
1072   * @param  useSimplePagedResultsControl
1073   *              Indicates whether to use the {@link SimplePagedResultsControl}
1074   *              when searching for entries to delete.
1075   * @param  searchRequestSizeLimit
1076   *              The size limit that should be used in each search request to
1077   *              specify the maximum number of entries to return in response
1078   *              to that request.  A value that is less than or equal to zero
1079   *              indicates that the client does not want to impose any size
1080   *              limit.
1081   * @param  pageSize
1082   *              The page size for the simple paged results request control, if
1083   *              it is to be used.
1084   * @param  useSubentriesControl
1085   *              Indicates whether to look for LDAP subentries when searching
1086   *              for entries to delete.
1087   * @param  searchControls
1088   *              A list of controls that should be included in search requests
1089   *              used to find the entries to delete.  This must not be
1090   *              {@code null} but may be empty.
1091   * @param  deleteControls
1092   *              A list of controls that should be included in delete requests.
1093   *              This must not be {@code null} but may be empty.
1094   * @param  deleteRateLimiter
1095   *              A fixed-rate barrier used to impose a rate limit on delete
1096   *              operations.  This may be {@code null} if no rate limit should
1097   *              be imposed.
1098   *
1099   * @return  An object with information about the results of the subtree
1100   *          delete processing.
1101   */
1102  private static SubtreeDeleterResult delete(final LDAPInterface connection,
1103               final DN baseDN, final boolean deleteBaseEntry,
1104               final boolean useSetSubtreeAccessibilityOperation,
1105               final boolean useSimplePagedResultsControl,
1106               final int searchRequestSizeLimit, final int pageSize,
1107               final boolean useSubentriesControl,
1108               final List<Control> searchControls,
1109               final List<Control> deleteControls,
1110               final FixedRateBarrier deleteRateLimiter)
1111  {
1112    if (useSetSubtreeAccessibilityOperation)
1113    {
1114      final ExtendedResult setInaccessibleResult =
1115           setInaccessible(connection, baseDN);
1116      if (setInaccessibleResult != null)
1117      {
1118        return new SubtreeDeleterResult(setInaccessibleResult, false, null,
1119             0L,  new TreeMap<DN,LDAPResult>());
1120      }
1121    }
1122
1123    final SubtreeDeleterResult result;
1124    if (useSimplePagedResultsControl)
1125    {
1126      result = deleteEntriesWithSimplePagedResults(connection, baseDN,
1127           deleteBaseEntry, searchRequestSizeLimit, pageSize,
1128           useSubentriesControl, searchControls, deleteControls,
1129           deleteRateLimiter);
1130    }
1131    else
1132    {
1133      result = deleteEntriesWithoutSimplePagedResults(connection, baseDN,
1134           deleteBaseEntry, searchRequestSizeLimit, useSubentriesControl,
1135           searchControls, deleteControls, deleteRateLimiter);
1136    }
1137
1138    if (result.completelySuccessful() && useSetSubtreeAccessibilityOperation)
1139    {
1140      final ExtendedResult removeAccessibilityRestrictionResult =
1141           removeAccessibilityRestriction(connection, baseDN);
1142      if (removeAccessibilityRestrictionResult.getResultCode() ==
1143           ResultCode.SUCCESS)
1144      {
1145        return new SubtreeDeleterResult(null, false, null,
1146             result.getEntriesDeleted(), result.getDeleteErrorsTreeMap());
1147      }
1148      else
1149      {
1150        return new SubtreeDeleterResult(removeAccessibilityRestrictionResult,
1151             true, null, result.getEntriesDeleted(),
1152             result.getDeleteErrorsTreeMap());
1153      }
1154    }
1155    else
1156    {
1157      return new SubtreeDeleterResult(null,
1158           useSetSubtreeAccessibilityOperation,
1159           result.getSearchError(), result.getEntriesDeleted(),
1160           result.getDeleteErrorsTreeMap());
1161    }
1162  }
1163
1164
1165
1166  /**
1167   * Marks the specified subtree as inaccessible.
1168   *
1169   * @param  connection
1170   *              The {@link LDAPInterface} instance to use to communicate with
1171   *              the directory server.  While this may be an individual
1172   *              {@link LDAPConnection}, it may be better as a connection
1173   *              pool with automatic retry enabled so that it's more likely to
1174   *              succeed in the event that a connection becomes invalid or an
1175   *              operation experiences a transient failure.  It must not be
1176   *              {@code null}.
1177   * @param  baseDN
1178   *              The base DN for the subtree to make inaccessible.  It must not
1179   *              be {@code null}.
1180   *
1181   * @return  An {@code LDAPResult} with information about a failure that
1182   *          occurred while trying to make the subtree inaccessible, or
1183   *          {@code null} if the subtree was successfully made inaccessible.
1184   */
1185  private static ExtendedResult setInaccessible(final LDAPInterface connection,
1186                                                final DN baseDN)
1187  {
1188    // Use the "Who Am I?" extended operation to get the authorization identity
1189    // of the provided connection.
1190    final ExtendedResult genericWhoAmIResult = processExtendedOperation(
1191         connection, new WhoAmIExtendedRequest());
1192    if (genericWhoAmIResult.getResultCode() != ResultCode.SUCCESS)
1193    {
1194      return genericWhoAmIResult;
1195    }
1196
1197    final WhoAmIExtendedResult whoAmIResult =
1198         (WhoAmIExtendedResult) genericWhoAmIResult;
1199
1200
1201    // Extract the user DN from the "Who Am I?" result's authorization ID.
1202    final String authzDN;
1203    final String authzID = whoAmIResult.getAuthorizationID();
1204    if (authzID.startsWith("dn:"))
1205    {
1206      authzDN = authzID.substring(3);
1207    }
1208    else
1209    {
1210      return new ExtendedResult(-1, ResultCode.LOCAL_ERROR,
1211           ERR_SUBTREE_DELETER_INTERFACE_WHO_AM_I_AUTHZ_ID_NOT_DN.get(
1212                authzID),
1213           null, StaticUtils.NO_STRINGS, null, null, StaticUtils.NO_CONTROLS);
1214    }
1215
1216
1217    // Use the set subtree accessibility extended operation to make the target
1218    // subtree hidden and read-only.
1219    final ExtendedResult setInaccessibleResult = processExtendedOperation(
1220         connection,
1221         SetSubtreeAccessibilityExtendedRequest.createSetHiddenRequest(
1222              baseDN.toString(), authzDN));
1223
1224    if (setInaccessibleResult.getResultCode() == ResultCode.SUCCESS)
1225    {
1226      return null;
1227    }
1228    else
1229    {
1230      return setInaccessibleResult;
1231    }
1232  }
1233
1234
1235
1236
1237  /**
1238   * Deletes the specified subtree with the given settings.  The simple paged
1239   * results control will be used in the course of searching for entries to
1240   * delete.
1241   *
1242   * @param  connection
1243   *              The {@link LDAPInterface} instance to use to communicate with
1244   *              the directory server.  While this may be an individual
1245   *              {@link LDAPConnection}, it may be better as a connection
1246   *              pool with automatic retry enabled so that it's more likely to
1247   *              succeed in the event that a connection becomes invalid or an
1248   *              operation experiences a transient failure.  It must not be
1249   *              {@code null}.
1250   * @param  baseDN
1251   *              The base DN for the subtree to delete.  It must not be
1252   *              {@code null}.
1253   * @param  deleteBaseEntry
1254   *              Indicates whether the base entry itself should be deleted
1255   *              along with its subordinates (if {@code true}), or if only the
1256   *              subordinates of the base entry should be deleted but the base
1257   *              entry itself should remain (if {@code false}).
1258   * @param  searchRequestSizeLimit
1259   *              The size limit that should be used in each search request to
1260   *              specify the maximum number of entries to return in response
1261   *              to that request.  A value that is less than or equal to zero
1262   *              indicates that the client does not want to impose any size
1263   *              limit.
1264   * @param  pageSize
1265   *              The page size for the simple paged results request control, if
1266   *              it is to be used.
1267   * @param  useSubentriesControl
1268   *              Indicates whether to look for LDAP subentries when searching
1269   *              for entries to delete.
1270   * @param  searchControls
1271   *              A list of controls that should be included in search requests
1272   *              used to find the entries to delete.  This must not be
1273   *              {@code null} but may be empty.
1274   * @param  deleteControls
1275   *              A list of controls that should be included in delete requests.
1276   *              This must not be {@code null} but may be empty.
1277   * @param  deleteRateLimiter
1278   *              A fixed-rate barrier used to impose a rate limit on delete
1279   *              operations.  This may be {@code null} if no rate limit should
1280   *              be imposed.
1281   *
1282   * @return  An object with information about the results of the subtree
1283   *          delete processing.
1284   */
1285  private static SubtreeDeleterResult deleteEntriesWithSimplePagedResults(
1286                      final LDAPInterface connection, final DN baseDN,
1287                      final boolean deleteBaseEntry,
1288                      final int searchRequestSizeLimit,
1289                      final int pageSize,
1290                      final boolean useSubentriesControl,
1291                      final List<Control> searchControls,
1292                      final List<Control> deleteControls,
1293                      final FixedRateBarrier deleteRateLimiter)
1294  {
1295    // If we should use the subentries control, then first search to find all
1296    // subentries in the subtree.
1297    final TreeSet<DN> dnsToDelete = new TreeSet<>();
1298    if (useSubentriesControl)
1299    {
1300      try
1301      {
1302        final SearchRequest searchRequest = createSubentriesSearchRequest(
1303             baseDN, 0, searchControls, dnsToDelete);
1304        doPagedResultsSearch(connection, searchRequest, pageSize);
1305      }
1306      catch (final LDAPSearchException e)
1307      {
1308        Debug.debugException(e);
1309        return new SubtreeDeleterResult(null, false, e.getSearchResult(), 0L,
1310             new TreeMap<DN,LDAPResult>());
1311      }
1312    }
1313
1314
1315    // Perform a paged search to find all all entries (except subentries) in the
1316    // target subtree.
1317    try
1318    {
1319      final SearchRequest searchRequest = createNonSubentriesSearchRequest(
1320           baseDN, 0, searchControls, dnsToDelete);
1321      doPagedResultsSearch(connection, searchRequest, pageSize);
1322    }
1323    catch (final LDAPSearchException e)
1324    {
1325      Debug.debugException(e);
1326      return new SubtreeDeleterResult(null, false, e.getSearchResult(), 0L,
1327           new TreeMap<DN,LDAPResult>());
1328    }
1329
1330
1331    // If we should not delete the base entry, then remove it from the set of
1332    // DNs to delete.
1333    if (! deleteBaseEntry)
1334    {
1335      dnsToDelete.remove(baseDN);
1336    }
1337
1338
1339    // Iterate through the DNs in reverse order and start deleting.  If we
1340    // encounter any entry that can't be deleted, then remove all of its
1341    // ancestors from the set of DNs to delete and create delete errors for
1342    // them.
1343    final AtomicReference<SearchResult> searchError = new AtomicReference<>();
1344    final AtomicLong entriesDeleted = new AtomicLong(0L);
1345    final TreeMap<DN,LDAPResult> deleteErrors = new TreeMap<>();
1346    final Iterator<DN> iterator = dnsToDelete.descendingIterator();
1347    while (iterator.hasNext())
1348    {
1349      final DN dn = iterator.next();
1350      if (! deleteErrors.containsKey(dn))
1351      {
1352        if (! deleteEntry(connection, dn, deleteControls, entriesDeleted,
1353             deleteErrors, deleteRateLimiter, searchRequestSizeLimit,
1354             searchControls, useSubentriesControl, searchError))
1355        {
1356          DN parentDN = dn.getParent();
1357          while ((parentDN != null) && parentDN.isDescendantOf(baseDN, true))
1358          {
1359            if (deleteErrors.containsKey(parentDN))
1360            {
1361              break;
1362            }
1363
1364            deleteErrors.put(parentDN,
1365                 new LDAPResult(-1, ResultCode.NOT_ALLOWED_ON_NONLEAF,
1366                      ERR_SUBTREE_DELETER_SKIPPING_UNDELETABLE_ANCESTOR.get(
1367                           String.valueOf(parentDN), String.valueOf(dn)),
1368                      null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS));
1369            parentDN = parentDN.getParent();
1370          }
1371        }
1372      }
1373    }
1374
1375    return new SubtreeDeleterResult(null, false, null, entriesDeleted.get(),
1376         deleteErrors);
1377  }
1378
1379
1380
1381  /**
1382   * Creates a search request that can be used to find all LDAP subentries at
1383   * or below the specified base DN.
1384   *
1385   * @param  baseDN
1386   *              The base DN to use for the search request.  It must not be
1387   *              {@code null}.
1388   * @param  searchRequestSizeLimit
1389   *              The size limit that should be used in each search request to
1390   *              specify the maximum number of entries to return in response
1391   *              to that request.  A value that is less than or equal to zero
1392   *              indicates that the client does not want to impose any size
1393   *              limit.
1394   * @param  controls
1395   *              The set of controls to use for the search request.  It must
1396   *              not be {@code null} but may be empty.
1397   * @param  dnSet
1398   *              The set of DNs that should be updated with the DNs of the
1399   *              matching entries.  It must not be {@code null} and must be
1400   *              updatable.
1401   *
1402   * @return  A search request that can be used to find all LDAP subentries at
1403   *          or below the specified base DN.
1404   */
1405  private static SearchRequest createSubentriesSearchRequest(
1406                                    final DN baseDN,
1407                                    final int searchRequestSizeLimit,
1408                                    final List<Control> controls,
1409                                    final SortedSet<DN> dnSet)
1410  {
1411    final Filter filter =
1412         Filter.createEqualityFilter("objectClass", "ldapSubentry");
1413
1414    final SubtreeDeleterSearchResultListener searchListener =
1415         new SubtreeDeleterSearchResultListener(baseDN, filter, dnSet);
1416
1417    final SearchRequest searchRequest = new SearchRequest(searchListener,
1418         baseDN.toString(), SearchScope.SUB, DereferencePolicy.NEVER,
1419         searchRequestSizeLimit, 0, false, filter, "1.1");
1420
1421    for (final Control c : controls)
1422    {
1423      searchRequest.addControl(c);
1424    }
1425    searchRequest.addControl(new SubentriesRequestControl(false));
1426
1427    return searchRequest;
1428  }
1429
1430
1431
1432  /**
1433   * Creates a search request that can be used to find all entries at or below
1434   * the specified base DN that are not LDAP subentries.
1435   *
1436   * @param  baseDN
1437   *              The base DN to use for the search request.  It must not be
1438   *              {@code null}.
1439   * @param  searchRequestSizeLimit
1440   *              The size limit that should be used in each search request to
1441   *              specify the maximum number of entries to return in response
1442   *              to that request.  A value that is less than or equal to zero
1443   *              indicates that the client does not want to impose any size
1444   *              limit.
1445   * @param  controls
1446   *              The set of controls to use for the search request.  It must
1447   *              not be {@code null} but may be empty.
1448   * @param  dnSet
1449   *              The set of DNs that should be updated with the DNs of the
1450   *              matching entries.  It must not be {@code null} and must be
1451   *              updatable.
1452   *
1453   * @return  A search request that can be used to find all entries at or below
1454   *          the specified base DN that are not LDAP subentries.
1455   */
1456  private static SearchRequest createNonSubentriesSearchRequest(
1457                                    final DN baseDN,
1458                                    final int searchRequestSizeLimit,
1459                                    final List<Control> controls,
1460                                    final SortedSet<DN> dnSet)
1461  {
1462    final Filter filter = Filter.createPresenceFilter("objectClass");
1463
1464    final SubtreeDeleterSearchResultListener searchListener =
1465         new SubtreeDeleterSearchResultListener(baseDN, filter, dnSet);
1466
1467    final SearchRequest searchRequest = new SearchRequest(searchListener,
1468         baseDN.toString(), SearchScope.SUB, DereferencePolicy.NEVER,
1469         searchRequestSizeLimit, 0, false, filter, "1.1");
1470
1471    for (final Control c : controls)
1472    {
1473      searchRequest.addControl(c);
1474    }
1475
1476    return searchRequest;
1477  }
1478
1479
1480
1481  /**
1482   * Uses the simple paged results control to iterate through all entries in
1483   * the server that match the criteria from the provided search request.
1484   *
1485   * @param  connection
1486   *              The {@link LDAPInterface} instance to use to communicate with
1487   *              the directory server.  While this may be an individual
1488   *              {@link LDAPConnection}, it may be better as a connection
1489   *              pool with automatic retry enabled so that it's more likely to
1490   *              succeed in the event that a connection becomes invalid or an
1491   *              operation experiences a transient failure.  It must not be
1492   *              {@code null}.
1493   * @param  searchRequest
1494   *              The search request to be processed using the simple paged
1495   *              results control.  The request must not already include the
1496   *              simple paged results request control, but must otherwise be
1497   *              the request that should be processed, including any other
1498   *              controls that are desired.  It must not be {@code null}.
1499   * @param  pageSize
1500   *              The maximum number of entries that should be included in any
1501   *              page of results.  It must be greater than or equal to one.
1502   *
1503   * @throws  LDAPSearchException  If a problem is encountered during search
1504   *                               processing that prevents it from successfully
1505   *                               identifying all of the entries.
1506   */
1507  private static void doPagedResultsSearch(final LDAPInterface connection,
1508                                           final SearchRequest searchRequest,
1509                                           final int pageSize)
1510          throws LDAPSearchException
1511  {
1512    final SubtreeDeleterSearchResultListener searchListener =
1513         (SubtreeDeleterSearchResultListener)
1514         searchRequest.getSearchResultListener();
1515
1516    ASN1OctetString pagedResultsCookie = null;
1517    while (true)
1518    {
1519      final SearchRequest pagedResultsSearchRequest = searchRequest.duplicate();
1520      pagedResultsSearchRequest.addControl(new SimplePagedResultsControl(
1521           pageSize, pagedResultsCookie, true));
1522
1523      SearchResult searchResult;
1524      try
1525      {
1526        searchResult = connection.search(pagedResultsSearchRequest);
1527      }
1528      catch (final LDAPSearchException e)
1529      {
1530        Debug.debugException(e);
1531        searchResult = e.getSearchResult();
1532      }
1533
1534      if (searchResult.getResultCode() == ResultCode.NO_SUCH_OBJECT)
1535      {
1536        // This means that the base entry doesn't exist.  This isn't an error.
1537        // It just means that there aren't any entries to delete.
1538        return;
1539      }
1540      else if (searchResult.getResultCode() != ResultCode.SUCCESS)
1541      {
1542        throw new LDAPSearchException(searchResult);
1543      }
1544      else if (searchListener.getFirstException() != null)
1545      {
1546        throw new LDAPSearchException(searchListener.getFirstException());
1547      }
1548
1549      final SimplePagedResultsControl responseControl;
1550      try
1551      {
1552        responseControl = SimplePagedResultsControl.get(searchResult);
1553      }
1554      catch (final LDAPException e)
1555      {
1556        Debug.debugException(e);
1557        throw new LDAPSearchException(e);
1558      }
1559
1560      if (responseControl == null)
1561      {
1562        throw new LDAPSearchException(ResultCode.CONTROL_NOT_FOUND,
1563             ERR_SUBTREE_DELETER_MISSING_PAGED_RESULTS_RESPONSE.get(
1564                  searchRequest.getBaseDN(), searchRequest.getFilter()));
1565      }
1566
1567      if (responseControl.moreResultsToReturn())
1568      {
1569        pagedResultsCookie = responseControl.getCookie();
1570      }
1571      else
1572      {
1573        return;
1574      }
1575    }
1576  }
1577
1578
1579
1580  /**
1581   * Attempts to delete an entry from the server.  If the delete attempt fails
1582   * with a {@link ResultCode#NOT_ALLOWED_ON_NONLEAF} result, then an attempt
1583   * will be made to search for all of the subordinates of the target entry so
1584   * that they can be deleted, and then a second attempt will be made to remove
1585   * the target entry.
1586   *
1587   * @param  connection
1588   *              The {@link LDAPInterface} instance to use to communicate with
1589   *              the directory server.  While this may be an individual
1590   *              {@link LDAPConnection}, it may be better as a connection
1591   *              pool with automatic retry enabled so that it's more likely to
1592   *              succeed in the event that a connection becomes invalid or an
1593   *              operation experiences a transient failure.  It must not be
1594   *              {@code null}.
1595   * @param  dn   The DN of the entry to delete.  It must not be {@code null}.
1596   * @param  deleteControls
1597   *              A list of the controls that should be included in the delete
1598   *              request.  It must not be {@code null}, but may be empty.
1599   * @param  entriesDeleted
1600   *              A counter that should be incremented for each entry that is
1601   *              successfully deleted.  It must not be {@code null}.
1602   * @param  deleteErrors
1603   *              A map that should be updated with the DN of the entry and the
1604   *              delete result, if the delete is unsuccessful.  It must not be
1605   *              {@code null} and must be updatable.
1606   * @param  deleteRateLimiter
1607   *              A fixed-rate barrier used to impose a rate limit on delete
1608   *              operations.  This may be {@code null} if no rate limit should
1609   *              be imposed.
1610   * @param  searchRequestSizeLimit
1611   *              The size limit that should be used in each search request to
1612   *              specify the maximum number of entries to return in response
1613   *              to that request.  A value that is less than or equal to zero
1614   *              indicates that the client does not want to impose any size
1615   *              limit.
1616   * @param  searchControls
1617   *              A list of controls that should be included in search
1618   *              requests, if the initial delete attempt fails because the
1619   *              entry has subordinates.  It must not be {@code null}, but may
1620   *              be empty.
1621   * @param  useSubentriesControl
1622   *              Indicates whether to look for LDAP subentries when searching
1623   *              for entries to delete.
1624   * @param  searchError
1625   *              A reference that may be updated, if it is not already set,
1626   *              with information about an error that occurred during search
1627   *              processing.  It must not be {@code null}, but may be
1628   *              unassigned.
1629   *
1630   * @return  {@code true} if the entry was successfully deleted, or
1631   *          {@code false} if not.
1632   */
1633  private static boolean deleteEntry(final LDAPInterface connection,
1634                              final DN dn, final List<Control> deleteControls,
1635                              final AtomicLong entriesDeleted,
1636                              final SortedMap<DN,LDAPResult> deleteErrors,
1637                              final FixedRateBarrier deleteRateLimiter,
1638                              final int searchRequestSizeLimit,
1639                              final List<Control> searchControls,
1640                              final boolean useSubentriesControl,
1641                              final AtomicReference<SearchResult> searchError)
1642  {
1643    if (deleteRateLimiter != null)
1644    {
1645      deleteRateLimiter.await();
1646    }
1647
1648    LDAPResult deleteResult;
1649    try
1650    {
1651      deleteResult = connection.delete(dn.toString());
1652    }
1653    catch (final LDAPException e)
1654    {
1655      Debug.debugException(e);
1656      deleteResult = e.toLDAPResult();
1657    }
1658
1659    final ResultCode resultCode = deleteResult.getResultCode();
1660    if (resultCode == ResultCode.SUCCESS)
1661    {
1662      // The entry was successfully deleted.
1663      entriesDeleted.incrementAndGet();
1664      return true;
1665    }
1666    else if (resultCode == ResultCode.NO_SUCH_OBJECT)
1667    {
1668      // This is fine.  It must have been deleted between the time of the
1669      // search and the time we got around to deleting it.
1670      return true;
1671    }
1672    else if (resultCode == ResultCode.NOT_ALLOWED_ON_NONLEAF)
1673    {
1674      // The entry must have children.  Try to recursively delete it.
1675      return searchAndDelete(connection, dn, searchRequestSizeLimit,
1676           searchControls, useSubentriesControl, searchError, deleteControls,
1677           entriesDeleted, deleteErrors, deleteRateLimiter);
1678    }
1679    else
1680    {
1681      // This is just an error.
1682      deleteErrors.put(dn, deleteResult);
1683      return false;
1684    }
1685  }
1686
1687
1688
1689  /**
1690   * Issues a subtree search (or a pair of subtree searches if the subentries
1691   * control should be used) to find any entries below the provided base DN,
1692   * and then attempts to delete all of those entries.
1693   *
1694   * @param  connection
1695   *              The {@link LDAPInterface} instance to use to communicate with
1696   *              the directory server.  While this may be an individual
1697   *              {@link LDAPConnection}, it may be better as a connection
1698   *              pool with automatic retry enabled so that it's more likely to
1699   *              succeed in the event that a connection becomes invalid or an
1700   *              operation experiences a transient failure.  It must not be
1701   *              {@code null}.
1702   * @param  baseDN
1703   *              The base DN for the subtree in which to perform the search and
1704   *              delete operations.  It must not be {@code null}.
1705   * @param  searchRequestSizeLimit
1706   *              The size limit that should be used in each search request to
1707   *              specify the maximum number of entries to return in response
1708   *              to that request.  A value that is less than or equal to zero
1709   *              indicates that the client does not want to impose any size
1710   *              limit.
1711   * @param  searchControls
1712   *              A list of controls that should be included in search
1713   *              requests, if the initial delete attempt fails because the
1714   *              entry has subordinates.  It must not be {@code null}, but may
1715   *              be empty.
1716   * @param  useSubentriesControl
1717   *              Indicates whether to look for LDAP subentries when searching
1718   *              for entries to delete.
1719   * @param  searchError
1720   *              A reference that may be updated, if it is not already set,
1721   *              with information about an error that occurred during search
1722   *              processing.  It must not be {@code null}, but may be
1723   *              unassigned.
1724   * @param  deleteControls
1725   *              A list of the controls that should be included in the delete
1726   *              request.  It must not be {@code null}, but may be empty.
1727   * @param  entriesDeleted
1728   *              A counter that should be incremented for each entry that is
1729   *              successfully deleted.  It must not be {@code null}.
1730   * @param  deleteErrors
1731   *              A map that should be updated with the DN of the entry and the
1732   *              delete result, if the delete is unsuccessful.  It must not be
1733   *              {@code null} and must be updatable.
1734   * @param  deleteRateLimiter
1735   *              A fixed-rate barrier used to impose a rate limit on delete
1736   *              operations.  This may be {@code null} if no rate limit should
1737   *              be imposed.
1738   *
1739   * @return  {@code true} if the subtree was successfully deleted, or
1740   *          {@code false} if any errors occurred that prevented one or more
1741   *          entries from being removed.
1742   */
1743  private static boolean searchAndDelete(final LDAPInterface connection,
1744                              final DN baseDN,
1745                              final int searchRequestSizeLimit,
1746                              final List<Control> searchControls,
1747                              final boolean useSubentriesControl,
1748                              final AtomicReference<SearchResult> searchError,
1749                              final List<Control> deleteControls,
1750                              final AtomicLong entriesDeleted,
1751                              final SortedMap<DN,LDAPResult> deleteErrors,
1752                              final FixedRateBarrier deleteRateLimiter)
1753  {
1754    while (true)
1755    {
1756      // If appropriate, search for subentries.
1757      SearchResult subentriesSearchResult = null;
1758      final TreeSet<DN> dnsToDelete = new TreeSet<>();
1759      if (useSubentriesControl)
1760      {
1761        try
1762        {
1763          subentriesSearchResult = connection.search(
1764               createSubentriesSearchRequest(baseDN, searchRequestSizeLimit,
1765                    searchControls, dnsToDelete));
1766        }
1767        catch (final LDAPSearchException e)
1768        {
1769          Debug.debugException(e);
1770          subentriesSearchResult = e.getSearchResult();
1771        }
1772      }
1773
1774
1775      // Search for non-subentries.
1776      SearchResult nonSubentriesSearchResult;
1777      try
1778      {
1779        nonSubentriesSearchResult = connection.search(
1780             createNonSubentriesSearchRequest(baseDN, searchRequestSizeLimit,
1781                  searchControls, dnsToDelete));
1782      }
1783      catch (final LDAPSearchException e)
1784      {
1785        Debug.debugException(e);
1786        nonSubentriesSearchResult = e.getSearchResult();
1787      }
1788
1789
1790      // If we didn't find any entries, then there's nothing to do but
1791      // potentially update the search error.
1792      if (dnsToDelete.isEmpty())
1793      {
1794        if (subentriesSearchResult != null)
1795        {
1796          switch (subentriesSearchResult.getResultCode().intValue())
1797          {
1798            case ResultCode.SUCCESS_INT_VALUE:
1799            case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
1800              // These are both fine.
1801              break;
1802
1803            default:
1804              searchError.compareAndSet(null, subentriesSearchResult);
1805              return false;
1806          }
1807        }
1808
1809        switch (nonSubentriesSearchResult.getResultCode().intValue())
1810        {
1811          case ResultCode.SUCCESS_INT_VALUE:
1812          case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
1813            // These are both fine.
1814            break;
1815
1816          default:
1817            searchError.compareAndSet(null, nonSubentriesSearchResult);
1818            return false;
1819        }
1820
1821        // Even though we didn't delete anything, we can assume that the entries
1822        // don't exist, so we'll consider it successful.
1823        return true;
1824      }
1825
1826
1827      // Iterate through the entries that we found and delete the ones that we
1828      // can.
1829      boolean anySuccessful = false;
1830      boolean allSuccessful = true;
1831      final TreeSet<DN> ancestorsToSkip = new TreeSet<>();
1832
1833      final DeleteRequest deleteRequest = new DeleteRequest("");
1834      deleteRequest.setControls(deleteControls);
1835      for (final DN dn : dnsToDelete.descendingSet())
1836      {
1837        if (deleteErrors.containsKey(dn))
1838        {
1839          // We've already encountered an error for this entry, so don't try to
1840          // delete it.
1841          allSuccessful = false;
1842          continue;
1843        }
1844        else if (ancestorsToSkip.contains(dn))
1845        {
1846          // We've already encountered an error while trying to delete one of
1847          // the descendants of this entry, so we'll skip it on this pass.  We
1848          // might get it on another pass.
1849          allSuccessful = false;
1850          continue;
1851        }
1852
1853        // If there is a rate limiter, then wait on it.
1854        if (deleteRateLimiter != null)
1855        {
1856          deleteRateLimiter.await();
1857        }
1858
1859        // Try to delete the target entry.
1860        LDAPResult deleteResult;
1861        try
1862        {
1863          deleteRequest.setDN(dn);
1864          deleteResult = connection.delete(deleteRequest);
1865        }
1866        catch (final LDAPException e)
1867        {
1868          Debug.debugException(e);
1869          deleteResult = e.toLDAPResult();
1870        }
1871
1872        switch (deleteResult.getResultCode().intValue())
1873        {
1874          case ResultCode.SUCCESS_INT_VALUE:
1875            // The entry was successfully deleted.
1876            anySuccessful = true;
1877            entriesDeleted.incrementAndGet();
1878            break;
1879
1880          case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
1881            // The entry doesn't exist.  It may have been deleted between the
1882            // time we searched for it and the time we tried to delete it.
1883            // We'll treat this like a success, but won't increment the
1884            // counter.
1885            anySuccessful = true;
1886            break;
1887
1888          case ResultCode.NOT_ALLOWED_ON_NONLEAF_INT_VALUE:
1889            // This suggests that the entry has children.  If it is the base
1890            // entry, then we may be able to loop back around and delete it on
1891            // another pass.  Otherwise, try to recursively delete it.
1892            if (dn.equals(baseDN))
1893            {
1894              allSuccessful = false;
1895            }
1896            else
1897            {
1898              if (searchAndDelete(connection, dn, searchRequestSizeLimit,
1899                   searchControls, useSubentriesControl, searchError,
1900                   deleteControls, entriesDeleted, deleteErrors,
1901                   deleteRateLimiter))
1902              {
1903                anySuccessful = true;
1904              }
1905              else
1906              {
1907                allSuccessful = false;
1908
1909                DN parentDN = dn.getParent();
1910                while (parentDN != null)
1911                {
1912                  ancestorsToSkip.add(parentDN);
1913                  parentDN = parentDN.getParent();
1914                }
1915              }
1916            }
1917            break;
1918
1919          default:
1920            // We definitely couldn't delete this entry, and we're not going to
1921            // make another attempt.  Put it in the set of delete errors, and
1922            // also include the DNs of all of its ancestors.
1923            deleteErrors.put(dn, deleteResult);
1924
1925            DN parentDN = dn.getParent();
1926            while ((parentDN != null) && parentDN.isDescendantOf(baseDN, true))
1927            {
1928              deleteErrors.put(parentDN,
1929                   new LDAPResult(-1, ResultCode.NOT_ALLOWED_ON_NONLEAF,
1930                      ERR_SUBTREE_DELETER_SKIPPING_UNDELETABLE_ANCESTOR.get(
1931                           String.valueOf(parentDN), String.valueOf(dn)),
1932                      null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS));
1933              parentDN = parentDN.getParent();
1934            }
1935
1936            allSuccessful = false;
1937            break;
1938        }
1939      }
1940
1941
1942      // Look at the search results and see if we need to update the search
1943      // error.  There's no error for a result code of SUCCESS or
1944      // NO_SUCH_OBJECT.  If the result code is SIZE_LIMIT_EXCEEDED, then that's
1945      // an error only if we couldn't delete any of the entries that we found.
1946      // If the result code is anything else, then that's an error.
1947      if (subentriesSearchResult != null)
1948      {
1949        switch (subentriesSearchResult.getResultCode().intValue())
1950        {
1951          case ResultCode.SUCCESS_INT_VALUE:
1952          case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
1953            break;
1954
1955          case ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE:
1956            if (! anySuccessful)
1957            {
1958              searchError.compareAndSet(null, subentriesSearchResult);
1959            }
1960            break;
1961
1962          default:
1963            searchError.compareAndSet(null, subentriesSearchResult);
1964            break;
1965        }
1966      }
1967
1968      switch (nonSubentriesSearchResult.getResultCode().intValue())
1969      {
1970        case ResultCode.SUCCESS_INT_VALUE:
1971        case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
1972          break;
1973
1974        case ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE:
1975          if (! anySuccessful)
1976          {
1977            searchError.compareAndSet(null, nonSubentriesSearchResult);
1978          }
1979          break;
1980
1981        default:
1982          searchError.compareAndSet(null, nonSubentriesSearchResult);
1983          break;
1984      }
1985
1986
1987      // Evaluate the success or failure of the processing that we performed.
1988      if (allSuccessful)
1989      {
1990        // We were able to successfully complete all of the deletes that we
1991        // attempted.  If the base entry was included in that set, then we were
1992        // successful and can return true.  Otherwise, we should loop back
1993        // around because that suggests there are more entries to delete.
1994        if (dnsToDelete.contains(baseDN))
1995        {
1996          return true;
1997        }
1998      }
1999      else if (! anySuccessful)
2000      {
2001        // We couldn't delete any of the entries that we tried.  This is
2002        // definitely an error.
2003        return false;
2004      }
2005
2006
2007      // If we've gotten here, then that means that we deleted at least some of
2008      // the entries, but we need to loop back around and make another attempt
2009    }
2010  }
2011
2012
2013
2014  /**
2015   * Deletes the specified subtree with the given settings.  The simple paged
2016   * results control will not be used in the course of searching for entries to
2017   * delete.
2018   *
2019   * @param  connection
2020   *              The {@link LDAPInterface} instance to use to communicate with
2021   *              the directory server.  While this may be an individual
2022   *              {@link LDAPConnection}, it may be better as a connection
2023   *              pool with automatic retry enabled so that it's more likely to
2024   *              succeed in the event that a connection becomes invalid or an
2025   *              operation experiences a transient failure.  It must not be
2026   *              {@code null}.
2027   * @param  baseDN
2028   *              The base DN for the subtree to delete.  It must not be
2029   *              {@code null}.
2030   * @param  deleteBaseEntry
2031   *              Indicates whether the base entry itself should be deleted
2032   *              along with its subordinates (if {@code true}), or if only the
2033   *              subordinates of the base entry should be deleted but the base
2034   *              entry itself should remain (if {@code false}).
2035   * @param  searchRequestSizeLimit
2036   *              The size limit that should be used in each search request to
2037   *              specify the maximum number of entries to return in response
2038   *              to that request.  A value that is less than or equal to zero
2039   *              indicates that the client does not want to impose any size
2040   *              limit.
2041   * @param  useSubentriesControl
2042   *              Indicates whether to look for LDAP subentries when searching
2043   *              for entries to delete.
2044   * @param  searchControls
2045   *              A list of controls that should be included in search requests
2046   *              used to find the entries to delete.  This must not be
2047   *              {@code null} but may be empty.
2048   * @param  deleteControls
2049   *              A list of controls that should be included in delete requests.
2050   *              This must not be {@code null} but may be empty.
2051   * @param  deleteRateLimiter
2052   *              A fixed-rate barrier used to impose a rate limit on delete
2053   *              operations.  This may be {@code null} if no rate limit should
2054   *              be imposed.
2055   *
2056   * @return  An object with information about the results of the subtree
2057   *          delete processing.
2058   */
2059  private static SubtreeDeleterResult deleteEntriesWithoutSimplePagedResults(
2060                      final LDAPInterface connection, final DN baseDN,
2061                      final boolean deleteBaseEntry,
2062                      final int searchRequestSizeLimit,
2063                      final boolean useSubentriesControl,
2064                      final List<Control> searchControls,
2065                      final List<Control> deleteControls,
2066                      final FixedRateBarrier deleteRateLimiter)
2067  {
2068    // If we should use the subentries control, then first search to find all
2069    // subentries in the subentry, and delete them first.  Continue the
2070    // process until we run out of entries or until we can't delete any more.
2071    final TreeSet<DN> dnsToDelete = new TreeSet<>();
2072    final AtomicReference<SearchResult> searchError = new AtomicReference<>();
2073    final AtomicLong entriesDeleted = new AtomicLong(0L);
2074    final TreeMap<DN,LDAPResult> deleteErrors = new TreeMap<>();
2075    if (useSubentriesControl)
2076    {
2077      final SearchRequest searchRequest = createSubentriesSearchRequest(
2078           baseDN, searchRequestSizeLimit, searchControls, dnsToDelete);
2079      searchAndDelete(connection, baseDN, searchRequest, useSubentriesControl,
2080           searchControls, dnsToDelete, searchError, deleteBaseEntry,
2081           deleteControls, deleteRateLimiter,
2082           entriesDeleted, deleteErrors);
2083    }
2084
2085
2086    // Create a search request that doesn't use the subentries request
2087    // control,and use that to conduct the searches to identify the entries to
2088    // delete.
2089    final SearchRequest searchRequest = createNonSubentriesSearchRequest(baseDN,
2090         searchRequestSizeLimit, searchControls, dnsToDelete);
2091    searchAndDelete(connection, baseDN, searchRequest, useSubentriesControl,
2092         searchControls, dnsToDelete, searchError, deleteBaseEntry,
2093         deleteControls, deleteRateLimiter,
2094         entriesDeleted, deleteErrors);
2095
2096    return new SubtreeDeleterResult(null, false, searchError.get(),
2097         entriesDeleted.get(), deleteErrors);
2098  }
2099
2100
2101
2102  /**
2103   * Repeatedly processes the provided search request until there are no more
2104   * matching entries or until no more entries can be deleted.
2105   *
2106   * @param  connection
2107   *              The {@link LDAPInterface} instance to use to communicate with
2108   *              the directory server.  While this may be an individual
2109   *              {@link LDAPConnection}, it may be better as a connection
2110   *              pool with automatic retry enabled so that it's more likely to
2111   *              succeed in the event that a connection becomes invalid or an
2112   *              operation experiences a transient failure.  It must not be
2113   *              {@code null}.
2114   * @param  baseDN
2115   *              The base DN for the subtree to delete.  It must not be
2116   *              {@code null}.
2117   * @param  searchRequest
2118   *              The search request to use to identify the entries to delete.
2119   *              It must not be {@code null}, and must be repeatable exactly
2120   *              as-is.
2121   * @param  useSubentriesControl
2122   *              Indicates whether to look for LDAP subentries when searching
2123   *              for entries to delete.
2124   * @param  searchControls
2125   *              A list of controls that should be included in search requests
2126   *              used to find the entries to delete.  This must not be
2127   *              {@code null} but may be empty.
2128   * @param  dnsToDelete
2129   *              A sorted set that will be updated during search processing
2130   *              with the DNs of the entries that match the search criteria.
2131   *              It must not be {@code null}, and must be updatable.
2132   * @param  searchError
2133   *              A reference to an error that was encountered during search
2134   *              processing.  It must not be {@code null}, but may be
2135   *              unassigned.
2136   * @param  deleteBaseEntry
2137   *              Indicates whether the base entry itself should be deleted
2138   *              along with its subordinates (if {@code true}), or if only the
2139   *              subordinates of the base entry should be deleted but the base
2140   *              entry itself should remain (if {@code false}).
2141   * @param  deleteControls
2142   *              A list of controls that should be included in delete requests.
2143   *              This must not be {@code null} but may be empty.
2144   * @param  deleteRateLimiter
2145   *              A fixed-rate barrier used to impose a rate limit on delete
2146   *              operations.  This may be {@code null} if no rate limit should
2147   *              be imposed.
2148   * @param  entriesDeleted
2149   *              A counter used to keep track of the number of entries that
2150   *              have been deleted.  It must not be {@code null}.
2151   * @param  deleteErrors
2152   *              A sorted map that will be updated with information about
2153   *              unsuccessful attempts to delete entries.  It must not be
2154   *              {@code null}, and must be updatable.
2155   */
2156  private static void searchAndDelete(final LDAPInterface connection,
2157                           final DN baseDN, final SearchRequest searchRequest,
2158                           final boolean useSubentriesControl,
2159                           final List<Control> searchControls,
2160                           final TreeSet<DN> dnsToDelete,
2161                           final AtomicReference<SearchResult> searchError,
2162                           final boolean deleteBaseEntry,
2163                           final List<Control> deleteControls,
2164                           final FixedRateBarrier deleteRateLimiter,
2165                           final AtomicLong entriesDeleted,
2166                           final SortedMap<DN,LDAPResult> deleteErrors)
2167  {
2168    while (true)
2169    {
2170      // Get the number of entries that have been deleted thus far.  If this
2171      // hasn't gone up by the end of this loop, then we'll stop looping.
2172      final long beforeDeleteCount = entriesDeleted.get();
2173
2174
2175      // Issue a search to find all of the entries we can that match the
2176      // search criteria.
2177      SearchResult searchResult;
2178      try
2179      {
2180        searchResult = connection.search(searchRequest);
2181      }
2182      catch (final LDAPSearchException e)
2183      {
2184        Debug.debugException(e);
2185        searchResult = e.getSearchResult();
2186      }
2187
2188
2189      // See if we should update the search error result.
2190      if (searchError.get() == null)
2191      {
2192        final ResultCode searchResultCode = searchResult.getResultCode();
2193        if (searchResultCode == ResultCode.SUCCESS)
2194        {
2195          // This is obviously not an error.
2196        }
2197        else if (searchResultCode == ResultCode.NO_SUCH_OBJECT)
2198        {
2199          // This is also not an error.  It means that the base entry doesn't
2200          // exist, so there's no point in continuing on.
2201          return;
2202        }
2203        else if (searchResultCode == ResultCode.SIZE_LIMIT_EXCEEDED)
2204        {
2205          // This is probably not an error, but we may consider it one if we
2206          // can't delete anything during this pass.
2207        }
2208        else
2209        {
2210          // This is an error.
2211          searchError.compareAndSet(null, searchResult);
2212        }
2213      }
2214
2215
2216      // If we should not delete the base entry, then remove it from the set.
2217      if (! deleteBaseEntry)
2218      {
2219        dnsToDelete.remove(baseDN);
2220      }
2221
2222
2223      // Iterate through the DN set, which should have been populated by the
2224      // search.  If any of them are in the delete errors map, then we'll skip
2225      // them.  All others we'll try to delete.
2226      final Iterator<DN> dnIterator = dnsToDelete.descendingIterator();
2227      while (dnIterator.hasNext())
2228      {
2229        final DN dnToDelete = dnIterator.next();
2230        dnIterator.remove();
2231
2232        // Don't try to delete the entry if we've already tried and failed.
2233        if (! deleteErrors.containsKey(dnToDelete))
2234        {
2235          if (! deleteEntry(connection, dnToDelete, deleteControls,
2236               entriesDeleted, deleteErrors, deleteRateLimiter,
2237               searchRequest.getSizeLimit(), searchControls,
2238               useSubentriesControl, searchError))
2239          {
2240            // We couldn't delete the entry.  That means we also won't be able
2241            // to delete its parents, so put them in the errors map so that we
2242            // won't even try to delete them.
2243            DN parentDN = dnToDelete.getParent();
2244            while ((parentDN != null) && parentDN.isDescendantOf(baseDN, true))
2245            {
2246              if (deleteErrors.containsKey(parentDN))
2247              {
2248                break;
2249              }
2250
2251              deleteErrors.put(parentDN,
2252                   new LDAPResult(-1, ResultCode.NOT_ALLOWED_ON_NONLEAF,
2253                        ERR_SUBTREE_DELETER_SKIPPING_UNDELETABLE_ANCESTOR.get(
2254                             String.valueOf(parentDN),
2255                             String.valueOf(dnToDelete)),
2256                        null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS));
2257              parentDN = parentDN.getParent();
2258            }
2259          }
2260        }
2261      }
2262
2263      final long afterDeleteCount = entriesDeleted.get();
2264      if (afterDeleteCount == beforeDeleteCount)
2265      {
2266        // We were unable to successfully delete any entries this time through
2267        // the loop.  That may mean that there aren't any more entries, or that
2268        // errors prevented deleting the entries we did find.  If we happened to
2269        // get a "size limit exceeded" search result, and if the search error
2270        // isn't set, then set it to the "size limit exceeded" result.
2271        if (searchResult.getResultCode() == ResultCode.SIZE_LIMIT_EXCEEDED)
2272        {
2273          searchError.compareAndSet(null, searchResult);
2274        }
2275
2276        return;
2277      }
2278    }
2279  }
2280
2281
2282
2283  /**
2284   * Removes teh subtree accessibility restriction from the server.
2285   *
2286   * @param  connection
2287   *              The {@link LDAPInterface} instance to use to communicate with
2288   *              the directory server.  While this may be an individual
2289   *              {@link LDAPConnection}, it may be better as a connection
2290   *              pool with automatic retry enabled so that it's more likely to
2291   *              succeed in the event that a connection becomes invalid or an
2292   *              operation experiences a transient failure.  It must not be
2293   *              {@code null}.
2294   * @param  baseDN
2295   *              The base DN for the subtree to make accessible.  It must not
2296   *              be {@code null}.
2297   *
2298   * @return  The result of the attempt to remove the subtree accessibility
2299   *          restriction.
2300   */
2301  private static ExtendedResult removeAccessibilityRestriction(
2302                                     final LDAPInterface connection,
2303                                     final DN baseDN)
2304  {
2305    return processExtendedOperation(connection,
2306         SetSubtreeAccessibilityExtendedRequest.createSetAccessibleRequest(
2307              baseDN.toString()));
2308  }
2309
2310
2311
2312  /**
2313   * Uses the provided connection to process the given extended request.
2314   *
2315   * @param  connection
2316   *              The {@link LDAPInterface} instance to use to communicate with
2317   *              the directory server.  While this may be an individual
2318   *              {@link LDAPConnection}, it may be better as a connection
2319   *              pool with automatic retry enabled so that it's more likely to
2320   *              succeed in the event that a connection becomes invalid or an
2321   *              operation experiences a transient failure.  It must not be
2322   *              {@code null}.
2323   * @param  request
2324   *              The extended request to be processed.  It must not be
2325   *              {@code null}.
2326   *
2327   * @return  The extended result obtained from processing the request.
2328   */
2329  private static ExtendedResult processExtendedOperation(
2330                                     final LDAPInterface connection,
2331                                     final ExtendedRequest request)
2332  {
2333    try
2334    {
2335      if (connection instanceof LDAPConnection)
2336      {
2337        return ((LDAPConnection) connection).processExtendedOperation(
2338             request);
2339      }
2340      else if (connection instanceof AbstractConnectionPool)
2341      {
2342        return ((AbstractConnectionPool) connection).processExtendedOperation(
2343             request);
2344      }
2345      else
2346      {
2347        return new ExtendedResult(-1, ResultCode.PARAM_ERROR,
2348             ERR_SUBTREE_DELETER_INTERFACE_EXTOP_NOT_SUPPORTED.get(
2349                  connection.getClass().getName()),
2350             null, StaticUtils.NO_STRINGS, null, null, StaticUtils.NO_CONTROLS);
2351      }
2352    }
2353    catch (final LDAPException e)
2354    {
2355      Debug.debugException(e);
2356      return new ExtendedResult(e);
2357    }
2358  }
2359
2360
2361
2362  /**
2363   * Attempts to determine whether the server advertises support for the
2364   * specified extended request.
2365   *
2366   * @param  connection
2367   *              The connection (or other {@link LDAPInterface} instance, like
2368   *              a connection pool) that should be used to communicate with the
2369   *              directory server.  It must not be {@code null}.
2370   * @param  rootDSE
2371   *              A reference to the server root DSE, if it has already been
2372   *              retrieved.  It must not be {@code null}, but may be
2373   *              unassigned.
2374   * @param  oid  The OID of the extended request for which to make the
2375   *              determination.  It must not be {@code null}.
2376   *
2377   * @return  {@code true} if the server advertises support for the specified
2378   *          request control, or {@code false} if not.
2379   */
2380  private static boolean supportsExtendedRequest(final LDAPInterface connection,
2381                              final AtomicReference<RootDSE> rootDSE,
2382                              final String oid)
2383  {
2384    final RootDSE dse = getRootDSE(connection, rootDSE);
2385    if (dse == null)
2386    {
2387      return false;
2388    }
2389    else
2390    {
2391      return dse.supportsExtendedOperation(oid);
2392    }
2393  }
2394
2395
2396
2397  /**
2398   * Attempts to determine whether the server advertises support for the
2399   * specified request control.
2400   *
2401   * @param  connection
2402   *              The connection (or other {@link LDAPInterface} instance, like
2403   *              a connection pool) that should be used to communicate with the
2404   *              directory server.  It must not be {@code null}.
2405   * @param  rootDSE
2406   *              A reference to the server root DSE, if it has already been
2407   *              retrieved.  It must not be {@code null}, but may be
2408   *              unassigned.
2409   * @param  oid  The OID of the request control for which to make the
2410   *              determination.  It must not be {@code null}.
2411   *
2412   * @return  {@code true} if the server advertises support for the specified
2413   *          request control, or {@code false} if not.
2414   */
2415  private static boolean supportsControl(final LDAPInterface connection,
2416                                         final AtomicReference<RootDSE> rootDSE,
2417                                         final String oid)
2418  {
2419    final RootDSE dse = getRootDSE(connection, rootDSE);
2420    if (dse == null)
2421    {
2422      return false;
2423    }
2424    else
2425    {
2426      return dse.supportsControl(oid);
2427    }
2428  }
2429
2430
2431
2432  /**
2433   * Retrieves the server's root DSE.  It will use the cached version if it's
2434   * already available, or will retrieve it from the server if not.
2435   *
2436   * @param  connection
2437   *              The connection (or other {@link LDAPInterface} instance, like
2438   *              a connection pool) that should be used to communicate with the
2439   *              directory server.  It must not be {@code null}.
2440   * @param  rootDSE
2441   *              A reference to the server root DSE, if it has already been
2442   *              retrieved.  It must not be {@code null}, but may be
2443   *              unassigned.
2444   *
2445   * @return  The server's root DSE, or {@code null} if it could not be
2446   *          retrieved.
2447   */
2448  private static RootDSE getRootDSE(final LDAPInterface connection,
2449                                    final AtomicReference<RootDSE> rootDSE)
2450  {
2451    final RootDSE dse = rootDSE.get();
2452    if (dse != null)
2453    {
2454      return dse;
2455    }
2456
2457    try
2458    {
2459      return connection.getRootDSE();
2460    }
2461    catch (final Exception e)
2462    {
2463      Debug.debugException(e);
2464      return null;
2465    }
2466  }
2467
2468
2469
2470  /**
2471   * Retrieves a string representation of this subtree deleter.
2472   *
2473   * @return  A string representation of this subtree deleter.
2474   */
2475  @Override()
2476  public String toString()
2477  {
2478    final StringBuilder buffer = new StringBuilder();
2479    toString(buffer);
2480    return buffer.toString();
2481  }
2482
2483
2484
2485  /**
2486   * Appends a string representation of this subtree deleter to the provided
2487   * buffer.
2488   *
2489   * @param  buffer  The buffer to which the string representation should be
2490   *                 appended.
2491   */
2492  public void toString(final StringBuilder buffer)
2493  {
2494    buffer.append("SubtreeDeleter(deleteBaseEntry=");
2495    buffer.append(deleteBaseEntry);
2496    buffer.append(", useSetSubtreeAccessibilityOperationIfAvailable=");
2497    buffer.append(useSetSubtreeAccessibilityOperationIfAvailable);
2498
2499    if (useSimplePagedResultsControlIfAvailable)
2500    {
2501      buffer.append(
2502           ", useSimplePagedResultsControlIfAvailable=true, pageSize=");
2503      buffer.append(simplePagedResultsPageSize);
2504    }
2505    else
2506    {
2507      buffer.append(", useSimplePagedResultsControlIfAvailable=false");
2508    }
2509
2510    buffer.append(", useManageDSAITControlIfAvailable=");
2511    buffer.append(useManageDSAITControlIfAvailable);
2512    buffer.append(", usePermitUnindexedSearchControlIfAvailable=");
2513    buffer.append(usePermitUnindexedSearchControlIfAvailable);
2514    buffer.append(", useSubentriesControlIfAvailable=");
2515    buffer.append(useSubentriesControlIfAvailable);
2516    buffer.append(", useReturnConflictEntriesRequestControlIfAvailable=");
2517    buffer.append(useReturnConflictEntriesRequestControlIfAvailable);
2518    buffer.append(", useSoftDeletedEntryAccessControlIfAvailable=");
2519    buffer.append(useSoftDeletedEntryAccessControlIfAvailable);
2520    buffer.append(", useHardDeleteControlIfAvailable=");
2521    buffer.append(useHardDeleteControlIfAvailable);
2522
2523    buffer.append(", additionalSearchControls={ ");
2524    final Iterator<Control> searchControlIterator =
2525         additionalSearchControls.iterator();
2526    while (searchControlIterator.hasNext())
2527    {
2528      buffer.append(searchControlIterator.next());
2529      if (searchControlIterator.hasNext())
2530      {
2531        buffer.append(',');
2532      }
2533      buffer.append(' ');
2534    }
2535
2536    buffer.append("}, additionalDeleteControls={");
2537    final Iterator<Control> deleteControlIterator =
2538         additionalSearchControls.iterator();
2539    while (deleteControlIterator.hasNext())
2540    {
2541      buffer.append(deleteControlIterator.next());
2542      if (deleteControlIterator.hasNext())
2543      {
2544        buffer.append(',');
2545      }
2546      buffer.append(' ');
2547    }
2548
2549    buffer.append("}, searchRequestSizeLimit=");
2550    buffer.append(searchRequestSizeLimit);
2551    buffer.append(')');
2552  }
2553}