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