All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.util.SubtreeDeleter Maven / Gradle / Ivy

Go to download

The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use Java API for communicating with LDAP directory servers and performing related tasks like reading and writing LDIF, encoding and decoding data using base64 and ASN.1 BER, and performing secure communication. This package contains the Standard Edition of the LDAP SDK, which is a complete, general-purpose library for communicating with LDAPv3 directory servers.

The newest version!
/*
 * Copyright 2019-2024 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2019-2024 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2019-2024 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.util;



import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.ldap.sdk.AbstractConnectionPool;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.DeleteRequest;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.ExtendedRequest;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPInterface;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.RootDSE;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.controls.DraftLDUPSubentriesRequestControl;
import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedRequest;
import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedResult;
import com.unboundid.ldap.sdk.unboundidds.controls.HardDeleteRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            PermitUnindexedSearchRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            ReturnConflictEntriesRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            SoftDeletedEntryAccessRequestControl;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            SetSubtreeAccessibilityExtendedRequest;

import static com.unboundid.util.UtilityMessages.*;



/**
 * This class provides a utility that can delete all entries below a specified
 * base DN (including the base entry itself by default, although it can be
 * preserved if desired) in an LDAP directory server.  It accomplishes this
 * through a combination of search and delete operations.  Ideally, it will
 * first perform a search to find all entries below the target base DN, but in
 * some cases, it may be necessary to intertwine search and delete operations
 * if it is not possible to retrieve all entries in the target subtree in
 * advance.
 * 

* The subtree deleter can optionally take advantage of a number of server * features to aid in processing, but does not require them. Some of these * features include: *
    *
  • * Set Subtree Accessibility Extended Operation -- A proprietary extended * operation supported by the Ping Identity, UnboundID, and * Nokia/Alcatel-Lucent 8661 Directory Server products. This operation can * restrict access to a specified subtree to all but a specified user. If * this is to be used, then the "Who Am I?" extended operation will first be * used to identify the user that is authenticated on the provided * connection, and then the set subtree accessibility extended operation * will be used to make the target subtree hidden and read-only for all * users except the user identified by the "Who Am I?" operation. As far as * all other clients are concerned, this will make the target subtree * immediately disappear. The subtree deleter will then be able to search * for the entries to delete, and then delete those entries, without * exposing other clients to its in-progress state. *

    * The set subtree accessibility extended operation will not automatically * be used. If the * {@link #setUseSetSubtreeAccessibilityOperationIfAvailable} method is * called with a value of {@code true}, then this extended operation will be * used if the server root DSE advertises support for both this operation * and the LDAP "Who Am I?" extended operation. *

    *
  • *
  • * Simple Paged Results Request Control -- A standard request control that * is supported by several types of directory servers. This control allows * a search to be broken up into pages to limit the number of entries that * are returned in any single operation (which can help an authorized * client circumvent search size limit restrictions). It can also help * ensure that if the server can return entries faster than the client can * consume them, it will not result in a large backlog on the server. *

    * The simple paged results request control will be used by default if the * server root DSE advertises support for it, with a default page size of * 100 entries. *

    *
  • *
  • * Manage DSA IT Request Control -- A standard request control that is * supported by several types of directory servers. This control indicates * that any referral entries (that is, entries that contain the "referral" * object class and a "ref" attribute) should be treated as regular entries * rather than triggering a referral result or a search result reference. * The subtree deleter will not make any attempt to follow referrals, and * if any referral or search result reference results are returned during * processing, then it may not be possible to completely remove all entries * in the target subtree. *

    * The manage DSA IT request control will be used by default if the server * root DSE advertises support for it. *

    *
  • *
  • * Permit Unindexed Search Request Control -- A proprietary request * control supported by the Ping Identity, UnboundID, and * Nokia/Alcatel-Lucent 8661 Directory Server products. This control * indicates that the client wishes to process the search even if it is * unindexed. *

    * The permit unindexed search request control will not automatically be * used. It may not needed if the requester has the unindexed-search * privilege, and the permit unindexed search request control requires that * the caller have either the unindexed-search or * unindexed-search-with-control privilege. If the * {@link #setUsePermitUnindexedSearchControlIfAvailable} method is called * with a value of {@code true}, then this control will be used if the * server root DSE advertises support for it. *

    *
  • *
  • * LDAP Subentries Request Control -- A standard request control that is * supported by several types of directory servers. It allows the client * to request a search that retrieves entries with the "ldapSubentry" * object class, which are normally excluded from search results. Note that * because of the nature of this control, if it is to be used, then two * separate sets of searches will be used: one that retrieves only * LDAP subentries, and a second that retrieves other types of entries. *

    * The LDAP subentries request control will be used by default if the server * root DSE advertises support for it. *

    *
  • *
  • * Return Conflict Entries Request Control -- A proprietary request control * that is supported by the Ping Identity, UnboundID, and * Nokia/Alcatel-Lucent 8661 Directory Server products. This control * indicates that the server should return replication conflict entries, * which are normally excluded from search results. *

    * The return conflict entries request control will be used by default if * the server root DSE advertises support for it. *

    *
  • *
  • * Soft-Deleted Entry Access Request Control -- A proprietary request * control that is supported by the Ping Identity, UnboundID, and * Nokia/Alcatel-Lucent 8661 Directory Server products. This control * indicates that the server should return soft-deleted entries, which are * normally excluded from search results. *

    * The soft-deleted entry access request control will be used by default if * the server root DSE advertises support for it. *

    *
  • * Hard Delete Request Control -- A proprietary request control that is * supported by the Ping Identity, UnboundID, and Nokia/Alcatel-Lucent 8661 * Directory Server products. This control indicates that the server * should process a delete operation as a hard delete, even if a * soft-delete policy would have otherwise converted it into a soft delete. * A subtree cannot be deleted if it contains soft-deleted entries, so this * should be used if the server is configured with such a soft-delete * policy. *

    * The hard delete request control will be used by default if the server * root DSE advertises support for it. *

    *
  • *
*/ @Mutable() @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) public final class SubtreeDeleter { // Indicates whether to delete the base entry itself, or only its // subordinates. private boolean deleteBaseEntry = true; // Indicates whether to include the hard delete request control in delete // requests, if the server root DSE advertises support for it. private boolean useHardDeleteControlIfAvailable = true; // Indicates whether to include the manage DSA IT request control in search // and delete requests, if the server root DSE advertises support for it. private boolean useManageDSAITControlIfAvailable = true; // Indicates whether to include the permit unindexed search request control in // search requests, if the server root DSE advertises support for it. private boolean usePermitUnindexedSearchControlIfAvailable = false; // Indicates whether to include the return conflict entries request control // in search requests, if the server root DSE advertises support for it. private boolean useReturnConflictEntriesRequestControlIfAvailable = true; // Indicates whether to use the simple paged results control in the course of // finding the entries to delete, if the server root DSE advertises support // for it. private boolean useSimplePagedResultsControlIfAvailable = true; // Indicates whether to include the soft-deleted entry access request control // in search requests, if the server root DSE advertises support for it. private boolean useSoftDeletedEntryAccessControlIfAvailable = true; // Indicates whether to use the subentries request control to search for LDAP // subentries if the server root DSE advertises support for it. private boolean useSubentriesControlIfAvailable = true; // Indicates whether to use the set subtree accessibility extended operation // to made the target subtree inaccessible, if the server root DSE advertises // support for it. private boolean useSetSubtreeAccessibilityOperationIfAvailable = false; // The maximum number of entries to return from any single search operation. private int searchRequestSizeLimit = 0; // The page size to use in conjunction with the simple paged results request // control. private int simplePagedResultsPageSize = 100; // The fixed-rate barrier that will be used to limit the rate at which delete // operations will be attempted. @Nullable private FixedRateBarrier deleteRateLimiter = null; // A list of additional controls that should be included in search requests // used to find the entries to delete. @NotNull private List additionalSearchControls = Collections.emptyList(); // A list of additional controls that should be included in delete requests // used to @NotNull private List additionalDeleteControls = Collections.emptyList(); /** * Creates a new instance of this subtree deleter with the default settings. */ public SubtreeDeleter() { // No implementation is required. } /** * Indicates whether the base entry itself should be deleted along with all of * its subordinates. This method returns {@code true} by default. * * @return {@code true} if the base entry should be deleted in addition to * its subordinates, or {@code false} if the base entry should not * be deleted but all of its subordinates should be. */ public boolean deleteBaseEntry() { return deleteBaseEntry; } /** * Specifies whether the base entry itself should be deleted along with all of * its subordinates. * * @param deleteBaseEntry * {@code true} to indicate that the base entry should be deleted * in addition to its subordinates, or {@code false} if only the * subordinates of the base entry should be removed. */ public void setDeleteBaseEntry(final boolean deleteBaseEntry) { this.deleteBaseEntry = deleteBaseEntry; } /** * Indicates whether to use the {@link SetSubtreeAccessibilityExtendedRequest} * to make the target subtree hidden before starting to search for entries to * delete if the server root DSE advertises support for both that extended * request and the "Who Am I?" extended request. In servers that support it, * this extended operation can make the target subtree hidden and read-only to * clients other than those authenticated as the user that issued the set * subtree accessibility request. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports both the set subtree * accessibility extended operation and the "Who Am I?" extended operation. * * @return {@code true} if the set subtree accessibility extended operation * should be used to make the target subtree hidden and read-only * before attempting to search for entries to delete if the server * root DSE advertises support for it, or {@code false} if the * operation should not be used. */ public boolean useSetSubtreeAccessibilityOperationIfAvailable() { return useSetSubtreeAccessibilityOperationIfAvailable; } /** * Specifies whether to use the {@link SetSubtreeAccessibilityExtendedRequest} * to make the target subtree hidden before starting to search for entries to * delete if the server root DSE advertises support for both that extended * request and the "Who Am I?" extended request. In servers that support it, * this extended operation can make the target subtree hidden and read-only to * clients other than those authenticated as the user that issued the set * subtree accessibility request. * * @param useSetSubtreeAccessibilityOperationIfAvailable * {@code true} to indicate that the set subtree accessibility * extended operation should be used to make the target subtree * hidden and read-only before starting to search for entries * to delete, or {@code false} if not. This value will be * ignored if the server root DSE does not advertise support for * both the set subtree accessibility extended operation and the * "Who Am I?" extended operation. */ public void setUseSetSubtreeAccessibilityOperationIfAvailable( final boolean useSetSubtreeAccessibilityOperationIfAvailable) { this.useSetSubtreeAccessibilityOperationIfAvailable = useSetSubtreeAccessibilityOperationIfAvailable; } /** * Indicates whether to use the {@link SimplePagedResultsControl} when * searching for entries to delete if the server advertises support for it. * Using this control can help avoid problems from running into the search * size limit, and can also prevent the server from trying to return entries * faster than the client can consume them. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the simple paged * results control. * * @return {@code true} if the simple paged results control should be used * when searching for entries to delete if the server root DSE * advertises support for it, or {@code false} if the control should * not be used. */ public boolean useSimplePagedResultsControlIfAvailable() { return useSimplePagedResultsControlIfAvailable; } /** * Specifies whether to use the {@link SimplePagedResultsControl} when * searching for entries to delete if the server advertises support for it. * Using this control can help avoid problems from running into the search * size limit, and can also prevent the server from trying to return entries * faster than the client can consume them. * * @param useSimplePagedResultsControlIfAvailable * {@code true} to indicate that the simple paged results control * should be used when searching for entries to delete, or * {@code false} if not. This value will be ignored if the * server root DSE does not advertise support for the simple * paged results control. */ public void setUseSimplePagedResultsControlIfAvailable( final boolean useSimplePagedResultsControlIfAvailable) { this.useSimplePagedResultsControlIfAvailable = useSimplePagedResultsControlIfAvailable; } /** * Retrieves the maximum number of entries that should be returned in each * page of results when using the simple paged results control. This value * will only be used if {@link #useSimplePagedResultsControlIfAvailable()} * returns {@code true} and the server root DSE indicates that it supports the * simple paged results control. *

* This method returns {@code 100} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the simple paged * results control. * * @return The maximum number of entries that should be returned in each page * of results when using the simple paged results control. */ public int getSimplePagedResultsPageSize() { return simplePagedResultsPageSize; } /** * Specifies the maximum number of entries that should be returned in each * page of results when using the simple paged results control. This value * will only be used if {@link #useSimplePagedResultsControlIfAvailable()} * returns {@code true} and the server root DSE indicates that it supports the * simple paged results control. * * @param simplePagedResultsPageSize * The maximum number of entries that should be returned in each * page of results when using the simple paged results control. * The value must be greater than or equal to one. */ public void setSimplePagedResultsPageSize( final int simplePagedResultsPageSize) { Validator.ensureTrue((simplePagedResultsPageSize >= 1), "SubtreeDeleter.simplePagedResultsPageSize must be greater than " + "or equal to 1."); this.simplePagedResultsPageSize = simplePagedResultsPageSize; } /** * Indicates whether to include the {@link ManageDsaITRequestControl} in * search and delete requests if the server root DSE advertises support for * it. The manage DSA IT request control tells the server that it should * return referral entries as regular entries rather than returning them as * search result references when processing a search operation, or returning a * referral result when attempting a delete. If any referrals are * encountered during processing and this control is not used, then it may * not be possible to completely delete the entire subtree. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the manage DSA IT * request control. * * @return {@code true} if the manage DSA IT request control should be * included in search and delete requests if the server root DSE * advertises support for it, or {@code false} if not. */ public boolean useManageDSAITControlIfAvailable() { return useManageDSAITControlIfAvailable; } /** * Specifies whether to include the {@link ManageDsaITRequestControl} in * search and delete requests if the server root DSE advertises support for * it. The manage DSA IT request control tells the server that it should * return referral entries as regular entries rather than returning them as * search result references when processing a search operation, or returning a * referral result when attempting a delete. If any referrals are * encountered during processing and this control is not used, then it may * not be possible to completely delete the entire subtree. * * @param useManageDSAITControlIfAvailable * {@code true} to indicate that the manage DSA IT request * control should be included in search and delete requests, * or {@code false} if not. This value will be ignored if the * server root DSE does not advertise support for the manage DSA * IT request control. */ public void setUseManageDSAITControlIfAvailable( final boolean useManageDSAITControlIfAvailable) { this.useManageDSAITControlIfAvailable = useManageDSAITControlIfAvailable; } /** * Indicates whether to include the * {@link PermitUnindexedSearchRequestControl} in search requests used to * identify the entries to be deleted if the server root DSE advertises * support for it. The permit unindexed search request control may allow * appropriately authorized clients to explicitly indicate that the server * should process an unindexed search that would normally be rejected. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the permit unindexed * search request control. * * @return {@code true} if search requests should include the permit * unindexed search request control if the server root DSE advertises * support for it, or {@code false} if not. */ public boolean usePermitUnindexedSearchControlIfAvailable() { return usePermitUnindexedSearchControlIfAvailable; } /** * Specifies whether to include the * {@link PermitUnindexedSearchRequestControl} in search request used to * identify the entries to be deleted if the server root DSE advertises * support for it. The permit unindexed search request control may allow * appropriately authorized clients to explicitly indicate that the server * should process an unindexed search that would normally be rejected. * * @param usePermitUnindexedSearchControlIfAvailable * {@code true} to indicate that the permit unindexed search * request control should be included in search requests, or * {@code false} if not. This value will be ignored if the * server root DSE does not advertise support for the permit * unindexed search request control. */ public void setUsePermitUnindexedSearchControlIfAvailable( final boolean usePermitUnindexedSearchControlIfAvailable) { this.usePermitUnindexedSearchControlIfAvailable = usePermitUnindexedSearchControlIfAvailable; } /** * Indicates whether to use the {@link DraftLDUPSubentriesRequestControl} when * searching for entries to delete if the server root DSE advertises support * for it. The subentries request control allows LDAP subentries to be * included in search results. These entries are normally excluded from * search results. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the subentries * request control. * * @return {@code true} if the subentries request control should be used * to retrieve LDAP subentries if the server root DSE advertises * support for it, or {@code false} if not. */ public boolean useSubentriesControlIfAvailable() { return useSubentriesControlIfAvailable; } /** * Specifies whether to use the {@link DraftLDUPSubentriesRequestControl} when * searching for entries to delete if the server root DSE advertises support * for it. The subentries request control allows LDAP subentries to be * included in search results. These entries are normally excluded from * search results. * * @param useSubentriesControlIfAvailable * [@code true} to indicate that the subentries request control * should be used to retrieve LDAP subentries, or {@code false} * if not. This value will be ignored if the server root DSE * does not advertise support for the subentries request * control. */ public void setUseSubentriesControlIfAvailable( final boolean useSubentriesControlIfAvailable) { this.useSubentriesControlIfAvailable = useSubentriesControlIfAvailable; } /** * Indicates whether to use the {@link ReturnConflictEntriesRequestControl} * when searching for entries to delete if the server root DSE advertises * support for it. The return conflict entries request control allows * replication conflict entries to be included in search results. These * entries are normally excluded from search results. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the return * conflict entries request control. * * @return {@code true} if the return conflict entries request control * should be used to retrieve replication conflict entries if the * server root DSE advertises support for it, or {@code false} if * not. */ public boolean useReturnConflictEntriesRequestControlIfAvailable() { return useReturnConflictEntriesRequestControlIfAvailable; } /** * Specifies whether to use the {@link ReturnConflictEntriesRequestControl} * when searching for entries to delete if the server root DSE advertises * support for it. The return conflict entries request control allows * replication conflict entries to be included in search results. These * entries are normally excluded from search results. * * @param useReturnConflictEntriesRequestControlIfAvailable * {@code true} to indicate that the return conflict entries * request control should be used to retrieve replication * conflict entries, or {@code false} if not. This value will be * ignored if the server root DSE does not advertise support for * the return conflict entries request control. */ public void setUseReturnConflictEntriesRequestControlIfAvailable( final boolean useReturnConflictEntriesRequestControlIfAvailable) { this.useReturnConflictEntriesRequestControlIfAvailable = useReturnConflictEntriesRequestControlIfAvailable; } /** * Indicates whether to use the {@link SoftDeletedEntryAccessRequestControl} * when searching for entries to delete if the server root DSE advertises * support for it. The soft-deleted entry access request control allows * soft-deleted entries to be included in search results. These entries are * normally excluded from search results. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the soft-deleted * entry access request control. * * @return {@code true} if the soft-deleted entry access request control * should be used to retrieve soft-deleted entries if the server * root DSE advertises support for it, or {@code false} if not. */ public boolean useSoftDeletedEntryAccessControlIfAvailable() { return useSoftDeletedEntryAccessControlIfAvailable; } /** * Specifies whether to use the {@link SoftDeletedEntryAccessRequestControl} * when searching for entries to delete if the server root DSE advertises * support for it. The soft-deleted entry access request control allows * soft-deleted entries to be included in search results. These entries are * normally excluded from search results. * * @param useSoftDeletedEntryAccessControlIfAvailable * {@code true} to indicate that the soft-deleted entry access * request control should be used to retrieve soft-deleted * entries, or {@code false} if not. This value will be ignored * if the server root DSE does not advertise support for the * soft-deleted entry access request control. */ public void setUseSoftDeletedEntryAccessControlIfAvailable( final boolean useSoftDeletedEntryAccessControlIfAvailable) { this.useSoftDeletedEntryAccessControlIfAvailable = useSoftDeletedEntryAccessControlIfAvailable; } /** * Indicates whether to include the {@link HardDeleteRequestControl} in * delete requests if the server root DSE advertises support for it. The * hard delete request control indicates that the server should treat a delete * operation as a hard delete even if it would have normally been processed as * a soft delete because it matches the criteria in a configured soft delete * policy. *

* This method returns {@code true} by default. Its value will be ignored if * the server root DSE does not indicate that it supports the hard delete * request control. * * @return {@code true} if the hard delete request control should be included * in delete requests if the server root DSE advertises support for * it, or {@code false} if not. */ public boolean useHardDeleteControlIfAvailable() { return useHardDeleteControlIfAvailable; } /** * Specifies whether to include the {@link HardDeleteRequestControl} in * delete requests if the server root DSE advertises support for it. The * hard delete request control indicates that the server should treat a delete * operation as a hard delete even if it would have normally been processed as * a soft delete because it matches the criteria in a configured soft delete * policy. * * @param useHardDeleteControlIfAvailable * {@code true} to indicate that the hard delete request control * should be included in delete requests, or {@code false} if * not. This value will be ignored if the server root DSE does * not advertise support for the hard delete request control. */ public void setUseHardDeleteControlIfAvailable( final boolean useHardDeleteControlIfAvailable) { this.useHardDeleteControlIfAvailable = useHardDeleteControlIfAvailable; } /** * Retrieves an unmodifiable list of additional controls that should be * included in search requests used to identify entries to delete. *

* This method returns an empty list by default. * * @return An unmodifiable list of additional controls that should be * included in search requests used to identify entries to delete. */ @NotNull() public List getAdditionalSearchControls() { return additionalSearchControls; } /** * Specifies a list of additional controls that should be included in search * requests used to identify entries to delete. * * @param additionalSearchControls * A list of additional controls that should be included in * search requests used to identify entries to delete. This must * not be {@code null} but may be empty. */ public void setAdditionalSearchControls( @NotNull final Control... additionalSearchControls) { setAdditionalSearchControls(Arrays.asList(additionalSearchControls)); } /** * Specifies a list of additional controls that should be included in search * requests used to identify entries to delete. * * @param additionalSearchControls * A list of additional controls that should be included in * search requests used to identify entries to delete. This must * not be {@code null} but may be empty. */ public void setAdditionalSearchControls( @NotNull final List additionalSearchControls) { this.additionalSearchControls = Collections.unmodifiableList( new ArrayList<>(additionalSearchControls)); } /** * Retrieves an unmodifiable list of additional controls that should be * included in delete requests. *

* This method returns an empty list by default. * * @return An unmodifiable list of additional controls that should be * included in delete requests. */ @NotNull() public List getAdditionalDeleteControls() { return additionalDeleteControls; } /** * Specifies a list of additional controls that should be included in delete * requests. * * @param additionalDeleteControls * A list of additional controls that should be included in * delete requests. This must not be {@code null} but may be * empty. */ public void setAdditionalDeleteControls( @NotNull final Control... additionalDeleteControls) { setAdditionalDeleteControls(Arrays.asList(additionalDeleteControls)); } /** * Specifies a list of additional controls that should be included in delete * requests. * * @param additionalDeleteControls * A list of additional controls that should be included in * delete requests. This must not be {@code null} but may be * empty. */ public void setAdditionalDeleteControls( @NotNull final List additionalDeleteControls) { this.additionalDeleteControls = Collections.unmodifiableList( new ArrayList<>(additionalDeleteControls)); } /** * Retrieves the size limit that should be used in each search request to * specify the maximum number of entries to return in response to that * request. If a search request matches more than this number of entries, * then the server may return a subset of the results and a search result * done message with a result code of {@link ResultCode#SIZE_LIMIT_EXCEEDED}. *

* This method returns a value of zero by default, which indicates that the * client does not want to impose any limit on the number of entries that may * be returned in response to any single search operation (although the server * may still impose a limit). * * @return The size limit that should be used in each search request to * specify the maximum number of entries to return in response to * that request, or zero to indicate that the client does not want to * impose any size limit. */ public int getSearchRequestSizeLimit() { return searchRequestSizeLimit; } /** * Specifies the size limit that should be used in each search request to * specify the maximum number of entries to return in response to that * request. If a search request matches more than this number of entries, * then the server may return a subset of the results and a search result * done message with a result code of {@link ResultCode#SIZE_LIMIT_EXCEEDED}. * A value that is less than or equal to zero indicates that the client does * not want to impose any limit on the number of entries that may be returned * in response to any single search operation (although the server may still * impose a limit). * * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. */ public void setSearchRequestSizeLimit(final int searchRequestSizeLimit) { if (searchRequestSizeLimit <= 0) { this.searchRequestSizeLimit = 0; } else { this.searchRequestSizeLimit = searchRequestSizeLimit; } } /** * Retrieves the fixed-rate barrier that may be used to impose a rate limit on * delete operations, if defined. *

* This method returns {@code null} by default, to indicate that no delete * rate limit will be imposed. * * @return The fixed-rate barrier that may be used to impose a rate limit on * delete operations, or {@code null} if no rate limit should be * imposed. */ @Nullable() public FixedRateBarrier getDeleteRateLimiter() { return deleteRateLimiter; } /** * Provides a fixed-rate barrier that may be used to impose a rate limit on * delete operations. * * @param deleteRateLimiter * A fixed-rate barrier that may be used to impose a rate limit * on delete operations. It may be {@code null} if no delete * rate limit should be imposed. */ public void setDeleteRateLimiter( @Nullable final FixedRateBarrier deleteRateLimiter) { this.deleteRateLimiter = deleteRateLimiter; } /** * Attempts to delete the specified subtree using the current settings. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to delete. It must not be * {@code null}. * * @return An object with information about the results of the subtree * delete processing. * * @throws LDAPException If the provided base DN cannot be parsed as a valid * DN. */ @NotNull() public SubtreeDeleterResult delete(@NotNull final LDAPInterface connection, @NotNull final String baseDN) throws LDAPException { return delete(connection, new DN(baseDN)); } /** * Attempts to delete the specified subtree using the current settings. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to delete. It must not be * {@code null}. * * @return An object with information about the results of the subtree * delete processing. */ @NotNull() public SubtreeDeleterResult delete(@NotNull final LDAPInterface connection, @NotNull final DN baseDN) { final AtomicReference rootDSE = new AtomicReference<>(); final boolean useSetSubtreeAccessibility = useSetSubtreeAccessibilityOperationIfAvailable && supportsExtendedRequest(connection, rootDSE, SetSubtreeAccessibilityExtendedRequest. SET_SUBTREE_ACCESSIBILITY_REQUEST_OID) && supportsExtendedRequest(connection, rootDSE, WhoAmIExtendedRequest.WHO_AM_I_REQUEST_OID); final boolean usePagedResults = useSimplePagedResultsControlIfAvailable && supportsControl(connection, rootDSE, SimplePagedResultsControl.PAGED_RESULTS_OID); final boolean useSubentries = useSubentriesControlIfAvailable && supportsControl(connection, rootDSE, DraftLDUPSubentriesRequestControl.SUBENTRIES_REQUEST_OID); final List searchControls = new ArrayList<>(10); searchControls.addAll(additionalSearchControls); final List deleteControls = new ArrayList<>(10); deleteControls.addAll(additionalDeleteControls); if (useHardDeleteControlIfAvailable && supportsControl(connection, rootDSE, HardDeleteRequestControl.HARD_DELETE_REQUEST_OID)) { deleteControls.add(new HardDeleteRequestControl(false)); } if (useManageDSAITControlIfAvailable && supportsControl(connection, rootDSE, ManageDsaITRequestControl.MANAGE_DSA_IT_REQUEST_OID)) { final ManageDsaITRequestControl c = new ManageDsaITRequestControl(false); searchControls.add(c); deleteControls.add(c); } if (usePermitUnindexedSearchControlIfAvailable && supportsControl(connection, rootDSE, PermitUnindexedSearchRequestControl. PERMIT_UNINDEXED_SEARCH_REQUEST_OID)) { searchControls.add(new PermitUnindexedSearchRequestControl(false)); } if (useReturnConflictEntriesRequestControlIfAvailable && supportsControl(connection, rootDSE, ReturnConflictEntriesRequestControl. RETURN_CONFLICT_ENTRIES_REQUEST_OID)) { searchControls.add(new ReturnConflictEntriesRequestControl(false)); } if (useSoftDeletedEntryAccessControlIfAvailable && supportsControl(connection, rootDSE, SoftDeletedEntryAccessRequestControl. SOFT_DELETED_ENTRY_ACCESS_REQUEST_OID)) { searchControls.add(new SoftDeletedEntryAccessRequestControl(false, true, false)); } return delete(connection, baseDN, deleteBaseEntry, useSetSubtreeAccessibility, usePagedResults, searchRequestSizeLimit, simplePagedResultsPageSize, useSubentries, searchControls, deleteControls, deleteRateLimiter); } /** * Attempts to delete the specified subtree using the current settings. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to delete. It must not be * {@code null}. * @param deleteBaseEntry * Indicates whether the base entry itself should be deleted * along with its subordinates (if {@code true}), or if only the * subordinates of the base entry should be deleted but the base * entry itself should remain (if {@code false}). * @param useSetSubtreeAccessibilityOperation * Indicates whether to use the * {@link SetSubtreeAccessibilityExtendedRequest} to make the * target subtree hidden and read-only before beginning to search * for entries to delete. * @param useSimplePagedResultsControl * Indicates whether to use the {@link SimplePagedResultsControl} * when searching for entries to delete. * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. * @param pageSize * The page size for the simple paged results request control, if * it is to be used. * @param useSubentriesControl * Indicates whether to look for LDAP subentries when searching * for entries to delete. * @param searchControls * A list of controls that should be included in search requests * used to find the entries to delete. This must not be * {@code null} but may be empty. * @param deleteControls * A list of controls that should be included in delete requests. * This must not be {@code null} but may be empty. * @param deleteRateLimiter * A fixed-rate barrier used to impose a rate limit on delete * operations. This may be {@code null} if no rate limit should * be imposed. * * @return An object with information about the results of the subtree * delete processing. */ @NotNull() private static SubtreeDeleterResult delete( @NotNull final LDAPInterface connection, @NotNull final DN baseDN, final boolean deleteBaseEntry, final boolean useSetSubtreeAccessibilityOperation, final boolean useSimplePagedResultsControl, final int searchRequestSizeLimit, final int pageSize, final boolean useSubentriesControl, @NotNull final List searchControls, @NotNull final List deleteControls, @Nullable final FixedRateBarrier deleteRateLimiter) { if (useSetSubtreeAccessibilityOperation) { final ExtendedResult setInaccessibleResult = setInaccessible(connection, baseDN); if (setInaccessibleResult != null) { return new SubtreeDeleterResult(setInaccessibleResult, false, null, 0L, new TreeMap()); } } final SubtreeDeleterResult result; if (useSimplePagedResultsControl) { result = deleteEntriesWithSimplePagedResults(connection, baseDN, deleteBaseEntry, searchRequestSizeLimit, pageSize, useSubentriesControl, searchControls, deleteControls, deleteRateLimiter); } else { result = deleteEntriesWithoutSimplePagedResults(connection, baseDN, deleteBaseEntry, searchRequestSizeLimit, useSubentriesControl, searchControls, deleteControls, deleteRateLimiter); } if (result.completelySuccessful() && useSetSubtreeAccessibilityOperation) { final ExtendedResult removeAccessibilityRestrictionResult = removeAccessibilityRestriction(connection, baseDN); if (removeAccessibilityRestrictionResult.getResultCode() == ResultCode.SUCCESS) { return new SubtreeDeleterResult(null, false, null, result.getEntriesDeleted(), result.getDeleteErrorsTreeMap()); } else { return new SubtreeDeleterResult(removeAccessibilityRestrictionResult, true, null, result.getEntriesDeleted(), result.getDeleteErrorsTreeMap()); } } else { return new SubtreeDeleterResult(null, useSetSubtreeAccessibilityOperation, result.getSearchError(), result.getEntriesDeleted(), result.getDeleteErrorsTreeMap()); } } /** * Marks the specified subtree as inaccessible. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to make inaccessible. It must not * be {@code null}. * * @return An {@code LDAPResult} with information about a failure that * occurred while trying to make the subtree inaccessible, or * {@code null} if the subtree was successfully made inaccessible. */ @Nullable() private static ExtendedResult setInaccessible( @NotNull final LDAPInterface connection, @NotNull final DN baseDN) { // Use the "Who Am I?" extended operation to get the authorization identity // of the provided connection. final ExtendedResult genericWhoAmIResult = processExtendedOperation( connection, new WhoAmIExtendedRequest()); if (genericWhoAmIResult.getResultCode() != ResultCode.SUCCESS) { return genericWhoAmIResult; } final WhoAmIExtendedResult whoAmIResult = (WhoAmIExtendedResult) genericWhoAmIResult; // Extract the user DN from the "Who Am I?" result's authorization ID. final String authzDN; final String authzID = whoAmIResult.getAuthorizationID(); if (authzID.startsWith("dn:")) { authzDN = authzID.substring(3); } else { return new ExtendedResult(-1, ResultCode.LOCAL_ERROR, ERR_SUBTREE_DELETER_INTERFACE_WHO_AM_I_AUTHZ_ID_NOT_DN.get( authzID), null, StaticUtils.NO_STRINGS, null, null, StaticUtils.NO_CONTROLS); } // Use the set subtree accessibility extended operation to make the target // subtree hidden and read-only. final ExtendedResult setInaccessibleResult = processExtendedOperation( connection, SetSubtreeAccessibilityExtendedRequest.createSetHiddenRequest( baseDN.toString(), authzDN)); if (setInaccessibleResult.getResultCode() == ResultCode.SUCCESS) { return null; } else { return setInaccessibleResult; } } /** * Deletes the specified subtree with the given settings. The simple paged * results control will be used in the course of searching for entries to * delete. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to delete. It must not be * {@code null}. * @param deleteBaseEntry * Indicates whether the base entry itself should be deleted * along with its subordinates (if {@code true}), or if only the * subordinates of the base entry should be deleted but the base * entry itself should remain (if {@code false}). * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. * @param pageSize * The page size for the simple paged results request control, if * it is to be used. * @param useSubentriesControl * Indicates whether to look for LDAP subentries when searching * for entries to delete. * @param searchControls * A list of controls that should be included in search requests * used to find the entries to delete. This must not be * {@code null} but may be empty. * @param deleteControls * A list of controls that should be included in delete requests. * This must not be {@code null} but may be empty. * @param deleteRateLimiter * A fixed-rate barrier used to impose a rate limit on delete * operations. This may be {@code null} if no rate limit should * be imposed. * * @return An object with information about the results of the subtree * delete processing. */ @NotNull() private static SubtreeDeleterResult deleteEntriesWithSimplePagedResults( @NotNull final LDAPInterface connection, @NotNull final DN baseDN, final boolean deleteBaseEntry, final int searchRequestSizeLimit, final int pageSize, final boolean useSubentriesControl, @NotNull final List searchControls, @NotNull final List deleteControls, @Nullable final FixedRateBarrier deleteRateLimiter) { // If we should use the subentries control, then first search to find all // subentries in the subtree. final TreeSet dnsToDelete = new TreeSet<>(); if (useSubentriesControl) { try { final SearchRequest searchRequest = createSubentriesSearchRequest( baseDN, 0, searchControls, dnsToDelete); doPagedResultsSearch(connection, searchRequest, pageSize); } catch (final LDAPSearchException e) { Debug.debugException(e); return new SubtreeDeleterResult(null, false, e.getSearchResult(), 0L, new TreeMap()); } } // Perform a paged search to find all all entries (except subentries) in the // target subtree. try { final SearchRequest searchRequest = createNonSubentriesSearchRequest( baseDN, 0, searchControls, dnsToDelete); doPagedResultsSearch(connection, searchRequest, pageSize); } catch (final LDAPSearchException e) { Debug.debugException(e); return new SubtreeDeleterResult(null, false, e.getSearchResult(), 0L, new TreeMap()); } // If we should not delete the base entry, then remove it from the set of // DNs to delete. if (! deleteBaseEntry) { dnsToDelete.remove(baseDN); } // Iterate through the DNs in reverse order and start deleting. If we // encounter any entry that can't be deleted, then remove all of its // ancestors from the set of DNs to delete and create delete errors for // them. final AtomicReference searchError = new AtomicReference<>(); final AtomicLong entriesDeleted = new AtomicLong(0L); final TreeMap deleteErrors = new TreeMap<>(); final Iterator iterator = dnsToDelete.descendingIterator(); while (iterator.hasNext()) { final DN dn = iterator.next(); if (! deleteErrors.containsKey(dn)) { if (! deleteEntry(connection, dn, deleteControls, entriesDeleted, deleteErrors, deleteRateLimiter, searchRequestSizeLimit, searchControls, useSubentriesControl, searchError)) { DN parentDN = dn.getParent(); while ((parentDN != null) && parentDN.isDescendantOf(baseDN, true)) { if (deleteErrors.containsKey(parentDN)) { break; } deleteErrors.put(parentDN, new LDAPResult(-1, ResultCode.NOT_ALLOWED_ON_NONLEAF, ERR_SUBTREE_DELETER_SKIPPING_UNDELETABLE_ANCESTOR.get( String.valueOf(parentDN), String.valueOf(dn)), null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS)); parentDN = parentDN.getParent(); } } } } return new SubtreeDeleterResult(null, false, null, entriesDeleted.get(), deleteErrors); } /** * Creates a search request that can be used to find all LDAP subentries at * or below the specified base DN. * * @param baseDN * The base DN to use for the search request. It must not be * {@code null}. * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. * @param controls * The set of controls to use for the search request. It must * not be {@code null} but may be empty. * @param dnSet * The set of DNs that should be updated with the DNs of the * matching entries. It must not be {@code null} and must be * updatable. * * @return A search request that can be used to find all LDAP subentries at * or below the specified base DN. */ @NotNull() private static SearchRequest createSubentriesSearchRequest( @NotNull final DN baseDN, final int searchRequestSizeLimit, @NotNull final List controls, @NotNull final SortedSet dnSet) { final Filter filter = Filter.createEqualityFilter("objectClass", "ldapSubentry"); final SubtreeDeleterSearchResultListener searchListener = new SubtreeDeleterSearchResultListener(baseDN, filter, dnSet); final SearchRequest searchRequest = new SearchRequest(searchListener, baseDN.toString(), SearchScope.SUB, DereferencePolicy.NEVER, searchRequestSizeLimit, 0, false, filter, "1.1"); for (final Control c : controls) { searchRequest.addControl(c); } searchRequest.addControl(new DraftLDUPSubentriesRequestControl(false)); return searchRequest; } /** * Creates a search request that can be used to find all entries at or below * the specified base DN that are not LDAP subentries. * * @param baseDN * The base DN to use for the search request. It must not be * {@code null}. * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. * @param controls * The set of controls to use for the search request. It must * not be {@code null} but may be empty. * @param dnSet * The set of DNs that should be updated with the DNs of the * matching entries. It must not be {@code null} and must be * updatable. * * @return A search request that can be used to find all entries at or below * the specified base DN that are not LDAP subentries. */ @NotNull() private static SearchRequest createNonSubentriesSearchRequest( @NotNull final DN baseDN, final int searchRequestSizeLimit, @NotNull final List controls, @NotNull final SortedSet dnSet) { final Filter filter = Filter.createPresenceFilter("objectClass"); final SubtreeDeleterSearchResultListener searchListener = new SubtreeDeleterSearchResultListener(baseDN, filter, dnSet); final SearchRequest searchRequest = new SearchRequest(searchListener, baseDN.toString(), SearchScope.SUB, DereferencePolicy.NEVER, searchRequestSizeLimit, 0, false, filter, "1.1"); for (final Control c : controls) { searchRequest.addControl(c); } return searchRequest; } /** * Uses the simple paged results control to iterate through all entries in * the server that match the criteria from the provided search request. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param searchRequest * The search request to be processed using the simple paged * results control. The request must not already include the * simple paged results request control, but must otherwise be * the request that should be processed, including any other * controls that are desired. It must not be {@code null}. * @param pageSize * The maximum number of entries that should be included in any * page of results. It must be greater than or equal to one. * * @throws LDAPSearchException If a problem is encountered during search * processing that prevents it from successfully * identifying all of the entries. */ private static void doPagedResultsSearch( @NotNull final LDAPInterface connection, @NotNull final SearchRequest searchRequest, final int pageSize) throws LDAPSearchException { final SubtreeDeleterSearchResultListener searchListener = (SubtreeDeleterSearchResultListener) searchRequest.getSearchResultListener(); ASN1OctetString pagedResultsCookie = null; while (true) { final SearchRequest pagedResultsSearchRequest = searchRequest.duplicate(); pagedResultsSearchRequest.addControl(new SimplePagedResultsControl( pageSize, pagedResultsCookie, true)); SearchResult searchResult; try { searchResult = connection.search(pagedResultsSearchRequest); } catch (final LDAPSearchException e) { Debug.debugException(e); searchResult = e.getSearchResult(); } if (searchResult.getResultCode() == ResultCode.NO_SUCH_OBJECT) { // This means that the base entry doesn't exist. This isn't an error. // It just means that there aren't any entries to delete. return; } else if (searchResult.getResultCode() != ResultCode.SUCCESS) { throw new LDAPSearchException(searchResult); } else if (searchListener.getFirstException() != null) { throw new LDAPSearchException(searchListener.getFirstException()); } final SimplePagedResultsControl responseControl; try { responseControl = SimplePagedResultsControl.get(searchResult); } catch (final LDAPException e) { Debug.debugException(e); throw new LDAPSearchException(e); } if (responseControl == null) { throw new LDAPSearchException(ResultCode.CONTROL_NOT_FOUND, ERR_SUBTREE_DELETER_MISSING_PAGED_RESULTS_RESPONSE.get( searchRequest.getBaseDN(), searchRequest.getFilter())); } if (responseControl.moreResultsToReturn()) { pagedResultsCookie = responseControl.getCookie(); } else { return; } } } /** * Attempts to delete an entry from the server. If the delete attempt fails * with a {@link ResultCode#NOT_ALLOWED_ON_NONLEAF} result, then an attempt * will be made to search for all of the subordinates of the target entry so * that they can be deleted, and then a second attempt will be made to remove * the target entry. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param dn The DN of the entry to delete. It must not be {@code null}. * @param deleteControls * A list of the controls that should be included in the delete * request. It must not be {@code null}, but may be empty. * @param entriesDeleted * A counter that should be incremented for each entry that is * successfully deleted. It must not be {@code null}. * @param deleteErrors * A map that should be updated with the DN of the entry and the * delete result, if the delete is unsuccessful. It must not be * {@code null} and must be updatable. * @param deleteRateLimiter * A fixed-rate barrier used to impose a rate limit on delete * operations. This may be {@code null} if no rate limit should * be imposed. * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. * @param searchControls * A list of controls that should be included in search * requests, if the initial delete attempt fails because the * entry has subordinates. It must not be {@code null}, but may * be empty. * @param useSubentriesControl * Indicates whether to look for LDAP subentries when searching * for entries to delete. * @param searchError * A reference that may be updated, if it is not already set, * with information about an error that occurred during search * processing. It must not be {@code null}, but may be * unassigned. * * @return {@code true} if the entry was successfully deleted, or * {@code false} if not. */ private static boolean deleteEntry(@NotNull final LDAPInterface connection, @NotNull final DN dn, @NotNull final List deleteControls, @NotNull final AtomicLong entriesDeleted, @NotNull final SortedMap deleteErrors, @Nullable final FixedRateBarrier deleteRateLimiter, final int searchRequestSizeLimit, @NotNull final List searchControls, final boolean useSubentriesControl, @NotNull final AtomicReference searchError) { if (deleteRateLimiter != null) { deleteRateLimiter.await(); } LDAPResult deleteResult; try { deleteResult = connection.delete(dn.toString()); } catch (final LDAPException e) { Debug.debugException(e); deleteResult = e.toLDAPResult(); } final ResultCode resultCode = deleteResult.getResultCode(); if (resultCode == ResultCode.SUCCESS) { // The entry was successfully deleted. entriesDeleted.incrementAndGet(); return true; } else if (resultCode == ResultCode.NO_SUCH_OBJECT) { // This is fine. It must have been deleted between the time of the // search and the time we got around to deleting it. return true; } else if (resultCode == ResultCode.NOT_ALLOWED_ON_NONLEAF) { // The entry must have children. Try to recursively delete it. return searchAndDelete(connection, dn, searchRequestSizeLimit, searchControls, useSubentriesControl, searchError, deleteControls, entriesDeleted, deleteErrors, deleteRateLimiter); } else { // This is just an error. deleteErrors.put(dn, deleteResult); return false; } } /** * Issues a subtree search (or a pair of subtree searches if the subentries * control should be used) to find any entries below the provided base DN, * and then attempts to delete all of those entries. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree in which to perform the search and * delete operations. It must not be {@code null}. * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. * @param searchControls * A list of controls that should be included in search * requests, if the initial delete attempt fails because the * entry has subordinates. It must not be {@code null}, but may * be empty. * @param useSubentriesControl * Indicates whether to look for LDAP subentries when searching * for entries to delete. * @param searchError * A reference that may be updated, if it is not already set, * with information about an error that occurred during search * processing. It must not be {@code null}, but may be * unassigned. * @param deleteControls * A list of the controls that should be included in the delete * request. It must not be {@code null}, but may be empty. * @param entriesDeleted * A counter that should be incremented for each entry that is * successfully deleted. It must not be {@code null}. * @param deleteErrors * A map that should be updated with the DN of the entry and the * delete result, if the delete is unsuccessful. It must not be * {@code null} and must be updatable. * @param deleteRateLimiter * A fixed-rate barrier used to impose a rate limit on delete * operations. This may be {@code null} if no rate limit should * be imposed. * * @return {@code true} if the subtree was successfully deleted, or * {@code false} if any errors occurred that prevented one or more * entries from being removed. */ private static boolean searchAndDelete( @NotNull final LDAPInterface connection, @NotNull final DN baseDN, final int searchRequestSizeLimit, @NotNull final List searchControls, final boolean useSubentriesControl, @NotNull final AtomicReference searchError, @NotNull final List deleteControls, @NotNull final AtomicLong entriesDeleted, @NotNull final SortedMap deleteErrors, @Nullable final FixedRateBarrier deleteRateLimiter) { while (true) { // If appropriate, search for subentries. SearchResult subentriesSearchResult = null; final TreeSet dnsToDelete = new TreeSet<>(); if (useSubentriesControl) { try { subentriesSearchResult = connection.search( createSubentriesSearchRequest(baseDN, searchRequestSizeLimit, searchControls, dnsToDelete)); } catch (final LDAPSearchException e) { Debug.debugException(e); subentriesSearchResult = e.getSearchResult(); } } // Search for non-subentries. SearchResult nonSubentriesSearchResult; try { nonSubentriesSearchResult = connection.search( createNonSubentriesSearchRequest(baseDN, searchRequestSizeLimit, searchControls, dnsToDelete)); } catch (final LDAPSearchException e) { Debug.debugException(e); nonSubentriesSearchResult = e.getSearchResult(); } // If we didn't find any entries, then there's nothing to do but // potentially update the search error. if (dnsToDelete.isEmpty()) { if (subentriesSearchResult != null) { switch (subentriesSearchResult.getResultCode().intValue()) { case ResultCode.SUCCESS_INT_VALUE: case ResultCode.NO_SUCH_OBJECT_INT_VALUE: // These are both fine. break; default: searchError.compareAndSet(null, subentriesSearchResult); return false; } } switch (nonSubentriesSearchResult.getResultCode().intValue()) { case ResultCode.SUCCESS_INT_VALUE: case ResultCode.NO_SUCH_OBJECT_INT_VALUE: // These are both fine. break; default: searchError.compareAndSet(null, nonSubentriesSearchResult); return false; } // Even though we didn't delete anything, we can assume that the entries // don't exist, so we'll consider it successful. return true; } // Iterate through the entries that we found and delete the ones that we // can. boolean anySuccessful = false; boolean allSuccessful = true; final TreeSet ancestorsToSkip = new TreeSet<>(); final DeleteRequest deleteRequest = new DeleteRequest(""); deleteRequest.setControls(deleteControls); for (final DN dn : dnsToDelete.descendingSet()) { if (deleteErrors.containsKey(dn)) { // We've already encountered an error for this entry, so don't try to // delete it. allSuccessful = false; continue; } else if (ancestorsToSkip.contains(dn)) { // We've already encountered an error while trying to delete one of // the descendants of this entry, so we'll skip it on this pass. We // might get it on another pass. allSuccessful = false; continue; } // If there is a rate limiter, then wait on it. if (deleteRateLimiter != null) { deleteRateLimiter.await(); } // Try to delete the target entry. LDAPResult deleteResult; try { deleteRequest.setDN(dn); deleteResult = connection.delete(deleteRequest); } catch (final LDAPException e) { Debug.debugException(e); deleteResult = e.toLDAPResult(); } switch (deleteResult.getResultCode().intValue()) { case ResultCode.SUCCESS_INT_VALUE: // The entry was successfully deleted. anySuccessful = true; entriesDeleted.incrementAndGet(); break; case ResultCode.NO_SUCH_OBJECT_INT_VALUE: // The entry doesn't exist. It may have been deleted between the // time we searched for it and the time we tried to delete it. // We'll treat this like a success, but won't increment the // counter. anySuccessful = true; break; case ResultCode.NOT_ALLOWED_ON_NONLEAF_INT_VALUE: // This suggests that the entry has children. If it is the base // entry, then we may be able to loop back around and delete it on // another pass. Otherwise, try to recursively delete it. if (dn.equals(baseDN)) { allSuccessful = false; } else { if (searchAndDelete(connection, dn, searchRequestSizeLimit, searchControls, useSubentriesControl, searchError, deleteControls, entriesDeleted, deleteErrors, deleteRateLimiter)) { anySuccessful = true; } else { allSuccessful = false; DN parentDN = dn.getParent(); while (parentDN != null) { ancestorsToSkip.add(parentDN); parentDN = parentDN.getParent(); } } } break; default: // We definitely couldn't delete this entry, and we're not going to // make another attempt. Put it in the set of delete errors, and // also include the DNs of all of its ancestors. deleteErrors.put(dn, deleteResult); DN parentDN = dn.getParent(); while ((parentDN != null) && parentDN.isDescendantOf(baseDN, true)) { deleteErrors.put(parentDN, new LDAPResult(-1, ResultCode.NOT_ALLOWED_ON_NONLEAF, ERR_SUBTREE_DELETER_SKIPPING_UNDELETABLE_ANCESTOR.get( String.valueOf(parentDN), String.valueOf(dn)), null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS)); parentDN = parentDN.getParent(); } allSuccessful = false; break; } } // Look at the search results and see if we need to update the search // error. There's no error for a result code of SUCCESS or // NO_SUCH_OBJECT. If the result code is SIZE_LIMIT_EXCEEDED, then that's // an error only if we couldn't delete any of the entries that we found. // If the result code is anything else, then that's an error. if (subentriesSearchResult != null) { switch (subentriesSearchResult.getResultCode().intValue()) { case ResultCode.SUCCESS_INT_VALUE: case ResultCode.NO_SUCH_OBJECT_INT_VALUE: break; case ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE: if (! anySuccessful) { searchError.compareAndSet(null, subentriesSearchResult); } break; default: searchError.compareAndSet(null, subentriesSearchResult); break; } } switch (nonSubentriesSearchResult.getResultCode().intValue()) { case ResultCode.SUCCESS_INT_VALUE: case ResultCode.NO_SUCH_OBJECT_INT_VALUE: break; case ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE: if (! anySuccessful) { searchError.compareAndSet(null, nonSubentriesSearchResult); } break; default: searchError.compareAndSet(null, nonSubentriesSearchResult); break; } // Evaluate the success or failure of the processing that we performed. if (allSuccessful) { // We were able to successfully complete all of the deletes that we // attempted. If the base entry was included in that set, then we were // successful and can return true. Otherwise, we should loop back // around because that suggests there are more entries to delete. if (dnsToDelete.contains(baseDN)) { return true; } } else if (! anySuccessful) { // We couldn't delete any of the entries that we tried. This is // definitely an error. return false; } // If we've gotten here, then that means that we deleted at least some of // the entries, but we need to loop back around and make another attempt } } /** * Deletes the specified subtree with the given settings. The simple paged * results control will not be used in the course of searching for entries to * delete. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to delete. It must not be * {@code null}. * @param deleteBaseEntry * Indicates whether the base entry itself should be deleted * along with its subordinates (if {@code true}), or if only the * subordinates of the base entry should be deleted but the base * entry itself should remain (if {@code false}). * @param searchRequestSizeLimit * The size limit that should be used in each search request to * specify the maximum number of entries to return in response * to that request. A value that is less than or equal to zero * indicates that the client does not want to impose any size * limit. * @param useSubentriesControl * Indicates whether to look for LDAP subentries when searching * for entries to delete. * @param searchControls * A list of controls that should be included in search requests * used to find the entries to delete. This must not be * {@code null} but may be empty. * @param deleteControls * A list of controls that should be included in delete requests. * This must not be {@code null} but may be empty. * @param deleteRateLimiter * A fixed-rate barrier used to impose a rate limit on delete * operations. This may be {@code null} if no rate limit should * be imposed. * * @return An object with information about the results of the subtree * delete processing. */ @NotNull() private static SubtreeDeleterResult deleteEntriesWithoutSimplePagedResults( @NotNull final LDAPInterface connection, @NotNull final DN baseDN, final boolean deleteBaseEntry, final int searchRequestSizeLimit, final boolean useSubentriesControl, @NotNull final List searchControls, @NotNull final List deleteControls, @Nullable final FixedRateBarrier deleteRateLimiter) { // If we should use the subentries control, then first search to find all // subentries in the subentry, and delete them first. Continue the // process until we run out of entries or until we can't delete any more. final TreeSet dnsToDelete = new TreeSet<>(); final AtomicReference searchError = new AtomicReference<>(); final AtomicLong entriesDeleted = new AtomicLong(0L); final TreeMap deleteErrors = new TreeMap<>(); if (useSubentriesControl) { final SearchRequest searchRequest = createSubentriesSearchRequest( baseDN, searchRequestSizeLimit, searchControls, dnsToDelete); searchAndDelete(connection, baseDN, searchRequest, useSubentriesControl, searchControls, dnsToDelete, searchError, deleteBaseEntry, deleteControls, deleteRateLimiter, entriesDeleted, deleteErrors); } // Create a search request that doesn't use the subentries request // control,and use that to conduct the searches to identify the entries to // delete. final SearchRequest searchRequest = createNonSubentriesSearchRequest(baseDN, searchRequestSizeLimit, searchControls, dnsToDelete); searchAndDelete(connection, baseDN, searchRequest, useSubentriesControl, searchControls, dnsToDelete, searchError, deleteBaseEntry, deleteControls, deleteRateLimiter, entriesDeleted, deleteErrors); return new SubtreeDeleterResult(null, false, searchError.get(), entriesDeleted.get(), deleteErrors); } /** * Repeatedly processes the provided search request until there are no more * matching entries or until no more entries can be deleted. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to delete. It must not be * {@code null}. * @param searchRequest * The search request to use to identify the entries to delete. * It must not be {@code null}, and must be repeatable exactly * as-is. * @param useSubentriesControl * Indicates whether to look for LDAP subentries when searching * for entries to delete. * @param searchControls * A list of controls that should be included in search requests * used to find the entries to delete. This must not be * {@code null} but may be empty. * @param dnsToDelete * A sorted set that will be updated during search processing * with the DNs of the entries that match the search criteria. * It must not be {@code null}, and must be updatable. * @param searchError * A reference to an error that was encountered during search * processing. It must not be {@code null}, but may be * unassigned. * @param deleteBaseEntry * Indicates whether the base entry itself should be deleted * along with its subordinates (if {@code true}), or if only the * subordinates of the base entry should be deleted but the base * entry itself should remain (if {@code false}). * @param deleteControls * A list of controls that should be included in delete requests. * This must not be {@code null} but may be empty. * @param deleteRateLimiter * A fixed-rate barrier used to impose a rate limit on delete * operations. This may be {@code null} if no rate limit should * be imposed. * @param entriesDeleted * A counter used to keep track of the number of entries that * have been deleted. It must not be {@code null}. * @param deleteErrors * A sorted map that will be updated with information about * unsuccessful attempts to delete entries. It must not be * {@code null}, and must be updatable. */ private static void searchAndDelete(@NotNull final LDAPInterface connection, @NotNull final DN baseDN, @NotNull final SearchRequest searchRequest, final boolean useSubentriesControl, @NotNull final List searchControls, @NotNull final TreeSet dnsToDelete, @NotNull final AtomicReference searchError, final boolean deleteBaseEntry, @NotNull final List deleteControls, @Nullable final FixedRateBarrier deleteRateLimiter, @NotNull final AtomicLong entriesDeleted, @NotNull final SortedMap deleteErrors) { while (true) { // Get the number of entries that have been deleted thus far. If this // hasn't gone up by the end of this loop, then we'll stop looping. final long beforeDeleteCount = entriesDeleted.get(); // Issue a search to find all of the entries we can that match the // search criteria. SearchResult searchResult; try { searchResult = connection.search(searchRequest); } catch (final LDAPSearchException e) { Debug.debugException(e); searchResult = e.getSearchResult(); } // See if we should update the search error result. if (searchError.get() == null) { final ResultCode searchResultCode = searchResult.getResultCode(); if (searchResultCode == ResultCode.SUCCESS) { // This is obviously not an error. } else if (searchResultCode == ResultCode.NO_SUCH_OBJECT) { // This is also not an error. It means that the base entry doesn't // exist, so there's no point in continuing on. return; } else if (searchResultCode == ResultCode.SIZE_LIMIT_EXCEEDED) { // This is probably not an error, but we may consider it one if we // can't delete anything during this pass. } else { // This is an error. searchError.compareAndSet(null, searchResult); } } // If we should not delete the base entry, then remove it from the set. if (! deleteBaseEntry) { dnsToDelete.remove(baseDN); } // Iterate through the DN set, which should have been populated by the // search. If any of them are in the delete errors map, then we'll skip // them. All others we'll try to delete. final Iterator dnIterator = dnsToDelete.descendingIterator(); while (dnIterator.hasNext()) { final DN dnToDelete = dnIterator.next(); dnIterator.remove(); // Don't try to delete the entry if we've already tried and failed. if (! deleteErrors.containsKey(dnToDelete)) { if (! deleteEntry(connection, dnToDelete, deleteControls, entriesDeleted, deleteErrors, deleteRateLimiter, searchRequest.getSizeLimit(), searchControls, useSubentriesControl, searchError)) { // We couldn't delete the entry. That means we also won't be able // to delete its parents, so put them in the errors map so that we // won't even try to delete them. DN parentDN = dnToDelete.getParent(); while ((parentDN != null) && parentDN.isDescendantOf(baseDN, true)) { if (deleteErrors.containsKey(parentDN)) { break; } deleteErrors.put(parentDN, new LDAPResult(-1, ResultCode.NOT_ALLOWED_ON_NONLEAF, ERR_SUBTREE_DELETER_SKIPPING_UNDELETABLE_ANCESTOR.get( String.valueOf(parentDN), String.valueOf(dnToDelete)), null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS)); parentDN = parentDN.getParent(); } } } } final long afterDeleteCount = entriesDeleted.get(); if (afterDeleteCount == beforeDeleteCount) { // We were unable to successfully delete any entries this time through // the loop. That may mean that there aren't any more entries, or that // errors prevented deleting the entries we did find. If we happened to // get a "size limit exceeded" search result, and if the search error // isn't set, then set it to the "size limit exceeded" result. if (searchResult.getResultCode() == ResultCode.SIZE_LIMIT_EXCEEDED) { searchError.compareAndSet(null, searchResult); } return; } } } /** * Removes teh subtree accessibility restriction from the server. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param baseDN * The base DN for the subtree to make accessible. It must not * be {@code null}. * * @return The result of the attempt to remove the subtree accessibility * restriction. */ @NotNull() private static ExtendedResult removeAccessibilityRestriction( @NotNull final LDAPInterface connection, @NotNull final DN baseDN) { return processExtendedOperation(connection, SetSubtreeAccessibilityExtendedRequest.createSetAccessibleRequest( baseDN.toString())); } /** * Uses the provided connection to process the given extended request. * * @param connection * The {@link LDAPInterface} instance to use to communicate with * the directory server. While this may be an individual * {@link LDAPConnection}, it may be better as a connection * pool with automatic retry enabled so that it's more likely to * succeed in the event that a connection becomes invalid or an * operation experiences a transient failure. It must not be * {@code null}. * @param request * The extended request to be processed. It must not be * {@code null}. * * @return The extended result obtained from processing the request. */ @NotNull() private static ExtendedResult processExtendedOperation( @NotNull final LDAPInterface connection, @NotNull final ExtendedRequest request) { try { if (connection instanceof LDAPConnection) { return ((LDAPConnection) connection).processExtendedOperation( request); } else if (connection instanceof AbstractConnectionPool) { return ((AbstractConnectionPool) connection).processExtendedOperation( request); } else { return new ExtendedResult(-1, ResultCode.PARAM_ERROR, ERR_SUBTREE_DELETER_INTERFACE_EXTOP_NOT_SUPPORTED.get( connection.getClass().getName()), null, StaticUtils.NO_STRINGS, null, null, StaticUtils.NO_CONTROLS); } } catch (final LDAPException e) { Debug.debugException(e); return new ExtendedResult(e); } } /** * Attempts to determine whether the server advertises support for the * specified extended request. * * @param connection * The connection (or other {@link LDAPInterface} instance, like * a connection pool) that should be used to communicate with the * directory server. It must not be {@code null}. * @param rootDSE * A reference to the server root DSE, if it has already been * retrieved. It must not be {@code null}, but may be * unassigned. * @param oid The OID of the extended request for which to make the * determination. It must not be {@code null}. * * @return {@code true} if the server advertises support for the specified * request control, or {@code false} if not. */ private static boolean supportsExtendedRequest( @NotNull final LDAPInterface connection, @NotNull final AtomicReference rootDSE, @NotNull final String oid) { final RootDSE dse = getRootDSE(connection, rootDSE); if (dse == null) { return false; } else { return dse.supportsExtendedOperation(oid); } } /** * Attempts to determine whether the server advertises support for the * specified request control. * * @param connection * The connection (or other {@link LDAPInterface} instance, like * a connection pool) that should be used to communicate with the * directory server. It must not be {@code null}. * @param rootDSE * A reference to the server root DSE, if it has already been * retrieved. It must not be {@code null}, but may be * unassigned. * @param oid The OID of the request control for which to make the * determination. It must not be {@code null}. * * @return {@code true} if the server advertises support for the specified * request control, or {@code false} if not. */ private static boolean supportsControl( @NotNull final LDAPInterface connection, @NotNull final AtomicReference rootDSE, @NotNull final String oid) { final RootDSE dse = getRootDSE(connection, rootDSE); if (dse == null) { return false; } else { return dse.supportsControl(oid); } } /** * Retrieves the server's root DSE. It will use the cached version if it's * already available, or will retrieve it from the server if not. * * @param connection * The connection (or other {@link LDAPInterface} instance, like * a connection pool) that should be used to communicate with the * directory server. It must not be {@code null}. * @param rootDSE * A reference to the server root DSE, if it has already been * retrieved. It must not be {@code null}, but may be * unassigned. * * @return The server's root DSE, or {@code null} if it could not be * retrieved. */ @Nullable() private static RootDSE getRootDSE(@NotNull final LDAPInterface connection, @NotNull final AtomicReference rootDSE) { final RootDSE dse = rootDSE.get(); if (dse != null) { return dse; } try { return connection.getRootDSE(); } catch (final Exception e) { Debug.debugException(e); return null; } } /** * Retrieves a string representation of this subtree deleter. * * @return A string representation of this subtree deleter. */ @Override() @NotNull() public String toString() { final StringBuilder buffer = new StringBuilder(); toString(buffer); return buffer.toString(); } /** * Appends a string representation of this subtree deleter to the provided * buffer. * * @param buffer The buffer to which the string representation should be * appended. */ public void toString(@NotNull final StringBuilder buffer) { buffer.append("SubtreeDeleter(deleteBaseEntry="); buffer.append(deleteBaseEntry); buffer.append(", useSetSubtreeAccessibilityOperationIfAvailable="); buffer.append(useSetSubtreeAccessibilityOperationIfAvailable); if (useSimplePagedResultsControlIfAvailable) { buffer.append( ", useSimplePagedResultsControlIfAvailable=true, pageSize="); buffer.append(simplePagedResultsPageSize); } else { buffer.append(", useSimplePagedResultsControlIfAvailable=false"); } buffer.append(", useManageDSAITControlIfAvailable="); buffer.append(useManageDSAITControlIfAvailable); buffer.append(", usePermitUnindexedSearchControlIfAvailable="); buffer.append(usePermitUnindexedSearchControlIfAvailable); buffer.append(", useSubentriesControlIfAvailable="); buffer.append(useSubentriesControlIfAvailable); buffer.append(", useReturnConflictEntriesRequestControlIfAvailable="); buffer.append(useReturnConflictEntriesRequestControlIfAvailable); buffer.append(", useSoftDeletedEntryAccessControlIfAvailable="); buffer.append(useSoftDeletedEntryAccessControlIfAvailable); buffer.append(", useHardDeleteControlIfAvailable="); buffer.append(useHardDeleteControlIfAvailable); buffer.append(", additionalSearchControls={ "); final Iterator searchControlIterator = additionalSearchControls.iterator(); while (searchControlIterator.hasNext()) { buffer.append(searchControlIterator.next()); if (searchControlIterator.hasNext()) { buffer.append(','); } buffer.append(' '); } buffer.append("}, additionalDeleteControls={"); final Iterator deleteControlIterator = additionalSearchControls.iterator(); while (deleteControlIterator.hasNext()) { buffer.append(deleteControlIterator.next()); if (deleteControlIterator.hasNext()) { buffer.append(','); } buffer.append(' '); } buffer.append("}, searchRequestSizeLimit="); buffer.append(searchRequestSizeLimit); buffer.append(')'); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy