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