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