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