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