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