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