001/*
002 * Copyright 2019-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2019-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2019-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.tools;
037
038
039
040import java.io.BufferedReader;
041import java.io.ByteArrayInputStream;
042import java.io.File;
043import java.io.FileInputStream;
044import java.io.InputStream;
045import java.io.InputStreamReader;
046import java.io.IOException;
047import java.io.OutputStream;
048import java.nio.charset.Charset;
049import java.security.GeneralSecurityException;
050import java.util.ArrayList;
051import java.util.Arrays;
052import java.util.Collections;
053import java.util.Iterator;
054import java.util.LinkedHashMap;
055import java.util.List;
056import java.util.Map;
057import java.util.TreeSet;
058import java.util.concurrent.TimeUnit;
059import java.util.concurrent.atomic.AtomicLong;
060import java.util.concurrent.atomic.AtomicReference;
061
062import com.unboundid.asn1.ASN1OctetString;
063import com.unboundid.ldap.sdk.Control;
064import com.unboundid.ldap.sdk.DeleteRequest;
065import com.unboundid.ldap.sdk.DereferencePolicy;
066import com.unboundid.ldap.sdk.DN;
067import com.unboundid.ldap.sdk.ExtendedResult;
068import com.unboundid.ldap.sdk.Filter;
069import com.unboundid.ldap.sdk.LDAPConnectionOptions;
070import com.unboundid.ldap.sdk.LDAPConnection;
071import com.unboundid.ldap.sdk.LDAPConnectionPool;
072import com.unboundid.ldap.sdk.LDAPException;
073import com.unboundid.ldap.sdk.LDAPResult;
074import com.unboundid.ldap.sdk.ResultCode;
075import com.unboundid.ldap.sdk.SearchRequest;
076import com.unboundid.ldap.sdk.SearchResult;
077import com.unboundid.ldap.sdk.SearchScope;
078import com.unboundid.ldap.sdk.UnsolicitedNotificationHandler;
079import com.unboundid.ldap.sdk.Version;
080import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
081import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
082import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
083import com.unboundid.ldap.sdk.controls.PreReadRequestControl;
084import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV1RequestControl;
085import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
086import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
087import com.unboundid.ldap.sdk.controls.SubtreeDeleteRequestControl;
088import com.unboundid.ldap.sdk.unboundidds.controls.
089            AssuredReplicationLocalLevel;
090import com.unboundid.ldap.sdk.unboundidds.controls.
091            AssuredReplicationRemoteLevel;
092import com.unboundid.ldap.sdk.unboundidds.controls.
093            AssuredReplicationRequestControl;
094import com.unboundid.ldap.sdk.unboundidds.controls.
095            GetAuthorizationEntryRequestControl;
096import com.unboundid.ldap.sdk.unboundidds.controls.
097            GetBackendSetIDRequestControl;
098import com.unboundid.ldap.sdk.unboundidds.controls.GetServerIDRequestControl;
099import com.unboundid.ldap.sdk.unboundidds.controls.
100            GetUserResourceLimitsRequestControl;
101import com.unboundid.ldap.sdk.unboundidds.controls.HardDeleteRequestControl;
102import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl;
103import com.unboundid.ldap.sdk.unboundidds.controls.
104            OperationPurposeRequestControl;
105import com.unboundid.ldap.sdk.unboundidds.controls.
106            ReplicationRepairRequestControl;
107import com.unboundid.ldap.sdk.unboundidds.controls.
108            RouteToBackendSetRequestControl;
109import com.unboundid.ldap.sdk.unboundidds.controls.RouteToServerRequestControl;
110import com.unboundid.ldap.sdk.unboundidds.controls.SoftDeleteRequestControl;
111import com.unboundid.ldap.sdk.unboundidds.controls.
112            SuppressReferentialIntegrityUpdatesRequestControl;
113import com.unboundid.ldap.sdk.unboundidds.extensions.
114            StartAdministrativeSessionExtendedRequest;
115import com.unboundid.ldap.sdk.unboundidds.extensions.
116            StartAdministrativeSessionPostConnectProcessor;
117import com.unboundid.ldif.LDIFWriter;
118import com.unboundid.util.Base64;
119import com.unboundid.util.Debug;
120import com.unboundid.util.FixedRateBarrier;
121import com.unboundid.util.LDAPCommandLineTool;
122import com.unboundid.util.NotNull;
123import com.unboundid.util.Nullable;
124import com.unboundid.util.ObjectPair;
125import com.unboundid.util.StaticUtils;
126import com.unboundid.util.SubtreeDeleter;
127import com.unboundid.util.SubtreeDeleterResult;
128import com.unboundid.util.ThreadSafety;
129import com.unboundid.util.ThreadSafetyLevel;
130import com.unboundid.util.args.Argument;
131import com.unboundid.util.args.ArgumentException;
132import com.unboundid.util.args.ArgumentParser;
133import com.unboundid.util.args.BooleanArgument;
134import com.unboundid.util.args.ControlArgument;
135import com.unboundid.util.args.DNArgument;
136import com.unboundid.util.args.DurationArgument;
137import com.unboundid.util.args.FileArgument;
138import com.unboundid.util.args.FilterArgument;
139import com.unboundid.util.args.IntegerArgument;
140import com.unboundid.util.args.StringArgument;
141
142import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
143
144
145
146/**
147 * This class provides a command-line tool that can be used to delete one or
148 * more entries from an LDAP directory server.  The DNs of entries to delete
149 * can be provided through command-line arguments, read from a file, or read
150 * from standard input.  Alternately, the tool can delete entries matching a
151 * given search filter.
152 * <BR>
153 * <BLOCKQUOTE>
154 *   <B>NOTE:</B>  This class, and other classes within the
155 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
156 *   supported for use against Ping Identity, UnboundID, and
157 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
158 *   for proprietary functionality or for external specifications that are not
159 *   considered stable or mature enough to be guaranteed to work in an
160 *   interoperable way with other types of LDAP servers.
161 * </BLOCKQUOTE>
162 */
163@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
164public final class LDAPDelete
165       extends LDAPCommandLineTool
166       implements UnsolicitedNotificationHandler
167{
168  /**
169   * The column at which output should be wrapped.
170   */
171  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
172
173
174
175  // The set of arguments supported by this program.
176  @Nullable private ArgumentParser parser = null;
177  @Nullable private BooleanArgument authorizationIdentity = null;
178  @Nullable private BooleanArgument clientSideSubtreeDelete = null;
179  @Nullable private BooleanArgument continueOnError = null;
180  @Nullable private BooleanArgument dryRun = null;
181  @Nullable private BooleanArgument followReferrals = null;
182  @Nullable private BooleanArgument getBackendSetID = null;
183  @Nullable private BooleanArgument getServerID = null;
184  @Nullable private BooleanArgument getUserResourceLimits = null;
185  @Nullable private BooleanArgument hardDelete = null;
186  @Nullable private BooleanArgument manageDsaIT = null;
187  @Nullable private BooleanArgument neverRetry = null;
188  @Nullable private BooleanArgument noOperation = null;
189  @Nullable private BooleanArgument replicationRepair = null;
190  @Nullable private BooleanArgument retryFailedOperations = null;
191  @Nullable private BooleanArgument softDelete = null;
192  @Nullable private BooleanArgument serverSideSubtreeDelete = null;
193  @Nullable private BooleanArgument suppressReferentialIntegrityUpdates = null;
194  @Nullable private BooleanArgument useAdministrativeSession = null;
195  @Nullable private BooleanArgument useAssuredReplication = null;
196  @Nullable private BooleanArgument verbose = null;
197  @Nullable private ControlArgument bindControl = null;
198  @Nullable private ControlArgument deleteControl = null;
199  @Nullable private DNArgument entryDN = null;
200  @Nullable private DNArgument proxyV1As = null;
201  @Nullable private DNArgument searchBaseDN = null;
202  @Nullable private DurationArgument assuredReplicationTimeout = null;
203  @Nullable private FileArgument dnFile = null;
204  @Nullable private FileArgument encryptionPassphraseFile = null;
205  @Nullable private FileArgument deleteEntriesMatchingFiltersFromFile = null;
206  @Nullable private FileArgument rejectFile = null;
207  @Nullable private FilterArgument assertionFilter = null;
208  @Nullable private FilterArgument deleteEntriesMatchingFilter = null;
209  @Nullable private IntegerArgument ratePerSecond = null;
210  @Nullable private IntegerArgument searchPageSize = null;
211  @Nullable private StringArgument assuredReplicationLocalLevel = null;
212  @Nullable private StringArgument assuredReplicationRemoteLevel = null;
213  @Nullable private StringArgument characterSet = null;
214  @Nullable private StringArgument getAuthorizationEntryAttribute = null;
215  @Nullable private StringArgument operationPurpose = null;
216  @Nullable private StringArgument preReadAttribute = null;
217  @Nullable private StringArgument proxyAs = null;
218  @Nullable private StringArgument routeToBackendSet = null;
219  @Nullable private StringArgument routeToServer = null;
220
221  // A reference to the reject writer that has been written, if it has been
222  // created.
223  @NotNull private final AtomicReference<LDIFWriter> rejectWriter =
224       new AtomicReference<>();
225
226  // The fixed-rate barrier (if any) used to enforce a rate limit on delete
227  // operations.
228  @Nullable private volatile FixedRateBarrier deleteRateLimiter = null;
229
230  // The input stream from to use for standard input.
231  @NotNull private final InputStream in;
232
233  // The connection pool to use to communicate with the directory server.
234  @Nullable private volatile LDAPConnectionPool connectionPool = null;
235
236  // Controls to include in requests.
237  @NotNull private volatile List<Control> deleteControls =
238       Collections.emptyList();
239  @NotNull private volatile List<Control> searchControls =
240       Collections.emptyList();
241  @NotNull private final List<RouteToBackendSetRequestControl>
242       routeToBackendSetRequestControls = new ArrayList<>(10);
243
244  // The subtree deleter to use to process client-side subtree deletes.
245  @Nullable private volatile SubtreeDeleter subtreeDeleter = null;
246
247
248
249  /**
250   * Runs this tool with the provided command-line arguments.  It will use the
251   * JVM-default streams for standard input, output, and error.
252   *
253   * @param  args  The command-line arguments to provide to this program.
254   */
255  public static void main(@NotNull final String... args)
256  {
257    final ResultCode resultCode = main(System.in, System.out, System.err, args);
258    if (resultCode != ResultCode.SUCCESS)
259    {
260      System.exit(resultCode.intValue());
261    }
262  }
263
264
265
266  /**
267   * Runs this tool with the provided streams and command-line arguments.
268   *
269   * @param  in    The input stream to use for standard input.  If this is
270   *               {@code null}, then no standard input will be used.
271   * @param  out   The output stream to use for standard output.  If this is
272   *               {@code null}, then standard output will be suppressed.
273   * @param  err   The output stream to use for standard error.  If this is
274   *               {@code null}, then standard error will be suppressed.
275   * @param  args  The command-line arguments provided to this program.
276   *
277   * @return  The result code obtained when running the tool.  Any result code
278   *          other than {@link ResultCode#SUCCESS} indicates an error.
279   */
280  @NotNull()
281  public static ResultCode main(@Nullable final InputStream in,
282                                @Nullable final OutputStream out,
283                                @Nullable final OutputStream err,
284                                @NotNull final String... args)
285  {
286    final LDAPDelete ldapDelete = new LDAPDelete(in, out, err);
287    return ldapDelete.runTool(args);
288  }
289
290
291
292  /**
293   * Creates a new instance of this tool with the provided streams.  Standard
294   * input will not be available.
295   *
296   * @param  out  The output stream to use for standard output.  If this is
297   *              {@code null}, then standard output will be suppressed.
298   * @param  err  The output stream to use for standard error.  If this is
299   *              {@code null}, then standard error will be suppressed.
300   */
301  public LDAPDelete(@Nullable final OutputStream out,
302                    @Nullable final OutputStream err)
303  {
304    this(null, out, err);
305  }
306
307
308
309  /**
310   * Creates a new instance of this tool with the provided streams.
311   *
312   * @param  in   The input stream to use for standard input.  If this is
313   *              {@code null}, then no standard input will be used.
314   * @param  out  The output stream to use for standard output.  If this is
315   *              {@code null}, then standard output will be suppressed.
316   * @param  err  The output stream to use for standard error.  If this is
317   *              {@code null}, then standard error will be suppressed.
318   */
319  public LDAPDelete(@Nullable final InputStream in,
320                    @Nullable final OutputStream out,
321                    @Nullable final OutputStream err)
322  {
323    super(out, err);
324
325    if (in == null)
326    {
327      this.in = new ByteArrayInputStream(StaticUtils.NO_BYTES);
328    }
329    else
330    {
331      this.in = in;
332    }
333  }
334
335
336
337  /**
338   * {@inheritDoc}
339   */
340  @Override()
341  @NotNull()
342  public String getToolName()
343  {
344    return "ldapdelete";
345  }
346
347
348
349  /**
350   * {@inheritDoc}
351   */
352  @Override()
353  @NotNull()
354  public String getToolDescription()
355  {
356    return INFO_LDAPDELETE_TOOL_DESCRIPTION.get();
357  }
358
359
360
361  /**
362   * {@inheritDoc}
363   */
364  @Override()
365  @NotNull()
366  public String getToolVersion()
367  {
368    return Version.NUMERIC_VERSION_STRING;
369  }
370
371
372
373  /**
374   * {@inheritDoc}
375   */
376  @Override()
377  public int getMinTrailingArguments()
378  {
379    return 0;
380  }
381
382
383
384  /**
385   * {@inheritDoc}
386   */
387  @Override()
388  public int getMaxTrailingArguments()
389  {
390    return Integer.MAX_VALUE;
391  }
392
393
394
395  /**
396   * {@inheritDoc}
397   */
398  @Override()
399  @NotNull()
400  public String getTrailingArgumentsPlaceholder()
401  {
402    return INFO_LDAPDELETE_TRAILING_ARGS_PLACEHOLDER.get();
403  }
404
405
406
407  /**
408   * {@inheritDoc}
409   */
410  @Override()
411  public boolean supportsInteractiveMode()
412  {
413    return true;
414  }
415
416
417
418  /**
419   * {@inheritDoc}
420   */
421  @Override()
422  public boolean defaultsToInteractiveMode()
423  {
424    return true;
425  }
426
427
428
429  /**
430   * {@inheritDoc}
431   */
432  @Override()
433  public boolean supportsPropertiesFile()
434  {
435    return true;
436  }
437
438
439
440  /**
441   * {@inheritDoc}
442   */
443  @Override()
444  public boolean supportsOutputFile()
445  {
446    return true;
447  }
448
449
450
451  /**
452   * {@inheritDoc}
453   */
454  @Override()
455  protected boolean defaultToPromptForBindPassword()
456  {
457    return true;
458  }
459
460
461
462  /**
463   * {@inheritDoc}
464   */
465  @Override()
466  protected boolean includeAlternateLongIdentifiers()
467  {
468    return true;
469  }
470
471
472
473  /**
474   * {@inheritDoc}
475   */
476  @Override()
477  protected boolean supportsSSLDebugging()
478  {
479    return true;
480  }
481
482
483
484  /**
485   * {@inheritDoc}
486   */
487  @Override()
488  protected boolean logToolInvocationByDefault()
489  {
490    return true;
491  }
492
493
494
495  /**
496   * {@inheritDoc}
497   */
498  @Override()
499  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
500         throws ArgumentException
501  {
502    this.parser = parser;
503
504
505    //
506    // Data Arguments
507    //
508
509    final String argGroupData = INFO_LDAPDELETE_ARG_GROUP_DATA.get();
510
511    entryDN = new DNArgument('b', "entryDN", false, 0, null,
512         INFO_LDAPDELETE_ARG_DESC_DN.get());
513    entryDN.addLongIdentifier("entry-dn", true);
514    entryDN.addLongIdentifier("dn", true);
515    entryDN.addLongIdentifier("dnToDelete", true);
516    entryDN.addLongIdentifier("dn-to-delete", true);
517    entryDN.addLongIdentifier("entry", true);
518    entryDN.addLongIdentifier("entryToDelete", true);
519    entryDN.addLongIdentifier("entry-to-delete", true);
520    entryDN.setArgumentGroupName(argGroupData);
521    parser.addArgument(entryDN);
522
523
524    dnFile = new FileArgument('f', "dnFile", false, 0, null,
525         INFO_LDAPDELETE_ARG_DESC_DN_FILE.get(), true, true, true, false);
526    dnFile.addLongIdentifier("dn-file", true);
527    dnFile.addLongIdentifier("dnFilename", true);
528    dnFile.addLongIdentifier("dn-filename", true);
529    dnFile.addLongIdentifier("deleteEntriesWithDNsFromFile", true);
530    dnFile.addLongIdentifier("delete-entries0-with-dns-from-file", true);
531    dnFile.addLongIdentifier("file", true);
532    dnFile.addLongIdentifier("filename", true);
533    dnFile.setArgumentGroupName(argGroupData);
534    parser.addArgument(dnFile);
535
536
537    deleteEntriesMatchingFilter = new FilterArgument(null,
538         "deleteEntriesMatchingFilter", false, 0, null,
539         INFO_LDAPDELETE_ARG_DESC_DELETE_ENTRIES_MATCHING_FILTER.get());
540    deleteEntriesMatchingFilter.addLongIdentifier(
541         "delete-entries-matching-filter", true);
542    deleteEntriesMatchingFilter.addLongIdentifier("deleteFilter", true);
543    deleteEntriesMatchingFilter.addLongIdentifier("delete-filter", true);
544    deleteEntriesMatchingFilter.addLongIdentifier("deleteSearchFilter", true);
545    deleteEntriesMatchingFilter.addLongIdentifier("delete-search-filter", true);
546    deleteEntriesMatchingFilter.addLongIdentifier("filter", true);
547    deleteEntriesMatchingFilter.setArgumentGroupName(argGroupData);
548    parser.addArgument(deleteEntriesMatchingFilter);
549
550
551    deleteEntriesMatchingFiltersFromFile = new FileArgument(null,
552         "deleteEntriesMatchingFiltersFromFile", false, 0, null,
553         INFO_LDAPDELETE_ARG_DESC_DELETE_ENTRIES_MATCHING_FILTER_FILE.get(),
554         true, true, true, false);
555    deleteEntriesMatchingFiltersFromFile.addLongIdentifier(
556         "delete-entries-matching-filters-from-file", true);
557    deleteEntriesMatchingFiltersFromFile.addLongIdentifier(
558         "deleteEntriesMatchingFilterFromFile", true);
559    deleteEntriesMatchingFiltersFromFile.addLongIdentifier(
560         "delete-entries-matching-filter-from-file", true);
561    deleteEntriesMatchingFiltersFromFile.addLongIdentifier("deleteFilterFile",
562         true);
563    deleteEntriesMatchingFiltersFromFile.addLongIdentifier("delete-filter-file",
564         true);
565    deleteEntriesMatchingFiltersFromFile.addLongIdentifier(
566         "deleteSearchFilterFile", true);
567    deleteEntriesMatchingFiltersFromFile.addLongIdentifier(
568         "delete-search-filter-file", true);
569    deleteEntriesMatchingFiltersFromFile.addLongIdentifier("filterFile", true);
570    deleteEntriesMatchingFiltersFromFile.addLongIdentifier("filter-file", true);
571    deleteEntriesMatchingFiltersFromFile.setArgumentGroupName(argGroupData);
572    parser.addArgument(deleteEntriesMatchingFiltersFromFile);
573
574
575    searchBaseDN = new DNArgument(null, "searchBaseDN", false, 0, null,
576         INFO_LDAPDELETE_ARG_DESC_SEARCH_BASE_DN.get(), DN.NULL_DN);
577    searchBaseDN.addLongIdentifier("search-base-dn", true);
578    searchBaseDN.addLongIdentifier("baseDN", true);
579    searchBaseDN.addLongIdentifier("base-dn", true);
580    searchBaseDN.setArgumentGroupName(argGroupData);
581    parser.addArgument(searchBaseDN);
582
583
584    searchPageSize = new IntegerArgument(null, "searchPageSize", false, 1,
585         null, INFO_LDAPDELETE_ARG_DESC_SEARCH_PAGE_SIZE.get(), 1,
586         Integer.MAX_VALUE);
587    searchPageSize.addLongIdentifier("search-page-size", true);
588    searchPageSize.addLongIdentifier("simplePagedResultsPageSize", true);
589    searchPageSize.addLongIdentifier("simple-paged-results-page-size", true);
590    searchPageSize.addLongIdentifier("pageSize", true);
591    searchPageSize.addLongIdentifier("page-size", true);
592    searchPageSize.setArgumentGroupName(argGroupData);
593    parser.addArgument(searchPageSize);
594
595
596    encryptionPassphraseFile = new FileArgument(null,
597         "encryptionPassphraseFile", false, 1, null,
598         INFO_LDAPDELETE_ARG_DESC_ENCRYPTION_PW_FILE.get(), true, true, true,
599         false);
600    encryptionPassphraseFile.addLongIdentifier("encryption-passphrase-file",
601         true);
602    encryptionPassphraseFile.addLongIdentifier("encryptionPasswordFile", true);
603    encryptionPassphraseFile.addLongIdentifier("encryption-password-file",
604         true);
605    encryptionPassphraseFile.addLongIdentifier("encryptionPINFile", true);
606    encryptionPassphraseFile.addLongIdentifier("encryption-pin-file", true);
607    encryptionPassphraseFile.setArgumentGroupName(argGroupData);
608    parser.addArgument(encryptionPassphraseFile);
609
610
611    characterSet = new StringArgument('i', "characterSet", false, 1,
612         INFO_LDAPDELETE_ARG_PLACEHOLDER_CHARSET.get(),
613         INFO_LDAPDELETE_ARG_DESC_CHARSET.get(), "UTF-8");
614    characterSet.addLongIdentifier("character-set", true);
615    characterSet.addLongIdentifier("charSet", true);
616    characterSet.addLongIdentifier("char-set", true);
617    characterSet.addLongIdentifier("encoding", true);
618    characterSet.setArgumentGroupName(argGroupData);
619    parser.addArgument(characterSet);
620
621
622    rejectFile = new FileArgument('R', "rejectFile", false, 1, null,
623         INFO_LDAPDELETE_ARG_DESC_REJECT_FILE.get(), false, true, true, false);
624    rejectFile.addLongIdentifier("reject-file", true);
625    rejectFile.addLongIdentifier("errorFile", true);
626    rejectFile.addLongIdentifier("error-file", true);
627    rejectFile.addLongIdentifier("failureFile", true);
628    rejectFile.addLongIdentifier("failure-file", true);
629    rejectFile.setArgumentGroupName(argGroupData);
630    parser.addArgument(rejectFile);
631
632
633    verbose = new BooleanArgument('v', "verbose", 1,
634         INFO_LDAPDELETE_ARG_DESC_VERBOSE.get());
635    verbose.setArgumentGroupName(argGroupData);
636    parser.addArgument(verbose);
637
638    // This argument has no effect.  It is provided for compatibility with a
639    // legacy ldapdelete tool, where the argument was also offered but had no
640    // effect.  In this tool, it is hidden.
641    final BooleanArgument scriptFriendly = new BooleanArgument(null,
642         "scriptFriendly", 1, INFO_LDAPDELETE_ARG_DESC_SCRIPT_FRIENDLY.get());
643    scriptFriendly.addLongIdentifier("script-friendly", true);
644    scriptFriendly.setArgumentGroupName(argGroupData);
645    scriptFriendly.setHidden(true);
646    parser.addArgument(scriptFriendly);
647
648
649
650    //
651    // Operation Arguments
652    //
653
654    final String argGroupOp = INFO_LDAPDELETE_ARG_GROUP_OPERATION.get();
655
656    // NOTE:  The retryFailedOperations argument is now hidden, as we will retry
657    // operations by default.  The neverRetry argument can be used to disable
658    // this.
659    retryFailedOperations = new BooleanArgument(null, "retryFailedOperations",
660         1, INFO_LDAPDELETE_ARG_DESC_RETRY_FAILED_OPS.get());
661    retryFailedOperations.addLongIdentifier("retry-failed-operations", true);
662    retryFailedOperations.addLongIdentifier("retryFailedOps", true);
663    retryFailedOperations.addLongIdentifier("retry-failed-ops", true);
664    retryFailedOperations.addLongIdentifier("retry", true);
665    retryFailedOperations.setArgumentGroupName(argGroupOp);
666    retryFailedOperations.setHidden(true);
667    parser.addArgument(retryFailedOperations);
668
669
670    neverRetry = new BooleanArgument(null, "neverRetry", 1,
671         INFO_LDAPDELETE_ARG_DESC_NEVER_RETRY.get());
672    neverRetry.addLongIdentifier("never-retry", true);
673    neverRetry.setArgumentGroupName(argGroupOp);
674    parser.addArgument(neverRetry);
675
676
677    dryRun = new BooleanArgument('n', "dryRun", 1,
678         INFO_LDAPDELETE_ARG_DESC_DRY_RUN.get());
679    dryRun.addLongIdentifier("dry-run", true);
680    dryRun.setArgumentGroupName(argGroupOp);
681    parser.addArgument(dryRun);
682
683
684    continueOnError = new BooleanArgument('c', "continueOnError", 1,
685         INFO_LDAPDELETE_ARG_DESC_CONTINUE_ON_ERROR.get());
686    continueOnError.addLongIdentifier("continue-on-error", true);
687    continueOnError.setArgumentGroupName(argGroupOp);
688    parser.addArgument(continueOnError);
689
690
691    followReferrals = new BooleanArgument(null, "followReferrals", 1,
692         INFO_LDAPDELETE_ARG_DESC_FOLLOW_REFERRALS.get());
693    followReferrals.addLongIdentifier("follow-referrals", true);
694    followReferrals.setArgumentGroupName(argGroupOp);
695    parser.addArgument(followReferrals);
696
697
698    useAdministrativeSession = new BooleanArgument(null,
699         "useAdministrativeSession", 1,
700         INFO_LDAPDELETE_ARG_DESC_USE_ADMIN_SESSION.get());
701    useAdministrativeSession.addLongIdentifier("use-administrative-session",
702         true);
703    useAdministrativeSession.addLongIdentifier("useAdminSession", true);
704    useAdministrativeSession.addLongIdentifier("use-admin-session", true);
705    useAdministrativeSession.setArgumentGroupName(argGroupOp);
706    parser.addArgument(useAdministrativeSession);
707
708
709    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
710         INFO_LDAPDELETE_ARG_PLACEHOLDER_RATE_PER_SECOND.get(),
711         INFO_LDAPDELETE_ARG_DESC_RATE_PER_SECOND.get(), 1, Integer.MAX_VALUE);
712    ratePerSecond.addLongIdentifier("rate-per-second", true);
713    ratePerSecond.addLongIdentifier("deletesPerSecond", true);
714    ratePerSecond.addLongIdentifier("deletes-per-second", true);
715    ratePerSecond.addLongIdentifier("operationsPerSecond", true);
716    ratePerSecond.addLongIdentifier("operations-per-second", true);
717    ratePerSecond.addLongIdentifier("opsPerSecond", true);
718    ratePerSecond.addLongIdentifier("ops-per-second", true);
719    ratePerSecond.setArgumentGroupName(argGroupOp);
720    parser.addArgument(ratePerSecond);
721
722
723    // This argument has no effect.  It is provided for compatibility with a
724    // legacy ldapdelete tool, but this version only supports LDAPv3, so this
725    // argument is hidden.
726    final IntegerArgument ldapVersion = new IntegerArgument('V', "ldapVersion",
727         false, 1, "{version}", INFO_LDAPDELETE_ARG_DESC_LDAP_VERSION.get(),
728         3, 3, 3);
729    ldapVersion.addLongIdentifier("ldap-version", true);
730    ldapVersion.setArgumentGroupName(argGroupOp);
731    ldapVersion.setHidden(true);
732    parser.addArgument(ldapVersion);
733
734
735
736    //
737    // Control Arguments
738    //
739
740    final String argGroupControls = INFO_LDAPDELETE_ARG_GROUP_CONTROLS.get();
741
742    clientSideSubtreeDelete = new BooleanArgument(null,
743         "clientSideSubtreeDelete", 1,
744         INFO_LDAPDELETE_ARG_DESC_CLIENT_SIDE_SUB_DEL.get());
745    clientSideSubtreeDelete.addLongIdentifier("client-side-subtree-delete",
746         true);
747    clientSideSubtreeDelete.setArgumentGroupName(argGroupControls);
748    parser.addArgument(clientSideSubtreeDelete);
749
750
751    serverSideSubtreeDelete = new BooleanArgument('x',
752         "serverSideSubtreeDelete", 1,
753         INFO_LDAPDELETE_ARG_DESC_SERVER_SIDE_SUB_DEL.get());
754    serverSideSubtreeDelete.addLongIdentifier("server-side-subtree-delete",
755         true);
756    serverSideSubtreeDelete.addLongIdentifier("deleteSubtree", true);
757    serverSideSubtreeDelete.addLongIdentifier("delete-subtree", true);
758    serverSideSubtreeDelete.addLongIdentifier("useSubtreeDeleteControl", true);
759    serverSideSubtreeDelete.addLongIdentifier("use-subtree-delete-control",
760         true);
761    serverSideSubtreeDelete.setArgumentGroupName(argGroupControls);
762    parser.addArgument(serverSideSubtreeDelete);
763
764
765    softDelete = new BooleanArgument('s', "softDelete", 1,
766         INFO_LDAPDELETE_ARG_DESC_SOFT_DELETE.get());
767    softDelete.addLongIdentifier("soft-delete", true);
768    softDelete.addLongIdentifier("useSoftDelete", true);
769    softDelete.addLongIdentifier("use-soft-delete", true);
770    softDelete.addLongIdentifier("useSoftDeleteControl", true);
771    softDelete.addLongIdentifier("use-soft-delete-control", true);
772    softDelete.setArgumentGroupName(argGroupControls);
773    parser.addArgument(softDelete);
774
775
776    hardDelete = new BooleanArgument(null, "hardDelete", 1,
777         INFO_LDAPDELETE_ARG_DESC_HARD_DELETE.get());
778    hardDelete.addLongIdentifier("hard-delete", true);
779    hardDelete.addLongIdentifier("useHardDelete", true);
780    hardDelete.addLongIdentifier("use-hard-delete", true);
781    hardDelete.addLongIdentifier("useHardDeleteControl", true);
782    hardDelete.addLongIdentifier("use-hard-delete-control", true);
783    hardDelete.setArgumentGroupName(argGroupControls);
784    parser.addArgument(hardDelete);
785
786
787    proxyAs = new StringArgument('Y', "proxyAs", false, 1,
788         INFO_LDAPDELETE_ARG_PLACEHOLDER_AUTHZ_ID.get(),
789         INFO_LDAPDELETE_ARG_DESC_PROXY_AS.get());
790    proxyAs.addLongIdentifier("proxy-as", true);
791    proxyAs.addLongIdentifier("proxyV2As", true);
792    proxyAs.addLongIdentifier("proxy-v2-as", true);
793    proxyAs.addLongIdentifier("proxiedAuth", true);
794    proxyAs.addLongIdentifier("proxied-auth", true);
795    proxyAs.addLongIdentifier("proxiedAuthorization", true);
796    proxyAs.addLongIdentifier("proxied-authorization", true);
797    proxyAs.addLongIdentifier("useProxiedAuth", true);
798    proxyAs.addLongIdentifier("use-proxied-auth", true);
799    proxyAs.addLongIdentifier("useProxiedAuthorization", true);
800    proxyAs.addLongIdentifier("use-proxied-authorization", true);
801    proxyAs.addLongIdentifier("useProxiedAuthControl", true);
802    proxyAs.addLongIdentifier("use-proxied-auth-control", true);
803    proxyAs.addLongIdentifier("useProxiedAuthorizationControl", true);
804    proxyAs.addLongIdentifier("use-proxied-authorization-control", true);
805    proxyAs.setArgumentGroupName(argGroupControls);
806    parser.addArgument(proxyAs);
807
808
809    proxyV1As = new DNArgument(null, "proxyV1As", false, 1, null,
810         INFO_LDAPDELETE_ARG_DESC_PROXY_V1_AS.get());
811    proxyV1As.addLongIdentifier("proxy-v1-as", true);
812    proxyV1As.setArgumentGroupName(argGroupControls);
813    parser.addArgument(proxyV1As);
814
815
816    manageDsaIT = new BooleanArgument(null, "useManageDsaIT", 1,
817         INFO_LDAPDELETE_ARG_DESC_MANAGE_DSA_IT.get());
818    manageDsaIT.addLongIdentifier("use-manage-dsa-it", true);
819    manageDsaIT.addLongIdentifier("manageDsaIT", true);
820    manageDsaIT.addLongIdentifier("manage-dsa-it", true);
821    manageDsaIT.addLongIdentifier("manageDsaITControl", true);
822    manageDsaIT.addLongIdentifier("manage-dsa-it-control", true);
823    manageDsaIT.addLongIdentifier("useManageDsaITControl", true);
824    manageDsaIT.addLongIdentifier("use-manage-dsa-it-control", true);
825    manageDsaIT.setArgumentGroupName(argGroupControls);
826    parser.addArgument(manageDsaIT);
827
828
829    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
830         null, INFO_LDAPDELETE_ARG_DESC_ASSERTION_FILTER.get());
831    assertionFilter.addLongIdentifier("assertion-filter", true);
832    assertionFilter.addLongIdentifier("useAssertionFilter", true);
833    assertionFilter.addLongIdentifier("use-assertion-filter", true);
834    assertionFilter.addLongIdentifier("assertionControl", true);
835    assertionFilter.addLongIdentifier("assertion-control", true);
836    assertionFilter.addLongIdentifier("useAssertionControl", true);
837    assertionFilter.addLongIdentifier("use-assertion-control", true);
838    assertionFilter.setArgumentGroupName(argGroupControls);
839    parser.addArgument(assertionFilter);
840
841
842    preReadAttribute = new StringArgument(null, "preReadAttribute", false, 0,
843         INFO_LDAPDELETE_ARG_PLACEHOLDER_ATTR.get(),
844         INFO_LDAPDELETE_ARG_DESC_PRE_READ_ATTR.get());
845    preReadAttribute.addLongIdentifier("pre-read-attribute", true);
846    preReadAttribute.setArgumentGroupName(argGroupControls);
847    parser.addArgument(preReadAttribute);
848
849
850    noOperation = new BooleanArgument(null, "noOperation", 1,
851         INFO_LDAPDELETE_ARG_DESC_NO_OP.get());
852    noOperation.addLongIdentifier("no-operation", true);
853    noOperation.addLongIdentifier("noOp", true);
854    noOperation.addLongIdentifier("no-op", true);
855    noOperation.setArgumentGroupName(argGroupControls);
856    parser.addArgument(noOperation);
857
858
859    getBackendSetID = new BooleanArgument(null, "getBackendSetID", 1,
860         INFO_LDAPDELETE_ARG_DESC_GET_BACKEND_SET_ID.get());
861    getBackendSetID.addLongIdentifier("get-backend-set-id", true);
862    getBackendSetID.addLongIdentifier("useGetBackendSetID", true);
863    getBackendSetID.addLongIdentifier("use-get-backend-set-id", true);
864    getBackendSetID.addLongIdentifier("useGetBackendSetIDControl", true);
865    getBackendSetID.addLongIdentifier("use-get-backend-set-id-control", true);
866    getBackendSetID.setArgumentGroupName(argGroupControls);
867    parser.addArgument(getBackendSetID);
868
869
870    routeToBackendSet = new StringArgument(null, "routeToBackendSet", false, 0,
871         INFO_LDAPDELETE_ARG_PLACEHOLDER_ROUTE_TO_BACKEND_SET.get(),
872         INFO_LDAPDELETE_ARG_DESC_ROUTE_TO_BACKEND_SET.get());
873    routeToBackendSet.addLongIdentifier("route-to-backend-set", true);
874    routeToBackendSet.addLongIdentifier("useRouteToBackendSet", true);
875    routeToBackendSet.addLongIdentifier("use0route-to-backend-set", true);
876    routeToBackendSet.addLongIdentifier("useRouteToBackendSetControl", true);
877    routeToBackendSet.addLongIdentifier("use-route-to-backend-set-control",
878         true);
879    routeToBackendSet.setArgumentGroupName(argGroupControls);
880    parser.addArgument(routeToBackendSet);
881
882
883    getServerID = new BooleanArgument(null, "getServerID", 1,
884         INFO_LDAPDELETE_ARG_DESC_GET_SERVER_ID.get());
885    getServerID.addLongIdentifier("get-server-id", true);
886    getServerID.addLongIdentifier("getBackendServerID", true);
887    getServerID.addLongIdentifier("get-backend-server-id", true);
888    getServerID.addLongIdentifier("useGetServerID", true);
889    getServerID.addLongIdentifier("use-get-server-id", true);
890    getServerID.addLongIdentifier("useGetServerIDControl", true);
891    getServerID.addLongIdentifier("use-get-server-id-control", true);
892    getServerID.setArgumentGroupName(argGroupControls);
893    parser.addArgument(getServerID);
894
895
896    routeToServer = new StringArgument(null, "routeToServer", false, 1,
897         INFO_LDAPDELETE_ARG_PLACEHOLDER_ID.get(),
898         INFO_LDAPDELETE_ARG_DESC_ROUTE_TO_SERVER.get());
899    routeToServer.addLongIdentifier("route-to-server", true);
900    routeToServer.addLongIdentifier("routeToBackendServer", true);
901    routeToServer.addLongIdentifier("route-to-backend-server", true);
902    routeToServer.addLongIdentifier("useRouteToServer", true);
903    routeToServer.addLongIdentifier("use-route-to-server", true);
904    routeToServer.addLongIdentifier("useRouteToBackendServer", true);
905    routeToServer.addLongIdentifier("use-route-to-backend-server", true);
906    routeToServer.addLongIdentifier("useRouteToServerControl", true);
907    routeToServer.addLongIdentifier("use-route-to-server-control", true);
908    routeToServer.addLongIdentifier("useRouteToBackendServerControl", true);
909    routeToServer.addLongIdentifier("use-route-to-backend-server-control",
910         true);
911    routeToServer.setArgumentGroupName(argGroupControls);
912    parser.addArgument(routeToServer);
913
914
915    useAssuredReplication = new BooleanArgument(null, "useAssuredReplication",
916         1, INFO_LDAPDELETE_ARG_DESC_USE_ASSURED_REPLICATION.get());
917    useAssuredReplication.addLongIdentifier("use-assured-replication", true);
918    useAssuredReplication.addLongIdentifier("assuredReplication", true);
919    useAssuredReplication.addLongIdentifier("assured-replication", true);
920    useAssuredReplication.addLongIdentifier("assuredReplicationControl", true);
921    useAssuredReplication.addLongIdentifier("assured-replication-control",
922         true);
923    useAssuredReplication.addLongIdentifier("useAssuredReplicationControl",
924         true);
925    useAssuredReplication.addLongIdentifier("use-assured-replication-control",
926         true);
927    useAssuredReplication.setArgumentGroupName(argGroupControls);
928    parser.addArgument(useAssuredReplication);
929
930
931    assuredReplicationLocalLevel = new StringArgument(null,
932         "assuredReplicationLocalLevel", false, 1,
933         INFO_LDAPDELETE_ARG_PLACEHOLDER_ASSURED_REPLICATION_LOCAL_LEVEL.get(),
934         INFO_LDAPDELETE_ARG_DESC_ASSURED_REPLICATION_LOCAL_LEVEL.get(),
935         StaticUtils.setOf(
936              "none",
937              "received-any-server",
938              "processed-all-servers"));
939    assuredReplicationLocalLevel.addLongIdentifier(
940         "assured-replication-local-level", true);
941    assuredReplicationLocalLevel.setArgumentGroupName(argGroupControls);
942    parser.addArgument(assuredReplicationLocalLevel);
943
944
945    assuredReplicationRemoteLevel = new StringArgument(null,
946         "assuredReplicationRemoteLevel", false, 1,
947         INFO_LDAPDELETE_ARG_PLACEHOLDER_ASSURED_REPLICATION_REMOTE_LEVEL.get(),
948         INFO_LDAPDELETE_ARG_DESC_ASSURED_REPLICATION_REMOTE_LEVEL.get(),
949         StaticUtils.setOf(
950              "none",
951              "received-any-remote-location",
952              "received-all-remote-locations",
953              "processed-all-remote-servers"));
954    assuredReplicationRemoteLevel.addLongIdentifier(
955         "assured-replication-remote-level", true);
956    assuredReplicationRemoteLevel.setArgumentGroupName(argGroupControls);
957    parser.addArgument(assuredReplicationRemoteLevel);
958
959
960    assuredReplicationTimeout = new DurationArgument(null,
961         "assuredReplicationTimeout", false, null,
962         INFO_LDAPDELETE_ARG_DESC_ASSURED_REPLICATION_TIMEOUT.get());
963    assuredReplicationTimeout.addLongIdentifier("assured-replication-timeout",
964         true);
965    assuredReplicationTimeout.setArgumentGroupName(argGroupControls);
966    parser.addArgument(assuredReplicationTimeout);
967
968
969    replicationRepair = new BooleanArgument(null, "replicationRepair", 1,
970         INFO_LDAPDELETE_ARG_DESC_REPLICATION_REPAIR.get());
971    replicationRepair.addLongIdentifier("replication-repair", true);
972    replicationRepair.addLongIdentifier("replicationRepairControl", true);
973    replicationRepair.addLongIdentifier("replication-repair-control", true);
974    replicationRepair.addLongIdentifier("useReplicationRepair", true);
975    replicationRepair.addLongIdentifier("use-replication-repair", true);
976    replicationRepair.addLongIdentifier("useReplicationRepairControl", true);
977    replicationRepair.addLongIdentifier("use-replication-repair-control", true);
978    replicationRepair.setArgumentGroupName(argGroupControls);
979    parser.addArgument(replicationRepair);
980
981
982    suppressReferentialIntegrityUpdates = new BooleanArgument(null,
983         "suppressReferentialIntegrityUpdates", 1,
984         INFO_LDAPDELETE_ARG_DESC_SUPPRESS_REFINT_UPDATES.get());
985    suppressReferentialIntegrityUpdates.addLongIdentifier(
986         "suppress-referential-integrity-updates", true);
987    suppressReferentialIntegrityUpdates.addLongIdentifier(
988         "useSuppressReferentialIntegrityUpdates", true);
989    suppressReferentialIntegrityUpdates.addLongIdentifier(
990         "use-suppress-referential-integrity-updates", true);
991    suppressReferentialIntegrityUpdates.addLongIdentifier(
992         "useSuppressReferentialIntegrityUpdatesControl", true);
993    suppressReferentialIntegrityUpdates.addLongIdentifier(
994         "use-suppress-referential-integrity-updates-control", true);
995    suppressReferentialIntegrityUpdates.setArgumentGroupName(argGroupControls);
996    parser.addArgument(suppressReferentialIntegrityUpdates);
997
998
999    operationPurpose = new StringArgument(null, "operationPurpose", false, 1,
1000         null, INFO_LDAPDELETE_ARG_DESC_OP_PURPOSE.get());
1001    operationPurpose.addLongIdentifier("operation-purpose", true);
1002    operationPurpose.addLongIdentifier("operationPurposeControl", true);
1003    operationPurpose.addLongIdentifier("operation-purpose-control", true);
1004    operationPurpose.addLongIdentifier("useOperationPurpose", true);
1005    operationPurpose.addLongIdentifier("use-operation-purpose", true);
1006    operationPurpose.addLongIdentifier("useOperationPurposeControl", true);
1007    operationPurpose.addLongIdentifier("use-operation-purpose-control", true);
1008    operationPurpose.setArgumentGroupName(argGroupControls);
1009    parser.addArgument(operationPurpose);
1010
1011
1012    authorizationIdentity = new BooleanArgument('E', "authorizationIdentity",
1013         1, INFO_LDAPDELETE_ARG_DESC_AUTHZ_ID.get());
1014    authorizationIdentity.addLongIdentifier("authorization-identity", true);
1015    authorizationIdentity.addLongIdentifier("useAuthorizationIdentity", true);
1016    authorizationIdentity.addLongIdentifier("use-authorization-identity", true);
1017    authorizationIdentity.addLongIdentifier(
1018         "useAuthorizationIdentityControl", true);
1019    authorizationIdentity.addLongIdentifier(
1020         "use-authorization-identity-control", true);
1021    authorizationIdentity.setArgumentGroupName(argGroupControls);
1022    parser.addArgument(authorizationIdentity);
1023
1024
1025    getAuthorizationEntryAttribute = new StringArgument(null,
1026         "getAuthorizationEntryAttribute", false, 0,
1027         INFO_LDAPDELETE_ARG_PLACEHOLDER_ATTR.get(),
1028         INFO_LDAPDELETE_ARG_DESC_GET_AUTHZ_ENTRY_ATTR.get());
1029    getAuthorizationEntryAttribute.addLongIdentifier(
1030         "get-authorization-entry-attribute", true);
1031    getAuthorizationEntryAttribute.setArgumentGroupName(argGroupControls);
1032    parser.addArgument(getAuthorizationEntryAttribute);
1033
1034
1035    getUserResourceLimits = new BooleanArgument(null, "getUserResourceLimits",
1036         1, INFO_LDAPDELETE_ARG_DESC_GET_USER_RESOURCE_LIMITS.get());
1037    getUserResourceLimits.addLongIdentifier("get-user-resource-limits", true);
1038    getUserResourceLimits.addLongIdentifier("getUserResourceLimitsControl",
1039         true);
1040    getUserResourceLimits.addLongIdentifier("get-user-resource-limits-control",
1041         true);
1042    getUserResourceLimits.addLongIdentifier("useGetUserResourceLimits", true);
1043    getUserResourceLimits.addLongIdentifier("use-get-user-resource-limits",
1044         true);
1045    getUserResourceLimits.addLongIdentifier(
1046         "useGetUserResourceLimitsControl", true);
1047    getUserResourceLimits.addLongIdentifier(
1048         "use-get-user-resource-limits-control", true);
1049    getUserResourceLimits.setArgumentGroupName(argGroupControls);
1050    parser.addArgument(getUserResourceLimits);
1051
1052
1053    deleteControl = new ControlArgument('J', "deleteControl", false, 0, null,
1054         INFO_LDAPDELETE_ARG_DESC_DELETE_CONTROL.get());
1055    deleteControl.addLongIdentifier("delete-control", true);
1056    deleteControl.addLongIdentifier("operationControl", true);
1057    deleteControl.addLongIdentifier("operation-control", true);
1058    deleteControl.addLongIdentifier("control", true);
1059    deleteControl.setArgumentGroupName(argGroupControls);
1060    parser.addArgument(deleteControl);
1061
1062
1063    bindControl = new ControlArgument(null, "bindControl", false, 0, null,
1064         INFO_LDAPDELETE_ARG_DESC_BIND_CONTROL.get());
1065    bindControl.addLongIdentifier("bind-control", true);
1066    bindControl.setArgumentGroupName(argGroupControls);
1067    parser.addArgument(bindControl);
1068
1069
1070
1071    //
1072    // Argument Constraints
1073    //
1074
1075    // At most one argument may be provided to select the entries to delete.
1076    parser.addExclusiveArgumentSet(entryDN, dnFile, deleteEntriesMatchingFilter,
1077         deleteEntriesMatchingFiltersFromFile);
1078
1079    // The searchBaseDN argument can only be used if identifying entries with
1080    // search filters.
1081    parser.addDependentArgumentSet(searchBaseDN, deleteEntriesMatchingFilter,
1082         deleteEntriesMatchingFiltersFromFile);
1083
1084    // The search page size argument can only be used if identifying entries
1085    // with search filters or performing a client-side subtree delete.
1086    parser.addDependentArgumentSet(searchPageSize, deleteEntriesMatchingFilter,
1087         deleteEntriesMatchingFiltersFromFile, clientSideSubtreeDelete);
1088
1089    // Follow referrals and manage DSA IT can't be used together.
1090    parser.addExclusiveArgumentSet(followReferrals, manageDsaIT);
1091
1092    // Client-side and server-side subtree delete can't be used together.
1093    parser.addExclusiveArgumentSet(clientSideSubtreeDelete,
1094         serverSideSubtreeDelete);
1095
1096    // A lot of options can't be used in conjunction with client-side
1097    // subtree delete.
1098    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, followReferrals);
1099    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, preReadAttribute);
1100    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, getBackendSetID);
1101    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, getServerID);
1102    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, noOperation);
1103    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, dryRun);
1104
1105    // Soft delete and hard delete can't be used together.
1106    parser.addExclusiveArgumentSet(softDelete, hardDelete);
1107  }
1108
1109
1110
1111  /**
1112   * {@inheritDoc}
1113   */
1114  @Override()
1115  public void doExtendedNonLDAPArgumentValidation()
1116         throws ArgumentException
1117  {
1118    // Trailing arguments can only be used if none of the other arguments used
1119    // to identify entries to delete are provided.
1120    if (! parser.getTrailingArguments().isEmpty())
1121    {
1122      for (final Argument a :
1123           Arrays.asList(entryDN, dnFile, deleteEntriesMatchingFilter,
1124                deleteEntriesMatchingFiltersFromFile))
1125      {
1126        if (a.isPresent())
1127        {
1128          throw new ArgumentException(
1129               ERR_LDAPDELETE_TRAILING_ARG_CONFLICT.get(
1130                    a.getIdentifierString()));
1131        }
1132      }
1133    }
1134
1135
1136    // If we should use the route to backend set request control, then validate
1137    // and pre-create those controls.
1138    if (routeToBackendSet.isPresent())
1139    {
1140      final List<String> values = routeToBackendSet.getValues();
1141      final Map<String,List<String>> idsByRP = new LinkedHashMap<>(
1142           StaticUtils.computeMapCapacity(values.size()));
1143      for (final String value : values)
1144      {
1145        final int colonPos = value.indexOf(':');
1146        if (colonPos <= 0)
1147        {
1148          throw new ArgumentException(
1149               ERR_LDAPDELETE_ROUTE_TO_BACKEND_SET_INVALID_FORMAT.get(value,
1150                    routeToBackendSet.getIdentifierString()));
1151        }
1152
1153        final String rpID = value.substring(0, colonPos);
1154        final String bsID = value.substring(colonPos+1);
1155
1156        List<String> idsForRP = idsByRP.get(rpID);
1157        if (idsForRP == null)
1158        {
1159          idsForRP = new ArrayList<>(values.size());
1160          idsByRP.put(rpID, idsForRP);
1161        }
1162        idsForRP.add(bsID);
1163      }
1164
1165      for (final Map.Entry<String,List<String>> e : idsByRP.entrySet())
1166      {
1167        final String rpID = e.getKey();
1168        final List<String> bsIDs = e.getValue();
1169        routeToBackendSetRequestControls.add(
1170             RouteToBackendSetRequestControl.createAbsoluteRoutingRequest(
1171                  true, rpID, bsIDs));
1172      }
1173    }
1174  }
1175
1176
1177
1178  /**
1179   * {@inheritDoc}
1180   */
1181  @Override()
1182  @NotNull()
1183  protected List<Control> getBindControls()
1184  {
1185    final ArrayList<Control> bindControls = new ArrayList<>(10);
1186
1187    if (bindControl.isPresent())
1188    {
1189      bindControls.addAll(bindControl.getValues());
1190    }
1191
1192    if (authorizationIdentity.isPresent())
1193    {
1194      bindControls.add(new AuthorizationIdentityRequestControl(true));
1195    }
1196
1197    if (getAuthorizationEntryAttribute.isPresent())
1198    {
1199      bindControls.add(new GetAuthorizationEntryRequestControl(true, true,
1200           getAuthorizationEntryAttribute.getValues()));
1201    }
1202
1203    if (getUserResourceLimits.isPresent())
1204    {
1205      bindControls.add(new GetUserResourceLimitsRequestControl(true));
1206    }
1207
1208    return bindControls;
1209  }
1210
1211
1212
1213  /**
1214   * {@inheritDoc}
1215   */
1216  @Override()
1217  protected boolean supportsMultipleServers()
1218  {
1219    // We will support providing information about multiple servers.  This tool
1220    // will not communicate with multiple servers concurrently, but it can
1221    // accept information about multiple servers in the event that a large set
1222    // of changes is to be processed and a server goes down in the middle of
1223    // those changes.  In this case, we can resume processing on a newly-created
1224    // connection, possibly to a different server.
1225    return true;
1226  }
1227
1228
1229
1230  /**
1231   * {@inheritDoc}
1232   */
1233  @Override()
1234  @NotNull()
1235  public LDAPConnectionOptions getConnectionOptions()
1236  {
1237    final LDAPConnectionOptions options = new LDAPConnectionOptions();
1238
1239    options.setUseSynchronousMode(true);
1240    options.setFollowReferrals(followReferrals.isPresent());
1241    options.setUnsolicitedNotificationHandler(this);
1242    options.setResponseTimeoutMillis(0L);
1243
1244    return options;
1245  }
1246
1247
1248
1249  /**
1250   * {@inheritDoc}
1251   */
1252  @Override()
1253  @NotNull()
1254  public ResultCode doToolProcessing()
1255  {
1256    // Get the controls that should be included in search and delete requests.
1257    searchControls = getSearchControls();
1258    deleteControls = getDeleteControls();
1259
1260    // If the ratePerSecond argument was provided, then create the fixed-rate
1261    // barrier.
1262    if (ratePerSecond.isPresent())
1263    {
1264      deleteRateLimiter = new FixedRateBarrier(1000L, ratePerSecond.getValue());
1265    }
1266
1267    // Create a subtree deleter instance if appropriate.
1268    if (clientSideSubtreeDelete.isPresent())
1269    {
1270      subtreeDeleter = new SubtreeDeleter();
1271      subtreeDeleter.setAdditionalSearchControls(searchControls);
1272      subtreeDeleter.setAdditionalSearchControls(deleteControls);
1273      subtreeDeleter.setDeleteRateLimiter(deleteRateLimiter);
1274      if (searchPageSize.isPresent())
1275      {
1276        subtreeDeleter.setSimplePagedResultsPageSize(searchPageSize.getValue());
1277      }
1278    }
1279
1280    // If the encryptionPassphraseFile argument was provided, then read that
1281    // passphrase.
1282    final char[] encryptionPassphrase;
1283    if (encryptionPassphraseFile.isPresent())
1284    {
1285      try
1286      {
1287        encryptionPassphrase = getPasswordFileReader().readPassword(
1288             encryptionPassphraseFile.getValue());
1289      }
1290      catch (final LDAPException e)
1291      {
1292        Debug.debugException(e);
1293        commentToErr(e.getMessage());
1294        return e.getResultCode();
1295      }
1296      catch (final Exception e)
1297      {
1298        Debug.debugException(e);
1299        commentToErr(ERR_LDAPDELETE_CANNOT_READ_ENCRYPTION_PW_FILE.get(
1300             encryptionPassphraseFile.getValue().getAbsolutePath(),
1301             StaticUtils.getExceptionMessage(e)));
1302        return ResultCode.LOCAL_ERROR;
1303      }
1304    }
1305    else
1306    {
1307      encryptionPassphrase = null;
1308    }
1309
1310
1311    // If the character set argument was specified, then make sure it's valid.
1312    final Charset charset;
1313    try
1314    {
1315      charset = Charset.forName(characterSet.getValue());
1316    }
1317    catch (final Exception e)
1318    {
1319      Debug.debugException(e);
1320      commentToErr(ERR_LDAPDELETE_UNSUPPORTED_CHARSET.get(
1321           characterSet.getValue()));
1322      return ResultCode.PARAM_ERROR;
1323    }
1324
1325
1326    // Get the connection pool.
1327    final StartAdministrativeSessionPostConnectProcessor p;
1328    if (useAdministrativeSession.isPresent())
1329    {
1330      p = new StartAdministrativeSessionPostConnectProcessor(
1331           new StartAdministrativeSessionExtendedRequest(getToolName(),
1332                true));
1333    }
1334    else
1335    {
1336      p = null;
1337    }
1338
1339    try
1340    {
1341      connectionPool = getConnectionPool(1, 2, 0, p, null, true,
1342           new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
1343                verbose.isPresent()));
1344      connectionPool.setRetryFailedOperationsDueToInvalidConnections(
1345           (! neverRetry.isPresent()));
1346    }
1347    catch (final LDAPException e)
1348    {
1349      Debug.debugException(e);
1350
1351      // Unable to create the connection pool, which means that either the
1352      // connection could not be established or the attempt to authenticate
1353      // the connection failed.  If the bind failed, then the report bind
1354      // result health check should have already reported the bind failure.
1355      // If the failure was something else, then display that failure result.
1356      if (e.getResultCode() != ResultCode.INVALID_CREDENTIALS)
1357      {
1358        for (final String line :
1359             ResultUtils.formatResult(e, true, 0, WRAP_COLUMN))
1360        {
1361          err(line);
1362        }
1363      }
1364      return e.getResultCode();
1365    }
1366
1367
1368    // Figure out the method that we'll identify the entries to delete and
1369    // take the appropriate action.
1370    final AtomicReference<ResultCode> returnCode = new AtomicReference<>();
1371    if (entryDN.isPresent())
1372    {
1373      deleteFromEntryDNArgument(returnCode);
1374    }
1375    else if (dnFile.isPresent())
1376    {
1377      deleteFromDNFile(returnCode, charset, encryptionPassphrase);
1378    }
1379    else if (deleteEntriesMatchingFilter.isPresent())
1380    {
1381      deleteFromFilters(returnCode);
1382    }
1383    else if (deleteEntriesMatchingFiltersFromFile.isPresent())
1384    {
1385      deleteFromFilterFile(returnCode, charset, encryptionPassphrase);
1386    }
1387    else if (! parser.getTrailingArguments().isEmpty())
1388    {
1389      deleteFromTrailingArguments(returnCode);
1390    }
1391    else
1392    {
1393      deleteFromStandardInput(returnCode, charset, encryptionPassphrase);
1394    }
1395
1396
1397    // Close the reject writer.
1398    final LDIFWriter rw = rejectWriter.get();
1399    if (rw != null)
1400    {
1401      try
1402      {
1403        rw.close();
1404      }
1405      catch (final Exception e)
1406      {
1407        Debug.debugException(e);
1408        commentToErr(ERR_LDAPDELETE_ERROR_CLOSING_REJECT_WRITER.get(
1409             rejectFile.getValue().getAbsolutePath(),
1410             StaticUtils.getExceptionMessage(e)));
1411        returnCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
1412      }
1413    }
1414
1415
1416    // Close the connection pool.
1417    connectionPool.close();
1418
1419
1420    returnCode.compareAndSet(null, ResultCode.SUCCESS);
1421    return returnCode.get();
1422  }
1423
1424
1425
1426  /**
1427   * Deletes entries whose DNs are specified in the entryDN argument.
1428   *
1429   * @param  returnCode  A reference that should be updated with the result code
1430   *                     from the first failure that is encountered.  It must
1431   *                     not be {@code null}, but may be unset.
1432   */
1433  private void deleteFromEntryDNArgument(
1434                    @NotNull final AtomicReference<ResultCode> returnCode)
1435  {
1436    for (final DN dn : entryDN.getValues())
1437    {
1438      if ((! deleteEntry(dn.toString(), returnCode)) &&
1439           (! continueOnError.isPresent()))
1440      {
1441        return;
1442      }
1443    }
1444  }
1445
1446
1447
1448  /**
1449   * Deletes entries whose DNs are contained in the files provided to the dnFile
1450   * argument.
1451   *
1452   * @param  returnCode            A reference that should be updated with the
1453   *                               result code from the first failure that is
1454   *                               encountered.  It must not be {@code null},
1455   *                               but may be unset.
1456   * @param  charset               The character set to use when reading the
1457   *                               data from the file.  It must not be
1458   *                               {@code null}.
1459   * @param  encryptionPassphrase  The passphrase to use to decrypt the data
1460   *                               read from the file if it happens to be
1461   *                               encrypted.  This may be {@code null} if the
1462   *                               user should be interactively prompted for the
1463   *                               passphrase if a file happens to be encrypted.
1464   */
1465  private void deleteFromDNFile(
1466                    @NotNull final AtomicReference<ResultCode> returnCode,
1467                    @NotNull final Charset charset,
1468                    @Nullable final char[] encryptionPassphrase)
1469  {
1470    final List<char[]> potentialPassphrases =
1471         new ArrayList<>(dnFile.getValues().size());
1472    if (encryptionPassphrase != null)
1473    {
1474      potentialPassphrases.add(encryptionPassphrase);
1475    }
1476
1477    for (final File f : dnFile.getValues())
1478    {
1479      if (verbose.isPresent())
1480      {
1481        commentToOut(INFO_LDAPDELETE_READING_DNS_FROM_FILE.get(
1482             f.getAbsolutePath()));
1483        out();
1484      }
1485
1486      try (FileInputStream fis = new FileInputStream(f))
1487      {
1488        if ((! deleteDNsFromInputStream(returnCode, fis, charset,
1489                    potentialPassphrases)) &&
1490             (! continueOnError.isPresent()))
1491        {
1492          return;
1493        }
1494      }
1495      catch (final Exception e)
1496      {
1497        commentToErr(ERR_LDAPDELETE_ERROR_OPENING_DN_FILE.get(
1498             f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
1499        if (! continueOnError.isPresent())
1500        {
1501          return;
1502        }
1503      }
1504    }
1505  }
1506
1507
1508
1509  /**
1510   * Deletes entries whose DNs are read from the provided input stream.
1511   *
1512   * @param  returnCode            A reference that should be updated with the
1513   *                               result code from the first failure that is
1514   *                               encountered.  It must not be {@code null},
1515   *                               but may be unset.
1516   * @param  inputStream           The input stream from which the data is to be
1517   *                               read.
1518   * @param  charset               The character set to use when reading the
1519   *                               data from the input stream.  It must not be
1520   *                               {@code null}.
1521   * @param  potentialPassphrases  A list of the potential passphrases that may
1522   *                               be used to decrypt data read from the
1523   *                               provided input stream.  It must not be
1524   *                               {@code null}, and must be updatable, but may
1525   *                               be empty.
1526   *
1527   * @return  {@code true} if all processing completed successfully, or
1528   *          {@code false} if not.
1529   *
1530   * @throws  IOException  If an error occurs while trying to read data from the
1531   *                       input stream or create the buffered reader.
1532   *
1533   * @throws  GeneralSecurityException  If a problem is encountered while
1534   *                                    attempting to interact with encrypted
1535   *                                    data read from the input stream.
1536   */
1537  private boolean deleteDNsFromInputStream(
1538                       @NotNull final AtomicReference<ResultCode> returnCode,
1539                       @NotNull final InputStream inputStream,
1540                       @NotNull final Charset charset,
1541                       @NotNull final List<char[]> potentialPassphrases)
1542          throws IOException, GeneralSecurityException
1543  {
1544    boolean successful = true;
1545    long lineNumber = 0;
1546
1547    final BufferedReader reader =
1548         getBufferedReader(inputStream, charset, potentialPassphrases);
1549    while (true)
1550    {
1551      final String line = reader.readLine();
1552      lineNumber++;
1553      if (line == null)
1554      {
1555        return successful;
1556      }
1557
1558      if (line.isEmpty() || line.startsWith("#"))
1559      {
1560        // The line is empty or contains a comment.  Ignore it.
1561      }
1562      else
1563      {
1564        // This is the DN of the entry to delete.
1565        if (! deleteDNFromInputStream(returnCode, line))
1566        {
1567          if (continueOnError.isPresent())
1568          {
1569            successful = false;
1570          }
1571          else
1572          {
1573            return false;
1574          }
1575        }
1576      }
1577    }
1578  }
1579
1580
1581
1582  /**
1583   * Extracts the DN of an entry to delete from the provided buffer and tries
1584   * to delete it.  The buffer may contain one of three things:
1585   * <UL>
1586   *   <LI>The bare string representation of a DN.</LI>
1587   *   <LI>The string "dn:" followed by an optional space and the bare string
1588   *       representation of a DN.</LI>
1589   *   <LI>The string "dn::" followed by an optional space and the
1590   *       base64-encoded representation of a DN.</LI>
1591   * </UL>
1592   *
1593   * @param  returnCode  A reference that should be updated with the result code
1594   *                     from the first failure that is encountered.  It must
1595   *                     not be {@code null}, but may be unset.
1596   * @param  rawString   The string representation of the DN to delete.
1597   *
1598   * @return  {@code true} if the buffer was empty or if it contained the DN of
1599   *          an entry that was successfully deleted, or {@code false} if an
1600   *          error occurred while extracting the DN or attempting to delete the
1601   *          target entry.
1602   */
1603  private boolean deleteDNFromInputStream(
1604                       @NotNull final AtomicReference<ResultCode> returnCode,
1605                       @NotNull final String rawString)
1606  {
1607    final String lowerString = StaticUtils.toLowerCase(rawString);
1608    if (lowerString.startsWith("dn::"))
1609    {
1610      final String base64EncodedDN = rawString.substring(4).trim();
1611      if (base64EncodedDN.isEmpty())
1612      {
1613        returnCode.compareAndSet(null, ResultCode.PARAM_ERROR);
1614        commentToErr(ERR_LDAPDELETE_BASE64_DN_EMPTY.get(rawString));
1615        return false;
1616      }
1617
1618      final String base64DecodedDN;
1619      try
1620      {
1621        base64DecodedDN = Base64.decodeToString(base64EncodedDN);
1622      }
1623      catch (final Exception e)
1624      {
1625        Debug.debugException(e);
1626        returnCode.compareAndSet(null, ResultCode.PARAM_ERROR);
1627        commentToErr(ERR_LDAPDELETE_BASE64_DN_NOT_BASE64.get(rawString));
1628        return false;
1629      }
1630
1631      return deleteEntry(base64DecodedDN, returnCode);
1632    }
1633    else if (lowerString.startsWith("dn:"))
1634    {
1635      final String dn = rawString.substring(3).trim();
1636      if (dn.isEmpty())
1637      {
1638        returnCode.compareAndSet(null, ResultCode.PARAM_ERROR);
1639        commentToErr(ERR_LDAPDELETE_DN_EMPTY.get(rawString));
1640        return false;
1641      }
1642
1643      return deleteEntry(dn, returnCode);
1644    }
1645    else
1646    {
1647      return deleteEntry(rawString, returnCode);
1648    }
1649  }
1650
1651
1652
1653  /**
1654   * Creates a buffered reader that can read data from the provided input stream
1655   * using the specified character set.  The data to be read may optionally be
1656   * passphrase-encrypted and/or gzip-compressed.
1657   *
1658   * @param  inputStream           The input stream from which the data is to be
1659   *                               read.
1660   * @param  charset               The character set to use when reading the
1661   *                               data from the input stream.  It must not be
1662   *                               {@code null}.
1663   * @param  potentialPassphrases  A list of the potential passphrases that may
1664   *                               be used to decrypt data read from the
1665   *                               provided input stream.  It must not be
1666   *                               {@code null}, and must be updatable, but may
1667   *                               be empty.
1668   *
1669   * @return  The buffered reader that can be used to read data from the
1670   *          provided input stream.
1671   *
1672   * @throws  IOException  If an error occurs while trying to read data from the
1673   *                       input stream or create the buffered reader.
1674   *
1675   * @throws  GeneralSecurityException  If a problem is encountered while
1676   *                                    attempting to interact with encrypted
1677   *                                    data read from the input stream.
1678   */
1679  @NotNull()
1680  private BufferedReader getBufferedReader(
1681               @NotNull final InputStream inputStream,
1682               @NotNull final Charset charset,
1683               @NotNull final List<char[]> potentialPassphrases)
1684          throws IOException, GeneralSecurityException
1685  {
1686    // Check to see if the input stream is encrypted.  If so, then get access to
1687    // a decrypted representation of its contents.
1688    final ObjectPair<InputStream,char[]> decryptedInputStreamData =
1689         ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream,
1690              potentialPassphrases, (! encryptionPassphraseFile.isPresent()),
1691              INFO_LDAPDELETE_ENCRYPTION_PASSPHRASE_PROMPT.get(),
1692              ERR_LDAPDELETE_ENCRYPTION_PASSPHRASE_ERROR.get(), getOut(),
1693              getErr());
1694    final InputStream decryptedInputStream =
1695         decryptedInputStreamData.getFirst();
1696    final char[] passphrase = decryptedInputStreamData.getSecond();
1697    if (passphrase != null)
1698    {
1699      boolean isExistingPassphrase = false;
1700      for (final char[] existingPassphrase : potentialPassphrases)
1701      {
1702        if (Arrays.equals(passphrase, existingPassphrase))
1703        {
1704          isExistingPassphrase = true;
1705          break;
1706        }
1707      }
1708
1709      if (! isExistingPassphrase)
1710      {
1711        potentialPassphrases.add(passphrase);
1712      }
1713    }
1714
1715
1716    // Check to see if the input stream is compressed.
1717    final InputStream decompressedInputStream =
1718         ToolUtils.getPossiblyGZIPCompressedInputStream(decryptedInputStream);
1719
1720
1721    // Get an input stream reader that uses the specified character set, and
1722    // then wrap that with a buffered reader.
1723    final InputStreamReader inputStreamReader =
1724         new InputStreamReader(decompressedInputStream, charset);
1725    return new BufferedReader(inputStreamReader);
1726  }
1727
1728
1729
1730  /**
1731   * Deletes entries that match filters specified in the
1732   * deleteEntriesMatchingFilter argument.
1733   *
1734   * @param  returnCode  A reference that should be updated with the result code
1735   *                     from the first failure that is encountered.  It must
1736   *                     not be {@code null}, but may be unset.
1737   */
1738  private void deleteFromFilters(
1739                    @NotNull final AtomicReference<ResultCode> returnCode)
1740  {
1741    for (final Filter f : deleteEntriesMatchingFilter.getValues())
1742    {
1743      if ((! searchAndDelete(f.toString(), returnCode)) &&
1744           (! continueOnError.isPresent()))
1745      {
1746        return;
1747      }
1748    }
1749  }
1750
1751
1752
1753  /**
1754   * Deletes entries that match filters specified in the
1755   * deleteEntriesMatchingFilterFromFile argument.
1756   *
1757   * @param  returnCode            A reference that should be updated with the
1758   *                               result code from the first failure that is
1759   *                               encountered.  It must not be {@code null},
1760   *                               but may be unset.
1761   * @param  charset               The character set to use when reading the
1762   *                               data from the file.  It must not be
1763   *                               {@code null}.
1764   * @param  encryptionPassphrase  The passphrase to use to decrypt the data
1765   *                               read from the file if it happens to be
1766   *                               encrypted.  This may be {@code null} if the
1767   *                               user should be interactively prompted for the
1768   *                               passphrase if a file happens to be encrypted.
1769   */
1770  private void deleteFromFilterFile(
1771                    @NotNull final AtomicReference<ResultCode> returnCode,
1772                    @NotNull final Charset charset,
1773                    @Nullable final char[] encryptionPassphrase)
1774  {
1775    final List<char[]> potentialPassphrases =
1776         new ArrayList<>(dnFile.getValues().size());
1777    if (encryptionPassphrase != null)
1778    {
1779      potentialPassphrases.add(encryptionPassphrase);
1780    }
1781
1782    for (final File f : deleteEntriesMatchingFiltersFromFile.getValues())
1783    {
1784      if (verbose.isPresent())
1785      {
1786        commentToOut(INFO_LDAPDELETE_READING_FILTERS_FROM_FILE.get(
1787             f.getAbsolutePath()));
1788        out();
1789      }
1790
1791      try (FileInputStream fis = new FileInputStream(f);
1792           BufferedReader reader =
1793                getBufferedReader(fis, charset, potentialPassphrases))
1794      {
1795        while (true)
1796        {
1797          final String line = reader.readLine();
1798          if (line == null)
1799          {
1800            break;
1801          }
1802
1803          if (line.isEmpty() || line.startsWith("#"))
1804          {
1805            continue;
1806          }
1807
1808          if ((! searchAndDelete(line, returnCode)) &&
1809               (! continueOnError.isPresent()))
1810          {
1811            return;
1812          }
1813        }
1814      }
1815      catch (final IOException | GeneralSecurityException e)
1816      {
1817        commentToErr(ERR_LDAPDELETE_ERROR_READING_FILTER_FILE.get(
1818             f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
1819        if (! continueOnError.isPresent())
1820        {
1821          return;
1822        }
1823      }
1824    }
1825  }
1826
1827
1828
1829
1830  /**
1831   * Issues a search with the provided filter and attempts to delete all
1832   * matching entries.
1833   *
1834   * @param  filterString  The string representation of the filter to use when
1835   *                       processing the search.  It must not be {@code null}.
1836   * @param  returnCode    A reference that should be updated with the result
1837   *                       code from the first failure that is encountered.  It
1838   *                       must not be {@code null}, but may be unset.
1839   *
1840   * @return  {@code true} if the search and all deletes were processed
1841   *          successfully, or {@code false} if any problems were encountered.
1842   */
1843  private boolean searchAndDelete(@NotNull final String filterString,
1844                       @NotNull final AtomicReference<ResultCode> returnCode)
1845  {
1846    boolean successful = true;
1847    final AtomicLong entriesDeleted = new AtomicLong(0L);
1848    for (final DN baseDN : searchBaseDN.getValues())
1849    {
1850      if (searchPageSize.isPresent())
1851      {
1852        successful &= doPagedSearchAndDelete(baseDN.toString(), filterString,
1853             returnCode, entriesDeleted);
1854      }
1855      else
1856      {
1857        successful &= doNonPagedSearchAndDelete(baseDN.toString(), filterString,
1858             returnCode, entriesDeleted);
1859      }
1860    }
1861
1862    if (successful && (entriesDeleted.get() == 0))
1863    {
1864      commentToErr(ERR_LDAPDELETE_SEARCH_RETURNED_NO_ENTRIES.get(filterString));
1865      returnCode.compareAndSet(null, ResultCode.NO_RESULTS_RETURNED);
1866      successful = false;
1867    }
1868
1869    return successful;
1870  }
1871
1872
1873
1874  /**
1875   * Issues the provided search using the simple paged results control and
1876   * attempts to delete all of the matching entries.
1877   *
1878   * @param  baseDN          The base DN for the search request.  It must not
1879   *                         be {@code null}.
1880   * @param  filterString    The string representation of the filter ot use for
1881   *                         the search request.  It must not be {@code null}.
1882   * @param  returnCode      A reference that should be updated with the result
1883   *                         code from the first failure that is encountered.
1884   *                         It must not be {@code null}, but may be unset.
1885   * @param  entriesDeleted  A counter that will be updated for each entry that
1886   *                         is successfully deleted.  It must not be
1887   *                         {@code null}.
1888   *
1889   * @return  {@code true} if all entries matching the search criteria were
1890   *          successfully deleted (even if there were no matching entries), or
1891   *          {@code false} if an error occurred while attempting to process a
1892   *          search or delete operation.
1893   */
1894  private boolean doPagedSearchAndDelete(@NotNull final String baseDN,
1895                       @NotNull final String filterString,
1896                       @NotNull final AtomicReference<ResultCode> returnCode,
1897                       @NotNull final AtomicLong entriesDeleted)
1898  {
1899    ASN1OctetString cookie = null;
1900    final TreeSet<DN> matchingEntryDNs = new TreeSet<>();
1901    final LDAPDeleteSearchListener searchListener =
1902         new LDAPDeleteSearchListener(this, matchingEntryDNs, baseDN,
1903              filterString, returnCode);
1904    while (true)
1905    {
1906      try
1907      {
1908        final ArrayList<Control> requestControls = new ArrayList<>(10);
1909        requestControls.addAll(searchControls);
1910        requestControls.add(new SimplePagedResultsControl(
1911             searchPageSize.getValue(), cookie, true));
1912
1913        final SearchRequest searchRequest = new SearchRequest(searchListener,
1914             baseDN, SearchScope.SUB, DereferencePolicy.NEVER, 0, 0, false,
1915             filterString, SearchRequest.NO_ATTRIBUTES);
1916        searchRequest.setControls(requestControls);
1917
1918        if (verbose.isPresent())
1919        {
1920          commentToOut(INFO_LDAPDELETE_ISSUING_SEARCH_REQUEST.get(
1921               String.valueOf(searchRequest)));
1922        }
1923
1924        final SearchResult searchResult = connectionPool.search(searchRequest);
1925
1926        if (verbose.isPresent())
1927        {
1928          commentToOut(INFO_LDAPDELETE_RECEIVED_SEARCH_RESULT.get(
1929               String.valueOf(searchResult)));
1930        }
1931
1932        final SimplePagedResultsControl responseControl =
1933             SimplePagedResultsControl.get(searchResult);
1934        if (responseControl == null)
1935        {
1936          throw new LDAPException(ResultCode.CONTROL_NOT_FOUND,
1937               ERR_LDAPDELETE_MISSING_PAGED_RESULTS_RESPONSE.get(searchResult));
1938        }
1939        else if (responseControl.moreResultsToReturn())
1940        {
1941          cookie = responseControl.getCookie();
1942        }
1943        else
1944        {
1945          break;
1946        }
1947      }
1948      catch (final LDAPException e)
1949      {
1950        Debug.debugException(e);
1951        returnCode.compareAndSet(null, e.getResultCode());
1952        commentToErr(ERR_LDAPDELETE_SEARCH_ERROR.get(baseDN, filterString,
1953             String.valueOf(e.getResultCode()), e.getMessage()));
1954      }
1955    }
1956
1957    boolean allSuccessful = true;
1958    final Iterator<DN> iterator = matchingEntryDNs.descendingIterator();
1959    while (iterator.hasNext())
1960    {
1961      if (deleteEntry(iterator.next().toString(), returnCode))
1962      {
1963        entriesDeleted.incrementAndGet();
1964      }
1965      else
1966      {
1967        allSuccessful = false;
1968        if (! continueOnError.isPresent())
1969        {
1970          break;
1971        }
1972      }
1973    }
1974
1975    return allSuccessful;
1976  }
1977
1978
1979
1980  /**
1981   * Issues the provided search (without using the simple paged results control)
1982   * and attempts to delete all of the matching entries.
1983   *
1984   * @param  baseDN          The base DN for the search request.  It must not
1985   *                         be {@code null}.
1986   * @param  filterString    The string representation of the filter ot use for
1987   *                         the search request.  It must not be {@code null}.
1988   * @param  returnCode      A reference that should be updated with the result
1989   *                         code from the first failure that is encountered.
1990   *                         It must not be {@code null}, but may be unset.
1991   * @param  entriesDeleted  A counter that will be updated for each entry that
1992   *                         is successfully deleted.  It must not be
1993   *                         {@code null}.
1994   *
1995   * @return  {@code true} if all entries matching the search criteria were
1996   *          successfully deleted (even if there were no matching entries), or
1997   *          {@code false} if an error occurred while attempting to process a
1998   *          search or delete operation.
1999   */
2000  private boolean doNonPagedSearchAndDelete(@NotNull final String baseDN,
2001                       @NotNull final String filterString,
2002                       @NotNull final AtomicReference<ResultCode> returnCode,
2003                       @NotNull final AtomicLong entriesDeleted)
2004  {
2005    final TreeSet<DN> matchingEntryDNs = new TreeSet<>();
2006    final LDAPDeleteSearchListener searchListener =
2007         new LDAPDeleteSearchListener(this, matchingEntryDNs, baseDN,
2008              filterString, returnCode);
2009    try
2010    {
2011      final SearchRequest searchRequest = new SearchRequest(searchListener,
2012           baseDN, SearchScope.SUB, DereferencePolicy.NEVER, 0, 0, false,
2013           filterString, SearchRequest.NO_ATTRIBUTES);
2014      searchRequest.setControls(searchControls);
2015
2016      if (verbose.isPresent())
2017      {
2018        commentToOut(INFO_LDAPDELETE_ISSUING_SEARCH_REQUEST.get(
2019             String.valueOf(searchRequest)));
2020      }
2021
2022      final SearchResult searchResult = connectionPool.search(searchRequest);
2023
2024      if (verbose.isPresent())
2025      {
2026        commentToOut(INFO_LDAPDELETE_RECEIVED_SEARCH_RESULT.get(
2027             String.valueOf(searchResult)));
2028      }
2029    }
2030    catch (final LDAPException e)
2031    {
2032      Debug.debugException(e);
2033      returnCode.compareAndSet(null, e.getResultCode());
2034      commentToErr(ERR_LDAPDELETE_SEARCH_ERROR.get(baseDN, filterString,
2035           String.valueOf(e.getResultCode()), e.getMessage()));
2036    }
2037
2038
2039    boolean allSuccessful = true;
2040    final Iterator<DN> iterator = matchingEntryDNs.descendingIterator();
2041    while (iterator.hasNext())
2042    {
2043      if (deleteEntry(iterator.next().toString(), returnCode))
2044      {
2045        entriesDeleted.incrementAndGet();
2046      }
2047      else
2048      {
2049        allSuccessful = false;
2050        if (! continueOnError.isPresent())
2051        {
2052          break;
2053        }
2054      }
2055    }
2056
2057    return allSuccessful;
2058  }
2059
2060
2061
2062  /**
2063   * Deletes entries whose DNs are specified as trailing arguments.
2064   *
2065   * @param  returnCode  A reference that should be updated with the result code
2066   *                     from the first failure that is encountered.  It must
2067   *                     not be {@code null}, but may be unset.
2068   */
2069  private void deleteFromTrailingArguments(
2070                    @NotNull final AtomicReference<ResultCode> returnCode)
2071  {
2072    for (final String dn : parser.getTrailingArguments())
2073    {
2074      if ((! deleteEntry(dn, returnCode)) && (! continueOnError.isPresent()))
2075      {
2076        return;
2077      }
2078    }
2079  }
2080
2081
2082
2083  /**
2084   * Deletes entries whose DNs are read from standard input.
2085   *
2086   * @param  returnCode            A reference that should be updated with the
2087   *                               result code from the first failure that is
2088   *                               encountered.  It must not be {@code null},
2089   *                               but may be unset.
2090   * @param  charset               The character set to use when reading the
2091   *                               data from standard input.  It must not be
2092   *                               {@code null}.
2093   * @param  encryptionPassphrase  The passphrase to use to decrypt the data
2094   *                               read from standard input if it happens to be
2095   *                               encrypted.  This may be {@code null} if the
2096   *                               user should be interactively prompted for the
2097   *                               passphrase if the data happens to be
2098   *                               encrypted.
2099   */
2100  private void deleteFromStandardInput(
2101                    @NotNull final AtomicReference<ResultCode> returnCode,
2102                    @NotNull final Charset charset,
2103                    @Nullable final char[] encryptionPassphrase)
2104  {
2105    final List<char[]> potentialPassphrases = new ArrayList<>(1);
2106    if (encryptionPassphrase != null)
2107    {
2108      potentialPassphrases.add(encryptionPassphrase);
2109    }
2110
2111    commentToOut(INFO_LDAPDELETE_READING_FROM_STDIN.get());
2112    out();
2113
2114    try
2115    {
2116      deleteDNsFromInputStream(returnCode, in, charset, potentialPassphrases);
2117    }
2118    catch (final Exception e)
2119    {
2120      Debug.debugException(e);
2121      returnCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
2122      commentToErr(ERR_LDAPDELETE_ERROR_READING_STDIN.get(
2123           StaticUtils.getExceptionMessage(e)));
2124    }
2125  }
2126
2127
2128
2129  /**
2130   * Attempts to delete the specified entry.
2131   *
2132   * @param  dn          The DN of the entry to delete.  It must not be
2133   *                     {@code null}.
2134   * @param  returnCode  A reference to the result code to be returned.  It must
2135   *                     not be {@code null}, but may be unset.  If it is unset
2136   *                     and the delete attempt fails, then this should be set
2137   *                     to the result code for the failed delete operation.
2138   *
2139   * @return  {@code true} if the entry was successfully deleted, or
2140   *          {@code false} if not.
2141   */
2142  private boolean deleteEntry(@NotNull final String dn,
2143               @NotNull final AtomicReference<ResultCode> returnCode)
2144  {
2145    // Display a message indicating that we're going to delete the entry.
2146    if (subtreeDeleter == null)
2147    {
2148      commentToOut(INFO_LDAPDELETE_DELETING_ENTRY.get(dn));
2149    }
2150    else
2151    {
2152      commentToOut(INFO_LDAPDELETE_CLIENT_SIDE_SUBTREE_DELETING.get(dn));
2153    }
2154
2155
2156    // If the --dryRun argument was provided, then don't actually delete the
2157    // entry.  Just pretend that it succeeded.
2158    if (dryRun.isPresent())
2159    {
2160      commentToOut(INFO_LDAPDELETE_NOT_DELETING_BECAUSE_OF_DRY_RUN.get(dn));
2161      return true;
2162    }
2163
2164    if (subtreeDeleter == null)
2165    {
2166      // If we need to rate limit the delete operations, then do that now.
2167      if (deleteRateLimiter != null)
2168      {
2169        deleteRateLimiter.await();
2170      }
2171
2172
2173      // Create and process the delete request.
2174      final DeleteRequest deleteRequest = new DeleteRequest(dn);
2175      deleteRequest.setControls(deleteControls);
2176
2177      boolean successlful;
2178      LDAPResult deleteResult;
2179      try
2180      {
2181        if (verbose.isPresent())
2182        {
2183          commentToOut(INFO_LDAPDELETE_SENDING_DELETE_REQUEST.get(
2184               String.valueOf(deleteRequest)));
2185        }
2186
2187        deleteResult = connectionPool.delete(deleteRequest);
2188        successlful = true;
2189      }
2190      catch (final LDAPException e)
2191      {
2192        Debug.debugException(e);
2193        deleteResult = e.toLDAPResult();
2194        successlful = false;
2195      }
2196
2197
2198      // Display information about the result.
2199      for (final String resultLine :
2200           ResultUtils.formatResult(deleteResult, true, 0, WRAP_COLUMN))
2201      {
2202        if (successlful)
2203        {
2204          out(resultLine);
2205        }
2206        else
2207        {
2208          err(resultLine);
2209        }
2210      }
2211
2212
2213      // If the delete attempt failed, then update the return code and/or
2214      // write to the reject writer, if appropriate.
2215      final ResultCode deleteResultCode = deleteResult.getResultCode();
2216      if ((deleteResultCode != ResultCode.SUCCESS) &&
2217         (deleteResultCode != ResultCode.NO_OPERATION))
2218      {
2219        returnCode.compareAndSet(null, deleteResultCode);
2220        writeToRejects(deleteRequest, deleteResult);
2221        err();
2222        return false;
2223      }
2224      else
2225      {
2226        out();
2227        return true;
2228      }
2229    }
2230    else
2231    {
2232      // Use the subtree deleter to attempt a client-side subtree delete.
2233      final SubtreeDeleterResult subtreeDeleterResult;
2234      try
2235      {
2236        subtreeDeleterResult = subtreeDeleter.delete(connectionPool, dn);
2237      }
2238      catch (final LDAPException e)
2239      {
2240        Debug.debugException(e);
2241        commentToErr(e.getMessage());
2242        writeToRejects(new DeleteRequest(dn), e.toLDAPResult());
2243        returnCode.compareAndSet(null, e.getResultCode());
2244        return false;
2245      }
2246
2247      if (subtreeDeleterResult.completelySuccessful())
2248      {
2249        final long entriesDeleted = subtreeDeleterResult.getEntriesDeleted();
2250        if (entriesDeleted == 0L)
2251        {
2252          final DeleteRequest deleteRequest = new DeleteRequest(dn);
2253          final LDAPResult result = new LDAPResult(-1,
2254               ResultCode.NO_SUCH_OBJECT,
2255               ERR_LDAPDELETE_CLIENT_SIDE_SUBTREE_DEL_NO_BASE_ENTRY.get(dn),
2256               null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
2257          for (final String line :
2258               ResultUtils.formatResult(result, true, 0, WRAP_COLUMN))
2259          {
2260            err(line);
2261          }
2262          writeToRejects(deleteRequest, result);
2263          returnCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
2264          err();
2265          return false;
2266        }
2267        else if (entriesDeleted == 1L)
2268        {
2269          commentToOut(
2270               INFO_LDAPDELETE_CLIENT_SIDE_SUBTREE_DEL_ONLY_BASE.get(dn));
2271          out();
2272          return true;
2273        }
2274        else
2275        {
2276          final long numSubordinates = entriesDeleted - 1L;
2277          commentToOut(INFO_LDAPDELETE_CLIENT_SIDE_SUBTREE_DEL_WITH_SUBS.get(dn,
2278               numSubordinates));
2279          out();
2280          return true;
2281        }
2282      }
2283      else
2284      {
2285        commentToErr(ERR_LDAPDELETE_CLIENT_SIDE_SUBTREE_DEL_FAILED.get());
2286        err();
2287
2288        final SearchResult searchError = subtreeDeleterResult.getSearchError();
2289        if (searchError != null)
2290        {
2291          returnCode.compareAndSet(null, searchError.getResultCode());
2292          commentToErr(
2293               ERR_LDAPDELETE_CLIENT_SIDE_SUBTREE_DEL_SEARCH_ERROR.get(dn));
2294          for (final String line :
2295            ResultUtils.formatResult(searchError, true, 0, WRAP_COLUMN))
2296          {
2297            err(line);
2298          }
2299          err();
2300        }
2301
2302        for (final Map.Entry<DN,LDAPResult> deleteError :
2303             subtreeDeleterResult.getDeleteErrorsDescendingMap().entrySet())
2304        {
2305          final String failureDN = deleteError.getKey().toString();
2306          final LDAPResult failureResult = deleteError.getValue();
2307          returnCode.compareAndSet(null, failureResult.getResultCode());
2308          commentToErr(ERR_LDAPDELETE_CLIENT_SIDE_SUBTREE_DEL_DEL_ERROR.get(
2309               failureDN, dn));
2310          writeToRejects(new DeleteRequest(failureDN), failureResult);
2311          for (final String line :
2312            ResultUtils.formatResult(failureResult, true, 0, WRAP_COLUMN))
2313          {
2314            err(line);
2315          }
2316          err();
2317        }
2318
2319        return false;
2320      }
2321    }
2322  }
2323
2324
2325
2326  /**
2327   * Writes information about a failed operation to the reject writer.  If an
2328   * error occurs while writing the rejected change, then that error will be
2329   * written to standard error.
2330   *
2331   * @param  deleteRequest  The delete request that failed.
2332   * @param  deleteResult   The result for the failed delete.
2333   */
2334  private void writeToRejects(@NotNull final DeleteRequest deleteRequest,
2335                              @NotNull final LDAPResult deleteResult)
2336  {
2337    if (! rejectFile.isPresent())
2338    {
2339      return;
2340    }
2341
2342    LDIFWriter w;
2343    try
2344    {
2345      w = rejectWriter.get();
2346      if (w == null)
2347      {
2348        w = new LDIFWriter(rejectFile.getValue());
2349        rejectWriter.set(w);
2350      }
2351    }
2352    catch (final Exception e)
2353    {
2354      Debug.debugException(e);
2355      commentToErr(ERR_LDAPDELETE_WRITE_TO_REJECTS_FAILED.get(
2356           StaticUtils.getExceptionMessage(e)));
2357      return;
2358    }
2359
2360    try
2361    {
2362      boolean firstLine = true;
2363      for (final String commentLine :
2364           ResultUtils.formatResult(deleteResult, false, 0, (WRAP_COLUMN - 2)))
2365      {
2366        w.writeComment(commentLine, firstLine, false);
2367        firstLine = false;
2368      }
2369      w.writeChangeRecord(deleteRequest.toLDIFChangeRecord());
2370      w.flush();
2371    }
2372    catch (final Exception e)
2373    {
2374      Debug.debugException(e);
2375      commentToErr(ERR_LDAPDELETE_WRITE_TO_REJECTS_FAILED.get(
2376           StaticUtils.getExceptionMessage(e)));
2377    }
2378  }
2379
2380
2381
2382  /**
2383   * Retrieves the set of controls that should be included in delete requests.
2384   *
2385   * @return  The set of controls that should be included in delete requests.
2386   */
2387  @NotNull()
2388  private List<Control> getDeleteControls()
2389  {
2390    final List<Control> controlList = new ArrayList<>(10);
2391
2392    if (deleteControl.isPresent())
2393    {
2394      controlList.addAll(deleteControl.getValues());
2395    }
2396
2397    controlList.addAll(routeToBackendSetRequestControls);
2398
2399    if (serverSideSubtreeDelete.isPresent())
2400    {
2401      controlList.add(new SubtreeDeleteRequestControl(true));
2402    }
2403
2404    if (softDelete.isPresent())
2405    {
2406      controlList.add(new SoftDeleteRequestControl(true, true));
2407    }
2408
2409    if (hardDelete.isPresent() && (! clientSideSubtreeDelete.isPresent()))
2410    {
2411      controlList.add(new HardDeleteRequestControl(true));
2412    }
2413
2414    if (proxyAs.isPresent())
2415    {
2416      controlList.add(
2417           new ProxiedAuthorizationV2RequestControl(proxyAs.getValue()));
2418    }
2419
2420    if (proxyV1As.isPresent())
2421    {
2422      controlList.add(new ProxiedAuthorizationV1RequestControl(
2423           proxyV1As.getValue().toString()));
2424    }
2425
2426    if (manageDsaIT.isPresent() && (! clientSideSubtreeDelete.isPresent()))
2427    {
2428      controlList.add(new ManageDsaITRequestControl(true));
2429    }
2430
2431    if (assertionFilter.isPresent())
2432    {
2433      controlList.add(
2434           new AssertionRequestControl(assertionFilter.getValue(), true));
2435    }
2436
2437    if (preReadAttribute.isPresent())
2438    {
2439      controlList.add(new PreReadRequestControl(true,
2440           preReadAttribute.getValues().toArray(StaticUtils.NO_STRINGS)));
2441    }
2442
2443    if (noOperation.isPresent())
2444    {
2445      controlList.add(new NoOpRequestControl());
2446    }
2447
2448    if (getBackendSetID.isPresent())
2449    {
2450      controlList.add(new GetBackendSetIDRequestControl(true));
2451    }
2452
2453    if (getServerID.isPresent())
2454    {
2455      controlList.add(new GetServerIDRequestControl(true));
2456    }
2457
2458    if (routeToServer.isPresent())
2459    {
2460      controlList.add(new RouteToServerRequestControl(true,
2461           routeToServer.getValue(), false, false, false));
2462    }
2463
2464    if (useAssuredReplication.isPresent())
2465    {
2466      AssuredReplicationLocalLevel localLevel = null;
2467      if (assuredReplicationLocalLevel.isPresent())
2468      {
2469        final String level = assuredReplicationLocalLevel.getValue();
2470        if (level.equalsIgnoreCase("none"))
2471        {
2472          localLevel = AssuredReplicationLocalLevel.NONE;
2473        }
2474        else if (level.equalsIgnoreCase("received-any-server"))
2475        {
2476          localLevel = AssuredReplicationLocalLevel.RECEIVED_ANY_SERVER;
2477        }
2478        else if (level.equalsIgnoreCase("processed-all-servers"))
2479        {
2480          localLevel = AssuredReplicationLocalLevel.PROCESSED_ALL_SERVERS;
2481        }
2482      }
2483
2484      AssuredReplicationRemoteLevel remoteLevel = null;
2485      if (assuredReplicationRemoteLevel.isPresent())
2486      {
2487        final String level = assuredReplicationRemoteLevel.getValue();
2488        if (level.equalsIgnoreCase("none"))
2489        {
2490          remoteLevel = AssuredReplicationRemoteLevel.NONE;
2491        }
2492        else if (level.equalsIgnoreCase("received-any-remote-location"))
2493        {
2494          remoteLevel =
2495               AssuredReplicationRemoteLevel.RECEIVED_ANY_REMOTE_LOCATION;
2496        }
2497        else if (level.equalsIgnoreCase("received-all-remote-locations"))
2498        {
2499          remoteLevel =
2500               AssuredReplicationRemoteLevel.RECEIVED_ALL_REMOTE_LOCATIONS;
2501        }
2502        else if (level.equalsIgnoreCase("processed-all-remote-servers"))
2503        {
2504          remoteLevel =
2505               AssuredReplicationRemoteLevel.PROCESSED_ALL_REMOTE_SERVERS;
2506        }
2507      }
2508
2509      Long timeoutMillis = null;
2510      if (assuredReplicationTimeout.isPresent())
2511      {
2512        timeoutMillis =
2513             assuredReplicationTimeout.getValue(TimeUnit.MILLISECONDS);
2514      }
2515
2516      final AssuredReplicationRequestControl c =
2517           new AssuredReplicationRequestControl(true, localLevel, localLevel,
2518                remoteLevel, remoteLevel, timeoutMillis, false);
2519      controlList.add(c);
2520    }
2521
2522    if (replicationRepair.isPresent())
2523    {
2524      controlList.add(new ReplicationRepairRequestControl());
2525    }
2526
2527    if (suppressReferentialIntegrityUpdates.isPresent())
2528    {
2529      controlList.add(
2530           new SuppressReferentialIntegrityUpdatesRequestControl(true));
2531    }
2532
2533    if (operationPurpose.isPresent())
2534    {
2535      controlList.add(new OperationPurposeRequestControl(true,
2536           "ldapdelete", Version.NUMERIC_VERSION_STRING,
2537           LDAPDelete.class.getName() + ".getDeleteControls",
2538           operationPurpose.getValue()));
2539    }
2540
2541    return Collections.unmodifiableList(controlList);
2542  }
2543
2544
2545
2546  /**
2547   * Retrieves the set of controls that should be included in search requests.
2548   *
2549   * @return  The set of controls that should be included in delete requests.
2550   */
2551  @NotNull()
2552  private List<Control> getSearchControls()
2553  {
2554    final List<Control> controlList = new ArrayList<>(10);
2555
2556    controlList.addAll(routeToBackendSetRequestControls);
2557
2558    if (manageDsaIT.isPresent())
2559    {
2560      controlList.add(new ManageDsaITRequestControl(true));
2561    }
2562
2563    if (proxyV1As.isPresent())
2564    {
2565      controlList.add(new ProxiedAuthorizationV1RequestControl(
2566           proxyV1As.getValue().toString()));
2567    }
2568
2569    if (proxyAs.isPresent())
2570    {
2571      controlList.add(
2572           new ProxiedAuthorizationV2RequestControl(proxyAs.getValue()));
2573    }
2574
2575    if (operationPurpose.isPresent())
2576    {
2577      controlList.add(new OperationPurposeRequestControl(true,
2578           "ldapdelete", Version.NUMERIC_VERSION_STRING,
2579           LDAPDelete.class.getName() + ".getSearchControls",
2580           operationPurpose.getValue()));
2581    }
2582
2583    if (routeToServer.isPresent())
2584    {
2585      controlList.add(new RouteToServerRequestControl(true,
2586           routeToServer.getValue(), false, false, false));
2587    }
2588
2589    return Collections.unmodifiableList(controlList);
2590  }
2591
2592
2593
2594  /**
2595   * {@inheritDoc}
2596   */
2597  @Override()
2598  public void handleUnsolicitedNotification(
2599                   @NotNull final LDAPConnection connection,
2600                   @NotNull final ExtendedResult notification)
2601  {
2602    final ArrayList<String> lines = new ArrayList<>(10);
2603    ResultUtils.formatUnsolicitedNotification(lines, notification, true, 0,
2604         WRAP_COLUMN);
2605    for (final String line : lines)
2606    {
2607      err(line);
2608    }
2609    err();
2610  }
2611
2612
2613
2614  /**
2615   * Writes a line-wrapped, commented version of the provided message to
2616   * standard output.
2617   *
2618   * @param  message  The message to be written.
2619   */
2620  void commentToOut(@NotNull final String message)
2621  {
2622    for (final String line : StaticUtils.wrapLine(message, WRAP_COLUMN - 2))
2623    {
2624      out("# ", line);
2625    }
2626  }
2627
2628
2629
2630  /**
2631   * Writes a line-wrapped, commented version of the provided message to
2632   * standard error.
2633   *
2634   * @param  message  The message to be written.
2635   */
2636  void commentToErr(@NotNull final String message)
2637  {
2638    for (final String line : StaticUtils.wrapLine(message, WRAP_COLUMN - 2))
2639    {
2640      err("# ", line);
2641    }
2642  }
2643
2644
2645
2646  /**
2647   * {@inheritDoc}
2648   */
2649  @Override()
2650  @NotNull()
2651  public LinkedHashMap<String[],String> getExampleUsages()
2652  {
2653    final LinkedHashMap<String[],String> examples =
2654         new LinkedHashMap<>(StaticUtils.computeMapCapacity(4));
2655
2656    examples.put(
2657         new String[]
2658         {
2659           "--hostname", "ds.example.com",
2660           "--port", "636",
2661           "--useSSL",
2662           "--bindDN", "uid=admin,dc=example,dc=com",
2663           "uid=test.user,ou=People,dc=example,dc=com"
2664         },
2665         INFO_LDAPDELETE_EXAMPLE_1.get());
2666
2667    examples.put(
2668         new String[]
2669         {
2670           "--hostname", "ds.example.com",
2671           "--port", "636",
2672           "--useSSL",
2673           "--trustStorePath", "trust-store.jks",
2674           "--bindDN", "uid=admin,dc=example,dc=com",
2675           "--bindPasswordFile", "admin-password.txt",
2676           "--dnFile", "dns-to-delete.txt"
2677         },
2678         INFO_LDAPDELETE_EXAMPLE_2.get());
2679
2680    examples.put(
2681         new String[]
2682         {
2683           "--hostname", "ds.example.com",
2684           "--port", "389",
2685           "--useStartTLS",
2686           "--trustStorePath", "trust-store.jks",
2687           "--bindDN", "uid=admin,dc=example,dc=com",
2688           "--bindPasswordFile", "admin-password.txt",
2689           "--deleteEntriesMatchingFilter", "(description=delete)"
2690         },
2691         INFO_LDAPDELETE_EXAMPLE_3.get());
2692
2693    examples.put(
2694         new String[]
2695         {
2696           "--hostname", "ds.example.com",
2697           "--port", "389",
2698           "--bindDN", "uid=admin,dc=example,dc=com"
2699         },
2700         INFO_LDAPDELETE_EXAMPLE_4.get());
2701
2702    return examples;
2703  }
2704}