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

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

/*
 * Copyright 2019-2022 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2019-2022 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-2022 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