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