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