001/*
002 * Copyright 2008-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2008-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.tools;
037
038
039
040import java.io.File;
041import java.io.FileInputStream;
042import java.io.FileOutputStream;
043import java.io.InputStream;
044import java.io.OutputStream;
045import java.io.PrintWriter;
046import java.text.DecimalFormat;
047import java.text.SimpleDateFormat;
048import java.util.ArrayList;
049import java.util.Arrays;
050import java.util.Collections;
051import java.util.Date;
052import java.util.EnumSet;
053import java.util.LinkedHashMap;
054import java.util.LinkedList;
055import java.util.List;
056import java.util.Map;
057import java.util.TreeMap;
058import java.util.concurrent.TimeUnit;
059import java.util.concurrent.atomic.AtomicBoolean;
060import java.util.concurrent.atomic.AtomicLong;
061import java.util.concurrent.atomic.AtomicReference;
062import java.util.zip.GZIPOutputStream;
063
064import com.unboundid.ldap.sdk.Control;
065import com.unboundid.ldap.sdk.DN;
066import com.unboundid.ldap.sdk.ExtendedResult;
067import com.unboundid.ldap.sdk.LDAPConnection;
068import com.unboundid.ldap.sdk.LDAPConnectionOptions;
069import com.unboundid.ldap.sdk.LDAPConnectionPool;
070import com.unboundid.ldap.sdk.LDAPException;
071import com.unboundid.ldap.sdk.ResultCode;
072import com.unboundid.ldap.sdk.UnsolicitedNotificationHandler;
073import com.unboundid.ldap.sdk.Version;
074import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
075import com.unboundid.ldap.sdk.controls.PermissiveModifyRequestControl;
076import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV1RequestControl;
077import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
078import com.unboundid.ldap.sdk.unboundidds.controls.
079            AssuredReplicationRequestControl;
080import com.unboundid.ldap.sdk.unboundidds.controls.AssuredReplicationLocalLevel;
081import com.unboundid.ldap.sdk.unboundidds.controls.
082            AssuredReplicationRemoteLevel;
083import com.unboundid.ldap.sdk.unboundidds.controls.HardDeleteRequestControl;
084import com.unboundid.ldap.sdk.unboundidds.controls.
085            IgnoreNoUserModificationRequestControl;
086import com.unboundid.ldap.sdk.unboundidds.controls.
087            NameWithEntryUUIDRequestControl;
088import com.unboundid.ldap.sdk.unboundidds.controls.
089            OperationPurposeRequestControl;
090import com.unboundid.ldap.sdk.unboundidds.controls.
091            PasswordUpdateBehaviorRequestControl;
092import com.unboundid.ldap.sdk.unboundidds.controls.
093            PasswordUpdateBehaviorRequestControlProperties;
094import com.unboundid.ldap.sdk.unboundidds.controls.
095            ReplicationRepairRequestControl;
096import com.unboundid.ldap.sdk.unboundidds.controls.SoftDeleteRequestControl;
097import com.unboundid.ldap.sdk.unboundidds.controls.
098            SuppressOperationalAttributeUpdateRequestControl;
099import com.unboundid.ldap.sdk.unboundidds.controls.
100            SuppressReferentialIntegrityUpdatesRequestControl;
101import com.unboundid.ldap.sdk.unboundidds.controls.SuppressType;
102import com.unboundid.ldif.LDIFChangeRecord;
103import com.unboundid.ldif.LDIFException;
104import com.unboundid.ldif.LDIFReader;
105import com.unboundid.ldif.LDIFWriter;
106import com.unboundid.util.Debug;
107import com.unboundid.util.FixedRateBarrier;
108import com.unboundid.util.LDAPCommandLineTool;
109import com.unboundid.util.NotNull;
110import com.unboundid.util.Nullable;
111import com.unboundid.util.ObjectPair;
112import com.unboundid.util.PassphraseEncryptedOutputStream;
113import com.unboundid.util.PassphraseEncryptedStreamHeader;
114import com.unboundid.util.StaticUtils;
115import com.unboundid.util.ThreadSafety;
116import com.unboundid.util.ThreadSafetyLevel;
117import com.unboundid.util.args.ArgumentException;
118import com.unboundid.util.args.ArgumentParser;
119import com.unboundid.util.args.BooleanArgument;
120import com.unboundid.util.args.ControlArgument;
121import com.unboundid.util.args.DNArgument;
122import com.unboundid.util.args.DurationArgument;
123import com.unboundid.util.args.FileArgument;
124import com.unboundid.util.args.IntegerArgument;
125import com.unboundid.util.args.StringArgument;
126
127import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
128
129
130
131/**
132 * This class provides a command-line tool that can be used to read change
133 * records for add, delete, modify and modify DN operations from an LDIF file,
134 * and then apply them in parallel using multiple threads for higher throughput.
135 * <BR>
136 * <BLOCKQUOTE>
137 *   <B>NOTE:</B>  This class, and other classes within the
138 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
139 *   supported for use against Ping Identity, UnboundID, and
140 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
141 *   for proprietary functionality or for external specifications that are not
142 *   considered stable or mature enough to be guaranteed to work in an
143 *   interoperable way with other types of LDAP servers.
144 * </BLOCKQUOTE>
145 * <BR><BR>
146 * Changes in the LDIF file to be processed should be ordered such that if there
147 * are any dependencies between changes, prerequisite changes come before the
148 * changes that depend on them (for example, if one add change record creates a
149 * parent entry and another creates a child entry, the add change record that
150 * creates the parent entry must come before the one that creates the child
151 * entry).  When this tool is preparing to process a change, it will determine
152 * whether the new change depends on any other changes that are currently in
153 * progress, and if so, will delay processing that change until its dependencies
154 * have been satisfied.  If a change does not depend on any other changes that
155 * are currently being processed, then it can be processed in parallel with
156 * those changes.
157 * <BR><BR>
158 * The tool will keep track of any changes that fail in a way that indicates
159 * they succeed if re-tried later (for example, an attempt to add an entry that
160 * fails because its parent does not exist, but its parent may be created later
161 * in the set of LDIF changes), and can optionally re-try those changes after
162 * processing is complete.  Any changes that are not retried, as well as changes
163 * that still fail after the retry attempts, will be written to a rejects file
164 * with information about the reason for the failure so that an administrator
165 * can take any necessary further action upon them.
166 */
167@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
168public final class ParallelUpdate
169       extends LDAPCommandLineTool
170       implements UnsolicitedNotificationHandler
171{
172  /**
173   * The column at which long lines should be wrapped.
174   */
175  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
176
177
178
179  /**
180   * The name of the password update behavior key that may be used to specify
181   * whether an update should be treated as a self-change.
182   */
183  @NotNull private static final String PW_UPDATE_BEHAVIOR_NAME_IS_SELF_CHANGE =
184       "is-self-change";
185
186
187
188  /**
189   * The name of the password update behavior key that may be used to specify
190   * whether an update should allow the password to be provided in pre-encoded
191   * form.
192   */
193  @NotNull private static final String
194       PW_UPDATE_BEHAVIOR_NAME_ALLOW_PRE_ENCODED_PW =
195            "allow-pre-encoded-password";
196
197
198
199  /**
200   * The name of the password update behavior key that may be used to specify
201   * whether the server should skip validation for the password.
202   */
203  @NotNull private static final String
204       PW_UPDATE_BEHAVIOR_NAME_SKIP_PW_VALIDATION = "skip-password-validation";
205
206
207
208  /**
209   * The name of the password update behavior key that may be used to specify
210   * whether the server should ignore the password history when determining
211   * whether to accept the new password.
212   */
213  @NotNull private static final String
214       PW_UPDATE_BEHAVIOR_NAME_IGNORE_PW_HISTORY = "ignore-password-history";
215
216
217
218  /**
219   * The name of the password update behavior key that may be used to specify
220   * whether the server should ignore the minimum password age when determining
221   * whether to allow the password change.
222   */
223  @NotNull private static final String
224       PW_UPDATE_BEHAVIOR_NAME_IGNORE_MIN_PW_AGE =
225            "ignore-minimum-password-age";
226
227
228
229  /**
230   * The name of the password update behavior key that may be used to specify
231   * the password storage scheme that should be used to encode the new password.
232   */
233  @NotNull private static final String
234       PW_UPDATE_BEHAVIOR_NAME_PW_STORAGE_SCHEME = "password-storage-scheme";
235
236
237
238  /**
239   * The name of the password update behavior key that may be used to specify
240   * whether the user must change their password on the next successful
241   * authentication.
242   */
243  @NotNull private static final String PW_UPDATE_BEHAVIOR_NAME_MUST_CHANGE_PW =
244       "must-change-password";
245
246
247
248  /**
249   * The assured replication local level value that indicates that no assurance
250   * is needed.
251   */
252  @NotNull private static final String ASSURED_REPLICATION_LOCAL_LEVEL_NONE =
253       "none";
254
255
256
257  /**
258   * The assured replication local level value that indicates that the response
259   * should be delayed until the change has been received by at least one other
260   * local server.
261   */
262  @NotNull private static final String
263       ASSURED_REPLICATION_LOCAL_LEVEL_RECEIVED_ANY_SERVER =
264            "received-any-server";
265
266
267
268  /**
269   * The assured replication local level value that indicates that the response
270   * should be delayed until the change has been processed by all available
271   * local servers.
272   */
273  @NotNull private static final String
274       ASSURED_REPLICATION_LOCAL_LEVEL_PROCESSED_ALL_SERVERS =
275            "processed-all-servers";
276
277
278
279  /**
280   * The assured replication remote level value that indicates that no assurance
281   * is needed.
282   */
283  @NotNull private static final String ASSURED_REPLICATION_REMOTE_LEVEL_NONE =
284       "none";
285
286
287
288  /**
289   * The assured replication remote level value that indicates that the response
290   * should be delayed until the change has been received by at least one server
291   * in at least one remote location.
292   */
293  @NotNull private static final String
294       ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ANY_REMOTE_LOCATION =
295            "received-any-remote-location";
296
297
298
299  /**
300   * The assured replication remote level value that indicates that the response
301   * should be delayed until the change has been received by at least one server
302   * in all remote locations.
303   */
304  @NotNull private static final String
305       ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ALL_REMOTE_LOCATIONS =
306            "received-all-remote-locations";
307
308
309
310  /**
311   * The assured replication remote level value that indicates that the response
312   * should be delayed until the change has been processed by all available
313   * servers in all remote locations.
314   */
315  @NotNull private static final String
316       ASSURED_REPLICATION_REMOTE_LEVEL_PROCESSED_ALL_REMOTE_SERVERS =
317            "processed-all-remote-servers";
318
319
320
321  /**
322   * The suppress operational attribute update value that indicates that updates
323   * to the last access time should be suppressed.
324   */
325  @NotNull private static final String SUPPRESS_OP_ATTR_LAST_ACCESS_TIME =
326       "last-access-time";
327
328
329
330  /**
331   * The suppress operational attribute update value that indicates that updates
332   * to the last login time should be suppressed.
333   */
334  @NotNull private static final String SUPPRESS_OP_ATTR_LAST_LOGIN_TIME =
335       "last-login-time";
336
337
338
339  /**
340   * The suppress operational attribute update value that indicates that updates
341   * to the last login IP address should be suppressed.
342   */
343  @NotNull private static final String SUPPRESS_OP_ATTR_LAST_LOGIN_IP =
344       "last-login-ip";
345
346
347
348  /**
349   * The suppress operational attribute update value that indicates that updates
350   * to the lastmod attributes (creatorsName, createTimestamp, modifiersName,
351   * modifyTimestamp) should be suppressed.
352   */
353  @NotNull private static final String SUPPRESS_OP_ATTR_LASTMOD = "lastmod";
354
355
356
357  // Indicates whether an error has occurred and that processing should be
358  // aborted.
359  @NotNull private final AtomicBoolean shouldAbort;
360
361  // Counters used to keep track of statistical information about processing.
362  @NotNull private final AtomicLong opsAttempted;
363  @NotNull private final AtomicLong opsRejected;
364  @NotNull private final AtomicLong opsSucceeded;
365  @NotNull private final AtomicLong totalOpDurationMillis;
366  private volatile long initialAttempted;
367  private volatile long initialSucceeded;
368
369  // Variables pertaining to operations to be retried.
370  @NotNull private final AtomicLong retryQueueSize;
371  @NotNull private final
372       Map<DN,List<ObjectPair<LDIFChangeRecord,LDAPException>>> retryQueue;
373
374  // The result code for the first operation that was rejected, if any.
375  @NotNull private final AtomicReference<ResultCode> firstRejectResultCode;
376
377  // The completion message for this tool, if available.
378  @NotNull private final AtomicReference<String> completionMessage;
379
380  // The rate limiter for this tool, if any.
381  @Nullable private FixedRateBarrier rateLimiter;
382
383  // Writers used to write rejects and log messages.
384  @Nullable private LDIFWriter rejectWriter;
385  @Nullable private PrintWriter logWriter;
386
387  // Variables used to keep track of data about processing intervals for use in
388  // periodic status updates.
389  private volatile long lastOpsAttempted;
390  private volatile long lastTotalDurationMillis;
391  private volatile long lastUpdateTimeMillis;
392  private volatile long processingStartTimeMillis;
393
394  // Thread-local date formatters used to format message timestamps.
395  @NotNull private final ThreadLocal<SimpleDateFormat> timestampFormatters;
396
397  // The set of command-line arguments for this program.
398  @Nullable private BooleanArgument allowUndeleteArg;
399  @Nullable private BooleanArgument defaultAddArg;
400  @Nullable private BooleanArgument followReferralsArg;
401  @Nullable private BooleanArgument hardDeleteArg;
402  @Nullable private BooleanArgument ignoreNoUserModificationArg;
403  @Nullable private BooleanArgument isCompressedArg;
404  @Nullable private BooleanArgument nameWithEntryUUIDArg;
405  @Nullable private BooleanArgument neverRetryArg;
406  @Nullable private BooleanArgument replicationRepairArg;
407  @Nullable private BooleanArgument softDeleteArg;
408  @Nullable private BooleanArgument suppressReferentialIntegrityUpdatesArg;
409  @Nullable private BooleanArgument useAssuredReplicationArg;
410  @Nullable private BooleanArgument useFirstRejectResultCodeAsExitCodeArg;
411  @Nullable private BooleanArgument useManageDsaITArg;
412  @Nullable private BooleanArgument usePermissiveModifyArg;
413  @Nullable private ControlArgument addControlArg;
414  @Nullable private ControlArgument bindControlArg;
415  @Nullable private ControlArgument deleteControlArg;
416  @Nullable private ControlArgument modifyControlArg;
417  @Nullable private ControlArgument modifyDNControlArg;
418  @Nullable private DNArgument proxyV1AsArg;
419  @Nullable private DurationArgument assuredReplicationTimeoutArg;
420  @Nullable private FileArgument encryptionPassphraseFileArg;
421  @Nullable private FileArgument ldifFileArg;
422  @Nullable private FileArgument logFileArg;
423  @Nullable private FileArgument rejectFileArg;
424  @Nullable private IntegerArgument numThreadsArg;
425  @Nullable private IntegerArgument ratePerSecondArg;
426  @Nullable private StringArgument assuredReplicationLocalLevelArg;
427  @Nullable private StringArgument assuredReplicationRemoteLevelArg;
428  @Nullable private StringArgument operationPurposeArg;
429  @Nullable private StringArgument passwordUpdateBehaviorArg;
430  @Nullable private StringArgument proxyAsArg;
431  @Nullable private StringArgument suppressOperationalAttributeUpdatesArg;
432
433
434
435  /**
436   * Parses the provided set of command-line arguments and then performs the
437   * necessary processing.
438   *
439   * @param  args  The command-line arguments provided to this program.
440   */
441  public static void main(@NotNull final String... args)
442  {
443    final ResultCode resultCode = main(System.out, System.err, args);
444    if (resultCode != ResultCode.SUCCESS)
445    {
446      System.exit(Math.min(resultCode.intValue(), 255));
447    }
448  }
449
450
451
452  /**
453   * Parses the provided set of command-line arguments and then performs the
454   * necessary processing.
455   *
456   * @param  out   The output stream to which standard output should be written.
457   *               It may be {@code null} if standard output should be
458   *               suppressed.
459   * @param  err   The output stream to which standard error should be written.
460   *               It may be {@code null} if standard error should be
461   *               suppressed.
462   * @param  args  The command-line arguments provided to this program.
463   *
464   * @return  Zero if all processing completed successfully, or nonzero if an
465   *          error occurred.
466   */
467  @NotNull()
468  public static ResultCode main(@Nullable final OutputStream out,
469                                @Nullable final OutputStream err,
470                                @NotNull final String... args)
471  {
472    final ParallelUpdate parallelupdate = new ParallelUpdate(out, err);
473    return parallelupdate.runTool(args);
474  }
475
476
477
478  /**
479   * Creates a new instance of this tool with the provided output and error
480   * streams.
481   *
482   * @param  out  The output stream to which standard output should be written.
483   *              It may be {@code null} if standard output should be
484   *              suppressed.
485   * @param  err  The output stream to which standard error should be written.
486   *              It may be {@code null} if standard error should be
487   *              suppressed.
488   */
489  public ParallelUpdate(@Nullable final OutputStream out,
490                        @Nullable final OutputStream err)
491  {
492    super(out, err);
493
494    shouldAbort = new AtomicBoolean(false);
495    opsAttempted = new AtomicLong(0L);
496    opsRejected = new AtomicLong(0L);
497    opsSucceeded = new AtomicLong(0L);
498    totalOpDurationMillis = new AtomicLong(0L);
499    initialAttempted = 0L;
500    initialSucceeded = 0L;
501    retryQueueSize = new AtomicLong(0L);
502    retryQueue = new TreeMap<>();
503    firstRejectResultCode = new AtomicReference<>();
504    completionMessage = new AtomicReference<>();
505    rejectWriter = null;
506    logWriter = null;
507    lastOpsAttempted = 0L;
508    lastTotalDurationMillis = 0L;
509    lastUpdateTimeMillis = 0L;
510    processingStartTimeMillis = System.currentTimeMillis();
511    timestampFormatters = new ThreadLocal<>();
512    allowUndeleteArg = null;
513    defaultAddArg = null;
514    followReferralsArg = null;
515    hardDeleteArg = null;
516    ignoreNoUserModificationArg = null;
517    isCompressedArg = null;
518    nameWithEntryUUIDArg = null;
519    neverRetryArg = null;
520    replicationRepairArg = null;
521    softDeleteArg = null;
522    suppressReferentialIntegrityUpdatesArg = null;
523    useAssuredReplicationArg = null;
524    useFirstRejectResultCodeAsExitCodeArg = null;
525    useManageDsaITArg = null;
526    usePermissiveModifyArg = null;
527    addControlArg = null;
528    bindControlArg = null;
529    deleteControlArg = null;
530    modifyControlArg = null;
531    modifyDNControlArg = null;
532    proxyV1AsArg = null;
533    assuredReplicationTimeoutArg = null;
534    encryptionPassphraseFileArg = null;
535    ldifFileArg = null;
536    logFileArg = null;
537    rejectFileArg = null;
538    numThreadsArg = null;
539    ratePerSecondArg = null;
540    assuredReplicationLocalLevelArg = null;
541    assuredReplicationRemoteLevelArg = null;
542    operationPurposeArg = null;
543    passwordUpdateBehaviorArg = null;
544    proxyAsArg = null;
545    suppressOperationalAttributeUpdatesArg = null;
546  }
547
548
549
550  /**
551   * {@inheritDoc}
552   */
553  @Override()
554  @NotNull()
555  public String getToolName()
556  {
557    return "parallel-update";
558  }
559
560
561
562  /**
563   * {@inheritDoc}
564   */
565  @Override()
566  @NotNull()
567  public String getToolDescription()
568  {
569    return INFO_PARALLEL_UPDATE_TOOL_DESCRIPTION_1.get();
570  }
571
572
573
574  /**
575   * {@inheritDoc}
576   */
577  @Override()
578  @NotNull()
579  public List<String> getAdditionalDescriptionParagraphs()
580  {
581    return Collections.unmodifiableList(Arrays.asList(
582         INFO_PARALLEL_UPDATE_TOOL_DESCRIPTION_2.get(),
583         INFO_PARALLEL_UPDATE_TOOL_DESCRIPTION_3.get()));
584  }
585
586
587
588  /**
589   * {@inheritDoc}
590   */
591  @Override()
592  @NotNull()
593  public String getToolVersion()
594  {
595    return Version.NUMERIC_VERSION_STRING;
596  }
597
598
599
600  /**
601   * {@inheritDoc}
602   */
603  @Override()
604  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
605         throws ArgumentException
606  {
607    ldifFileArg = new FileArgument('l', "ldifFile", true, 1, null,
608         INFO_PARALLEL_UPDATE_ARG_DESC_LDIF_FILE.get(), true, true, true,
609         false);
610    ldifFileArg.addLongIdentifier("ldif-file", true);
611    ldifFileArg.addLongIdentifier("inputFile", true);
612    ldifFileArg.addLongIdentifier("input-file", true);
613    ldifFileArg.setArgumentGroupName(
614         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
615    parser.addArgument(ldifFileArg);
616
617    isCompressedArg = new BooleanArgument('c', "isCompressed", 1,
618         INFO_PARALLEL_UPDATE_ARG_DESC_IS_COMPRESSED.get());
619    isCompressedArg.addLongIdentifier("is-compressed", true);
620    isCompressedArg.addLongIdentifier("compressed", true);
621    isCompressedArg.setArgumentGroupName(
622         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
623    isCompressedArg.setHidden(true);
624    parser.addArgument(isCompressedArg);
625
626    encryptionPassphraseFileArg = new FileArgument(null,
627         "encryptionPassphraseFile", false, 1, null,
628         INFO_PARALLEL_UPDATE_ARG_DESC_ENCRYPTION_PASSPHRASE_FILE.get(),
629         true, true, true, false);
630    encryptionPassphraseFileArg.addLongIdentifier(
631         "encryption-passphrase-file", true);
632    encryptionPassphraseFileArg.addLongIdentifier(
633         "encryptionPasswordFile", true);
634    encryptionPassphraseFileArg.addLongIdentifier(
635         "encryption-password-file", true);
636    encryptionPassphraseFileArg.setArgumentGroupName(
637         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
638    parser.addArgument(encryptionPassphraseFileArg);
639
640    rejectFileArg = new FileArgument('R', "rejectFile", true, 1, null,
641         INFO_PARALLEL_UPDATE_ARG_DESC_REJECT_FILE.get(), false, true, true,
642         false);
643    rejectFileArg.addLongIdentifier("reject-file", true);
644    rejectFileArg.addLongIdentifier("rejectsFile", true);
645    rejectFileArg.addLongIdentifier("rejects-file", true);
646    rejectFileArg.setArgumentGroupName(
647         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
648    parser.addArgument(rejectFileArg);
649
650    useFirstRejectResultCodeAsExitCodeArg = new BooleanArgument(null,
651         "useFirstRejectResultCodeAsExitCode", 1,
652         INFO_PARALLEL_UPDATE_USE_FIRST_REJECT_RC.get());
653    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
654         "use-first-reject-result-code-as-exit-code", true);
655    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
656         "useFirstRejectResultCode", true);
657    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
658         "use-first-reject-result-code", true);
659    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
660         "useFirstRejectResult", true);
661    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
662         "use-first-reject-result", true);
663    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
664         "useRejectResultCodeAsExitCode", true);
665    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
666         "use-reject-result-code-as-exit-code", true);
667    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
668         "useRejectResultCode", true);
669    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
670         "use-reject-result-code", true);
671    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
672         "useRejectResult", true);
673    useFirstRejectResultCodeAsExitCodeArg.addLongIdentifier(
674         "use-reject-result", true);
675    useFirstRejectResultCodeAsExitCodeArg.setArgumentGroupName(
676         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
677    parser.addArgument(useFirstRejectResultCodeAsExitCodeArg);
678
679    neverRetryArg = new BooleanArgument('r', "neverRetry", 1,
680         INFO_PARALLEL_UPDATE_ARG_DESC_NEVER_RETRY.get());
681    neverRetryArg.addLongIdentifier("never-retry", true);
682    neverRetryArg.setArgumentGroupName(
683         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
684    parser.addArgument(neverRetryArg);
685
686    logFileArg = new FileArgument('L', "logFile", false, 1, null,
687         INFO_PARALLEL_UPDATE_ARG_DESC_LOG_FILE.get(), false, true, true,
688         false);
689    logFileArg.addLongIdentifier("log-file", true);
690    logFileArg.setArgumentGroupName(
691         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
692    parser.addArgument(logFileArg);
693
694    defaultAddArg = new BooleanArgument('a', "defaultAdd", 1,
695         INFO_PARALLEL_UPDATE_ARG_DESC_DEFAULT_ADD.get());
696    defaultAddArg.addLongIdentifier("default-add", true);
697    defaultAddArg.setArgumentGroupName(
698         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
699    parser.addArgument(defaultAddArg);
700
701    followReferralsArg = new BooleanArgument(null, "followReferrals", 1,
702         INFO_PARALLEL_UPDATE_ARG_DESC_FOLLOW_REFERRALS.get());
703    followReferralsArg.addLongIdentifier("follow-referrals", true);
704    followReferralsArg.setArgumentGroupName(
705         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
706    parser.addArgument(followReferralsArg);
707
708    numThreadsArg = new IntegerArgument('t', "numThreads", true, 1, null,
709         INFO_PARALLEL_UPDATE_ARG_DESC_NUM_THREADS.get(), 1, Integer.MAX_VALUE,
710         8);
711    numThreadsArg.addLongIdentifier("num-threads", true);
712    numThreadsArg.addLongIdentifier("threads", true);
713    numThreadsArg.setArgumentGroupName(
714         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
715    parser.addArgument(numThreadsArg);
716
717    ratePerSecondArg = new IntegerArgument('s', "ratePerSecond", false, 1,
718         null, INFO_PARALLEL_UPDATE_ARG_DESC_RATE_PER_SECOND.get(), 1,
719         Integer.MAX_VALUE);
720    ratePerSecondArg.addLongIdentifier("rate-per-second", true);
721    ratePerSecondArg.addLongIdentifier("requestsPerSecond", true);
722    ratePerSecondArg.addLongIdentifier("requests-per-second", true);
723    ratePerSecondArg.addLongIdentifier("operationsPerSecond", true);
724    ratePerSecondArg.addLongIdentifier("operations-per-second", true);
725    ratePerSecondArg.addLongIdentifier("opsPerSecond", true);
726    ratePerSecondArg.addLongIdentifier("ops-per-second", true);
727    ratePerSecondArg.addLongIdentifier("rate", true);
728    ratePerSecondArg.setArgumentGroupName(
729         INFO_PARALLEL_UPDATE_ARG_GROUP_PROCESSING.get());
730    parser.addArgument(ratePerSecondArg);
731
732    usePermissiveModifyArg = new BooleanArgument('M', "usePermissiveModify", 1,
733         INFO_PARALLEL_UPDATE_ARG_DESC_USE_PERMISSIVE_MODIFY.get());
734    usePermissiveModifyArg.addLongIdentifier("use-permissive-modify", true);
735    usePermissiveModifyArg.addLongIdentifier("permissiveModify", true);
736    usePermissiveModifyArg.addLongIdentifier("permissive-modify", true);
737    usePermissiveModifyArg.addLongIdentifier("permissive", true);
738    usePermissiveModifyArg.setArgumentGroupName(
739         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
740    parser.addArgument(usePermissiveModifyArg);
741
742    ignoreNoUserModificationArg = new BooleanArgument(null,
743         "ignoreNoUserModification", 1,
744         INFO_PARALLEL_UPDATE_ARG_DESC_IGNORE_NO_USER_MOD.get());
745    ignoreNoUserModificationArg.addLongIdentifier("ignore-no-user-modification",
746         true);
747    ignoreNoUserModificationArg.addLongIdentifier("ignoreNoUserMod", true);
748    ignoreNoUserModificationArg.addLongIdentifier("ignore-no-user-mod", true);
749    ignoreNoUserModificationArg.setArgumentGroupName(
750         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
751    parser.addArgument(ignoreNoUserModificationArg);
752
753    proxyAsArg = new StringArgument('Y', "proxyAs", false, 1,
754         INFO_PARALLEL_UPDATE_ARG_PLACEHOLDER_PROXY_AS.get(),
755         INFO_PARALLEL_UPDATE_ARG_DESC_PROXY_AS.get());
756    proxyAsArg.addLongIdentifier("proxy-as", true);
757    proxyAsArg.addLongIdentifier("proxyV2As", true);
758    proxyAsArg.addLongIdentifier("proxy-v2-as", true);
759    proxyAsArg.setArgumentGroupName(
760         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
761    parser.addArgument(proxyAsArg);
762
763    proxyV1AsArg = new DNArgument(null, "proxyV1As", false, 1, null,
764         INFO_PARALLEL_UPDATE_ARG_DESC_PROXY_V1_AS.get());
765    proxyV1AsArg.addLongIdentifier("proxy-v1-as", true);
766    proxyV1AsArg.setArgumentGroupName(
767         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
768    parser.addArgument(proxyV1AsArg);
769
770    passwordUpdateBehaviorArg = new StringArgument(null,
771         "passwordUpdateBehavior", false, 0,
772         INFO_PARALLEL_UPDATE_ARG_PLACEHOLDER_PW_UPDATE_BEHAVIOR.get(),
773         INFO_PARALLEL_UPDATE_ARG_DESC_PW_UPDATE_BEHAVIOR.get());
774    passwordUpdateBehaviorArg.addLongIdentifier("password-update-behavior",
775         true);
776    passwordUpdateBehaviorArg.setArgumentGroupName(
777         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
778    parser.addArgument(passwordUpdateBehaviorArg);
779
780    operationPurposeArg = new StringArgument(null, "operationPurpose", false, 1,
781         INFO_PARALLEL_UPDATE_ARG_PLACEHOLDER_OPERATION_PURPOSE.get(),
782         INFO_PARALLEL_UPDATE_ARG_DESC_OPERATION_PURPOSE.get());
783    operationPurposeArg.addLongIdentifier("operation-purpose", true);
784    operationPurposeArg.addLongIdentifier("purpose", true);
785    operationPurposeArg.setArgumentGroupName(
786         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
787    parser.addArgument(operationPurposeArg);
788
789    useManageDsaITArg = new BooleanArgument(null, "useManageDsaIT", 1,
790         INFO_PARALLEL_UPDATE_ARG_DESC_USE_MANAGE_DSA_IT.get());
791    useManageDsaITArg.addLongIdentifier("use-manage-dsa-it", true);
792    useManageDsaITArg.addLongIdentifier("manageDsaIT", true);
793    useManageDsaITArg.addLongIdentifier("manage-dsa-it", true);
794    useManageDsaITArg.setArgumentGroupName(
795         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
796    parser.addArgument(useManageDsaITArg);
797
798    nameWithEntryUUIDArg = new BooleanArgument(null, "nameWithEntryUUID", 1,
799         INFO_LDAPMODIFY_ARG_DESCRIPTION_NAME_WITH_ENTRY_UUID.get());
800    nameWithEntryUUIDArg.addLongIdentifier("name-with-entryuuid", true);
801    nameWithEntryUUIDArg.addLongIdentifier("name-with-entry-uuid", true);
802    nameWithEntryUUIDArg.setArgumentGroupName(
803         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
804    parser.addArgument(nameWithEntryUUIDArg);
805
806    softDeleteArg = new BooleanArgument(null, "useSoftDelete", 1,
807         INFO_PARALLEL_UPDATE_ARG_DESC_SOFT_DELETE.get());
808    softDeleteArg.addLongIdentifier("use-soft-delete", true);
809    softDeleteArg.addLongIdentifier("softDelete", true);
810    softDeleteArg.addLongIdentifier("soft-delete", true);
811    softDeleteArg.setArgumentGroupName(
812         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
813    parser.addArgument(softDeleteArg);
814
815    hardDeleteArg = new BooleanArgument(null, "useHardDelete", 1,
816         INFO_PARALLEL_UPDATE_ARG_DESC_HARD_DELETE.get());
817    hardDeleteArg.addLongIdentifier("use-hard-delete", true);
818    hardDeleteArg.addLongIdentifier("hardDelete", true);
819    hardDeleteArg.addLongIdentifier("hard-delete", true);
820    hardDeleteArg.setArgumentGroupName(
821         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
822    parser.addArgument(hardDeleteArg);
823
824    allowUndeleteArg = new BooleanArgument(null, "allowUndelete", 1,
825         INFO_PARALLEL_UPDATE_ARG_DESC_ALLOW_UNDELETE.get());
826    allowUndeleteArg.addLongIdentifier("allow-undelete", true);
827    allowUndeleteArg.addLongIdentifier("undelete", true);
828    allowUndeleteArg.setArgumentGroupName(
829         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
830    parser.addArgument(allowUndeleteArg);
831
832    useAssuredReplicationArg = new BooleanArgument(null,
833         "useAssuredReplication", 1,
834         INFO_PWMOD_ARG_DESC_ASSURED_REPLICATION.get());
835    useAssuredReplicationArg.addLongIdentifier("use-assured-replication", true);
836    useAssuredReplicationArg.addLongIdentifier("assuredReplication", true);
837    useAssuredReplicationArg.addLongIdentifier("assured-replication", true);
838    useAssuredReplicationArg.setArgumentGroupName(
839         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
840    parser.addArgument(useAssuredReplicationArg);
841
842    assuredReplicationLocalLevelArg = new StringArgument(null,
843         "assuredReplicationLocalLevel", false, 1,
844         INFO_LDAPDELETE_ARG_PLACEHOLDER_ASSURED_REPLICATION_LOCAL_LEVEL.get(),
845         INFO_LDAPDELETE_ARG_DESC_ASSURED_REPLICATION_LOCAL_LEVEL.get(),
846         StaticUtils.setOf(
847              ASSURED_REPLICATION_LOCAL_LEVEL_NONE,
848              ASSURED_REPLICATION_LOCAL_LEVEL_RECEIVED_ANY_SERVER,
849              ASSURED_REPLICATION_LOCAL_LEVEL_PROCESSED_ALL_SERVERS),
850         (String) null);
851    assuredReplicationLocalLevelArg.addLongIdentifier(
852         "assured-replication-local-level", true);
853    assuredReplicationLocalLevelArg.setArgumentGroupName(
854         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
855    parser.addArgument(assuredReplicationLocalLevelArg);
856
857    assuredReplicationRemoteLevelArg = new StringArgument(null,
858         "assuredReplicationRemoteLevel", false, 1,
859         INFO_PARALLEL_UPDATE_ARG_PLACEHOLDER_ASSURED_REPLICATION_REMOTE_LEVEL.
860              get(),
861         INFO_PARALLEL_UPDATE_ARG_DESC_ASSURED_REPLICATION_REMOTE_LEVEL.get(),
862         StaticUtils.setOf(
863              ASSURED_REPLICATION_REMOTE_LEVEL_NONE,
864              ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ANY_REMOTE_LOCATION,
865              ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ALL_REMOTE_LOCATIONS,
866              ASSURED_REPLICATION_REMOTE_LEVEL_PROCESSED_ALL_REMOTE_SERVERS),
867         (String) null);
868    assuredReplicationRemoteLevelArg.addLongIdentifier(
869         "assured-replication-remote-level", true);
870    assuredReplicationRemoteLevelArg.setArgumentGroupName(
871         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
872    parser.addArgument(assuredReplicationRemoteLevelArg);
873
874    assuredReplicationTimeoutArg = new DurationArgument(null,
875         "assuredReplicationTimeout", false, null,
876         INFO_PWMOD_ARG_DESC_ASSURED_REPLICATION_TIMEOUT.get());
877    assuredReplicationTimeoutArg.addLongIdentifier(
878         "assured-replication-timeout", true);
879    assuredReplicationTimeoutArg.setArgumentGroupName(
880         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
881    parser.addArgument(assuredReplicationTimeoutArg);
882
883    replicationRepairArg = new BooleanArgument(null, "replicationRepair", 1,
884         INFO_PARALLEL_UPDATE_ARG_DESC_USE_REPLICATION_REPAIR.get());
885    replicationRepairArg.addLongIdentifier("replication-repair", true);
886    replicationRepairArg.setArgumentGroupName(
887         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
888    parser.addArgument(replicationRepairArg);
889
890    suppressOperationalAttributeUpdatesArg = new StringArgument(null,
891         "suppressOperationalAttributeUpdates", false, 0,
892         INFO_PARALLEL_UPDATE_ARG_PLACEHOLDER_SUPPRESS_OP_ATTR_UPDATES.get(),
893         INFO_PARALLEL_UPDATE_ARG_DESC_SUPPRESS_OP_ATTR_UPDATES.get(),
894         StaticUtils.setOf(
895              SUPPRESS_OP_ATTR_LAST_ACCESS_TIME,
896              SUPPRESS_OP_ATTR_LAST_LOGIN_TIME,
897              SUPPRESS_OP_ATTR_LAST_LOGIN_IP,
898              SUPPRESS_OP_ATTR_LASTMOD),
899         (String) null);
900    suppressOperationalAttributeUpdatesArg.addLongIdentifier(
901         "suppress-operational-attribute-updates", true);
902    suppressOperationalAttributeUpdatesArg.addLongIdentifier(
903         "suppressOperationalAttributeUpdate", true);
904    suppressOperationalAttributeUpdatesArg.addLongIdentifier(
905         "suppress-operational-attribute-update", true);
906    suppressOperationalAttributeUpdatesArg.setArgumentGroupName(
907         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
908    parser.addArgument(suppressOperationalAttributeUpdatesArg);
909
910    suppressReferentialIntegrityUpdatesArg = new BooleanArgument(null,
911         "suppressReferentialIntegrityUpdates", 1,
912         INFO_PARALLEL_UPDATE_ARG_DESC_SUPPRESS_REFERENTIAL_INTEGRITY_UPDATES.
913              get());
914    suppressReferentialIntegrityUpdatesArg.addLongIdentifier(
915         "suppress-referential-integrity-updates", true);
916    suppressReferentialIntegrityUpdatesArg.addLongIdentifier(
917         "suppressReferentialIntegrityUpdate", true);
918    suppressReferentialIntegrityUpdatesArg.addLongIdentifier(
919         "suppress-referential-integrity-update", true);
920    suppressReferentialIntegrityUpdatesArg.setArgumentGroupName(
921         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
922    parser.addArgument(suppressReferentialIntegrityUpdatesArg);
923
924    addControlArg = new ControlArgument(null, "addControl", false, 0, null,
925         INFO_PARALLEL_UPDATE_ARG_DESC_ADD_CONTROL.get());
926    addControlArg.addLongIdentifier("add-control", true);
927    addControlArg.setArgumentGroupName(
928         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
929    parser.addArgument(addControlArg);
930
931    bindControlArg = new ControlArgument(null, "bindControl", false, 0, null,
932         INFO_PARALLEL_UPDATE_ARG_DESC_BIND_CONTROL.get());
933    bindControlArg.addLongIdentifier("bind-control", true);
934    bindControlArg.setArgumentGroupName(
935         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
936    parser.addArgument(bindControlArg);
937
938    deleteControlArg = new ControlArgument(null, "deleteControl", false, 0,
939         null, INFO_PARALLEL_UPDATE_ARG_DESC_DELETE_CONTROL.get());
940    deleteControlArg.addLongIdentifier("delete-control", true);
941    deleteControlArg.setArgumentGroupName(
942         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
943    parser.addArgument(deleteControlArg);
944
945    modifyControlArg = new ControlArgument(null, "modifyControl", false, 0,
946         null, INFO_PARALLEL_UPDATE_ARG_DESC_MODIFY_CONTROL.get());
947    modifyControlArg.addLongIdentifier("modify-control", true);
948    modifyControlArg.setArgumentGroupName(
949         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
950    parser.addArgument(modifyControlArg);
951
952    modifyDNControlArg = new ControlArgument(null, "modifyDNControl", false, 0,
953         null, INFO_PARALLEL_UPDATE_ARG_DESC_MODIFY_DN_CONTROL.get());
954    modifyDNControlArg.addLongIdentifier("modify-dn-control", true);
955    modifyDNControlArg.setArgumentGroupName(
956         INFO_PARALLEL_UPDATE_ARG_GROUP_CONTROLS.get());
957    parser.addArgument(modifyDNControlArg);
958
959    parser.addExclusiveArgumentSet(followReferralsArg, useManageDsaITArg);
960
961    parser.addExclusiveArgumentSet(proxyAsArg, proxyV1AsArg);
962
963    parser.addExclusiveArgumentSet(softDeleteArg, hardDeleteArg);
964
965    parser.addDependentArgumentSet(assuredReplicationLocalLevelArg,
966         useAssuredReplicationArg);
967    parser.addDependentArgumentSet(assuredReplicationRemoteLevelArg,
968         useAssuredReplicationArg);
969    parser.addDependentArgumentSet(assuredReplicationTimeoutArg,
970         useAssuredReplicationArg);
971
972
973    parser.addExclusiveArgumentSet(useAssuredReplicationArg,
974         replicationRepairArg);
975  }
976
977
978
979  /**
980   * {@inheritDoc}
981   */
982  @Override()
983  public boolean supportsInteractiveMode()
984  {
985    return true;
986  }
987
988
989
990  /**
991   * {@inheritDoc}
992   */
993  @Override()
994  public boolean defaultsToInteractiveMode()
995  {
996    return true;
997  }
998
999
1000
1001  /**
1002   * {@inheritDoc}
1003   */
1004  @Override()
1005  public boolean supportsPropertiesFile()
1006  {
1007    return true;
1008  }
1009
1010
1011
1012  /**
1013   * {@inheritDoc}
1014   */
1015  @Override()
1016  protected boolean supportsDebugLogging()
1017  {
1018    return true;
1019  }
1020
1021
1022
1023  /**
1024   * {@inheritDoc}
1025   */
1026  @Override()
1027  protected boolean supportsAuthentication()
1028  {
1029    return true;
1030  }
1031
1032
1033
1034  /**
1035   * {@inheritDoc}
1036   */
1037  @Override()
1038  protected boolean defaultToPromptForBindPassword()
1039  {
1040    return true;
1041  }
1042
1043
1044
1045  /**
1046   * {@inheritDoc}
1047   */
1048  @Override()
1049  protected boolean supportsSASLHelp()
1050  {
1051    return true;
1052  }
1053
1054
1055
1056  /**
1057   * {@inheritDoc}
1058   */
1059  @Override()
1060  protected boolean includeAlternateLongIdentifiers()
1061  {
1062    return true;
1063  }
1064
1065
1066
1067  /**
1068   * {@inheritDoc}
1069   */
1070  @Override()
1071  @NotNull()
1072  protected List<Control> getBindControls()
1073  {
1074    final List<Control> bindControls = new ArrayList<>();
1075
1076    if ((bindControlArg != null) && bindControlArg.isPresent())
1077    {
1078      bindControls.addAll(bindControlArg.getValues());
1079    }
1080
1081    if ((suppressOperationalAttributeUpdatesArg != null) &&
1082         suppressOperationalAttributeUpdatesArg.isPresent())
1083    {
1084      final EnumSet<SuppressType> suppressTypes =
1085           EnumSet.noneOf(SuppressType.class);
1086      for (final String s : suppressOperationalAttributeUpdatesArg.getValues())
1087      {
1088        if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_ACCESS_TIME))
1089        {
1090          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
1091        }
1092        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_TIME))
1093        {
1094          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
1095        }
1096        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_IP))
1097        {
1098          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
1099        }
1100      }
1101
1102      bindControls.add(new SuppressOperationalAttributeUpdateRequestControl(
1103           true, suppressTypes));
1104    }
1105
1106    return Collections.emptyList();
1107  }
1108
1109
1110
1111  /**
1112   * {@inheritDoc}
1113   */
1114  @Override()
1115  protected boolean supportsMultipleServers()
1116  {
1117    return true;
1118  }
1119
1120
1121
1122  /**
1123   * {@inheritDoc}
1124   */
1125  @Override()
1126  protected boolean supportsSSLDebugging()
1127  {
1128    return true;
1129  }
1130
1131
1132
1133  /**
1134   * {@inheritDoc}
1135   */
1136  @Override()
1137  @NotNull()
1138  public LDAPConnectionOptions getConnectionOptions()
1139  {
1140    final LDAPConnectionOptions options = new LDAPConnectionOptions();
1141    options.setUseSynchronousMode(true);
1142    options.setFollowReferrals(
1143         ((followReferralsArg != null) && followReferralsArg.isPresent()));
1144    options.setUnsolicitedNotificationHandler(this);
1145    options.setResponseTimeoutMillis(0L);
1146    return options;
1147  }
1148
1149
1150
1151  /**
1152   * {@inheritDoc}
1153   */
1154  @Override()
1155  protected boolean logToolInvocationByDefault()
1156  {
1157    return true;
1158  }
1159
1160
1161
1162  /**
1163   * {@inheritDoc}
1164   */
1165  @Override()
1166  @Nullable()
1167  public String getToolCompletionMessage()
1168  {
1169    return completionMessage.get();
1170  }
1171
1172
1173
1174  /**
1175   * {@inheritDoc}
1176   */
1177  @Override()
1178  @NotNull()
1179  public ResultCode doToolProcessing()
1180  {
1181    // Create the sets of controls to include in each type of request.
1182    final Control[] addControls;
1183    final Control[] deleteControls;
1184    final Control[] modifyControls;
1185    final Control[] modifyDNControls;
1186    try
1187    {
1188      final List<Control> addControlList = new ArrayList<>();
1189      final List<Control> deleteControlList = new ArrayList<>();
1190      final List<Control> modifyControlList = new ArrayList<>();
1191      final List<Control> modifyDNControlList = new ArrayList<>();
1192
1193      getOperationControls(addControlList, deleteControlList,
1194           modifyControlList, modifyDNControlList);
1195
1196      addControls = StaticUtils.toArray(addControlList, Control.class);
1197      deleteControls = StaticUtils.toArray(deleteControlList, Control.class);
1198      modifyControls = StaticUtils.toArray(modifyControlList, Control.class);
1199      modifyDNControls =
1200           StaticUtils.toArray(modifyDNControlList, Control.class);
1201    }
1202    catch (final LDAPException e)
1203    {
1204      Debug.debugException(e);
1205      logCompletionMessage(true, e.getMessage());
1206      return e.getResultCode();
1207    }
1208
1209
1210    // Get the connection pool to use to communicate with the directory
1211    // server(s).
1212    final LDAPConnectionPool connectionPool;
1213    final int numThreads = numThreadsArg.getValue();
1214    try
1215    {
1216      connectionPool = getConnectionPool(numThreads, numThreads, 1, null, null,
1217           true, null);
1218      connectionPool.setConnectionPoolName("parallel-update");
1219      connectionPool.setRetryFailedOperationsDueToInvalidConnections(
1220           (! neverRetryArg.isPresent()));
1221    }
1222    catch (final LDAPException e)
1223    {
1224      Debug.debugException(e);
1225      logCompletionMessage(true,
1226           ERR_PARALLEL_UPDATE_CANNOT_CREATE_POOL.get(
1227                StaticUtils.getExceptionMessage(e)));
1228      return e.getResultCode();
1229    }
1230
1231
1232    // Create the LDIF reader that will read the changes to process.
1233    final LDIFReader ldifReader;
1234    final String encryptionPassphrase;
1235    try
1236    {
1237      final ObjectPair<LDIFReader,String> ldifReaderPair = createLDIFReader();
1238      ldifReader = ldifReaderPair.getFirst();
1239      encryptionPassphrase = ldifReaderPair.getSecond();
1240    }
1241    catch (final LDAPException e)
1242    {
1243      Debug.debugException(e);
1244      logCompletionMessage(true, e.getMessage());
1245      connectionPool.close();
1246      return e.getResultCode();
1247    }
1248
1249    final AtomicReference<ResultCode> resultCodeRef =
1250         new AtomicReference<>(ResultCode.SUCCESS);
1251    try
1252    {
1253      // If the LDIF file is encrypted, then get the ID of the encryption
1254      // settings definition (if any) used to generate the encryption key.
1255      final String encryptionSettingsDefinitionID;
1256      if (encryptionPassphrase == null)
1257      {
1258        encryptionSettingsDefinitionID = null;
1259      }
1260      else
1261      {
1262        encryptionSettingsDefinitionID = getEncryptionSettingsDefinitionID();
1263      }
1264
1265
1266      // Create the LDIF writer that will be used to write rejects.
1267      try
1268      {
1269        rejectWriter = createRejectWriter(encryptionPassphrase,
1270             encryptionSettingsDefinitionID);
1271      }
1272      catch (final LDAPException e)
1273      {
1274        Debug.debugException(e);
1275        logCompletionMessage(true, e.getMessage());
1276        return ResultCode.LOCAL_ERROR;
1277      }
1278
1279
1280      // If appropriate, create the log writer that will be used to provide a
1281      // log of the changes that are attempted.
1282      if (logFileArg.isPresent())
1283      {
1284        try
1285        {
1286          logWriter = new PrintWriter(logFileArg.getValue());
1287        }
1288        catch (final Exception e)
1289        {
1290          Debug.debugException(e);
1291          logCompletionMessage(true,
1292               ERR_PARALLEL_UPDATE_ERROR_CREATING_LOG_WRITER.get(
1293                    logFileArg.getValue().getAbsolutePath(),
1294                    StaticUtils.getExceptionMessage(e)));
1295          return ResultCode.LOCAL_ERROR;
1296        }
1297      }
1298
1299
1300      // Create The queue that will hold the operations to process.
1301      final ParallelUpdateOperationQueue operationQueue =
1302           new ParallelUpdateOperationQueue(this, numThreads,
1303                (2 * numThreads));
1304
1305
1306      // Create the rate limiter, if appropriate.
1307      if (ratePerSecondArg.isPresent())
1308      {
1309        rateLimiter = new FixedRateBarrier(1000L, ratePerSecondArg.getValue());
1310      }
1311      else
1312      {
1313        rateLimiter = null;
1314      }
1315
1316
1317      // Create and start all of the threads that will be used to process
1318      // requests.
1319      final List<ParallelUpdateOperationThread> operationThreadList =
1320           new ArrayList<>(numThreads);
1321      for (int i=1; i <= numThreads; i++)
1322      {
1323        final ParallelUpdateOperationThread operationThread =
1324             new ParallelUpdateOperationThread(this, connectionPool,
1325                  operationQueue, i, rateLimiter, addControls, deleteControls,
1326                  modifyControls, modifyDNControls,
1327                  allowUndeleteArg.isPresent());
1328        operationThreadList.add(operationThread);
1329        operationThread.start();
1330      }
1331
1332
1333      // Create a progress monitor that will be used to report periodic status
1334      // updates about the processing that has been performed.
1335      final ParallelUpdateProgressMonitor progressMonitor =
1336           new ParallelUpdateProgressMonitor(this);
1337      try
1338      {
1339        processingStartTimeMillis = System.currentTimeMillis();
1340        progressMonitor.start();
1341
1342        while (! shouldAbort.get())
1343        {
1344          final LDIFChangeRecord changeRecord;
1345          try
1346          {
1347            changeRecord = ldifReader.readChangeRecord(
1348                 defaultAddArg.isPresent());
1349          }
1350          catch (final LDIFException e)
1351          {
1352            Debug.debugException(e);
1353            if (e.mayContinueReading())
1354            {
1355              final String message =
1356                   ERR_PARALLEL_UPDATE_RECOVERABLE_LDIF_EXCEPTION.get(
1357                        ldifFileArg.getValue().getAbsolutePath(),
1358                        e.getMessage());
1359              logMessage(message);
1360              reject(null,
1361                   new LDAPException(ResultCode.DECODING_ERROR, message, e));
1362              opsAttempted.incrementAndGet();
1363              continue;
1364            }
1365            else
1366            {
1367              shouldAbort.set(true);
1368              final String message =
1369                   ERR_PARALLEL_UPDATE_UNRECOVERABLE_LDIF_EXCEPTION.get(
1370                        ldifFileArg.getValue().getAbsolutePath(),
1371                        StaticUtils.getExceptionMessage(e));
1372              reject(null,
1373                   new LDAPException(ResultCode.DECODING_ERROR, message, e));
1374              logCompletionMessage(true, message);
1375              return ResultCode.DECODING_ERROR;
1376            }
1377          }
1378          catch (final Exception e)
1379          {
1380            Debug.debugException(e);
1381            shouldAbort.set(true);
1382            final String message =
1383                 ERR_PARALLEL_UPDATE_ERROR_READING_LDIF_FILE.get(
1384                      ldifFileArg.getValue().getAbsolutePath(),
1385                      StaticUtils.getExceptionMessage(e));
1386            reject(null,
1387                 new LDAPException(ResultCode.LOCAL_ERROR, message, e));
1388            logCompletionMessage(true, message);
1389            return ResultCode.LOCAL_ERROR;
1390          }
1391
1392          if (changeRecord == null)
1393          {
1394            // We've reached the end of the LDIF file.
1395            break;
1396          }
1397          else
1398          {
1399            try
1400            {
1401              operationQueue.addChangeRecord(changeRecord);
1402            }
1403            catch (final Exception e)
1404            {
1405              Debug.debugException(e);
1406
1407              // This indicates that the attempt to enqueue the change record
1408              // was interrupted.  This shouldn't happen, but if it does, then
1409              // mark it to be retried.
1410              final LDAPException le = new LDAPException(ResultCode.LOCAL_ERROR,
1411                   ERR_PARALLEL_UPDATE_ENQUEUE_FAILED.get(
1412                        StaticUtils.getExceptionMessage(e)),
1413                   e);
1414              retry(changeRecord, le);
1415            }
1416          }
1417        }
1418
1419
1420        // If a failure was encountered, then abort.
1421        if (shouldAbort.get())
1422        {
1423          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1424               ResultCode.LOCAL_ERROR);
1425          return resultCodeRef.get();
1426        }
1427
1428
1429        // Indicate that we've reached the end of the LDIF file.
1430        out();
1431        wrapOut(0, WRAP_COLUMN,
1432             INFO_PARALLEL_UPDATE_END_OF_LDIF.get());
1433        out();
1434
1435
1436        // Wait for the operation queue to become idle so that we know there
1437        // are no more outstanding operations to be processed.
1438        operationQueue.waitUntilIdle();
1439        initialAttempted = opsAttempted.get();
1440        initialSucceeded = opsSucceeded.get();
1441
1442
1443        // If there are any operations to retry, then do so now.
1444        Map<DN,List<ObjectPair<LDIFChangeRecord,LDAPException>>> retryQueueCopy;
1445        synchronized (retryQueue)
1446        {
1447          retryQueueCopy = new TreeMap<>(retryQueue);
1448          retryQueue.clear();
1449        }
1450
1451        int lastRetryQueueSize = 0;
1452        while ((! retryQueueCopy.isEmpty()) &&
1453             (retryQueueCopy.size() != lastRetryQueueSize))
1454        {
1455          out();
1456          wrapOut(0, WRAP_COLUMN,
1457               INFO_PARALLEL_UPDATE_BEGINNING_RETRY.get(retryQueueCopy.size()));
1458          out();
1459
1460
1461          for (final
1462               Map.Entry<DN,List<ObjectPair<LDIFChangeRecord,LDAPException>>>
1463               e : retryQueueCopy.entrySet())
1464          {
1465            for (final ObjectPair<LDIFChangeRecord,LDAPException> p :
1466              e.getValue())
1467            {
1468              if (shouldAbort.get())
1469              {
1470                resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1471                     ResultCode.LOCAL_ERROR);
1472                return resultCodeRef.get();
1473              }
1474
1475              final LDIFChangeRecord changeRecord = p.getFirst();
1476              try
1477              {
1478                operationQueue.addChangeRecord(changeRecord);
1479                retryQueueSize.decrementAndGet();
1480              }
1481              catch (final Exception ex)
1482              {
1483                Debug.debugException(ex);
1484
1485                // This indicates that the attempt to enqueue the change record
1486                // was interrupted.  This shouldn't happen, but if it does, then
1487                // mark it to be retried.
1488                final LDAPException le = new LDAPException(
1489                     ResultCode.LOCAL_ERROR,
1490                     ERR_PARALLEL_UPDATE_ENQUEUE_FAILED.get(
1491                          StaticUtils.getExceptionMessage(ex)),
1492                     ex);
1493                retry(changeRecord, le);
1494              }
1495            }
1496          }
1497
1498
1499          operationQueue.waitUntilIdle();
1500          lastRetryQueueSize = retryQueueCopy.size();
1501
1502          synchronized (retryQueue)
1503          {
1504            retryQueueCopy = new TreeMap<>(retryQueue);
1505            retryQueue.clear();
1506          }
1507        }
1508
1509
1510        // If we've gotten here, then it means that either the retry queue
1511        // (NOTE:  we actually need to use retryQueueCopy) is empty or none of
1512        // the retry attempts succeeded on the last pass.  If it's the latter,
1513        // then reject any of the remaining operations.
1514        synchronized (retryQueue)
1515        {
1516          final int remainingToRetry = retryQueueCopy.size();
1517          if (remainingToRetry > 0)
1518          {
1519            if (remainingToRetry == 1)
1520            {
1521              wrapErr(0, WRAP_COLUMN,
1522                   ERR_PARALLEL_UPDATE_NO_PROGRESS_ONE.get());
1523            }
1524            else
1525            {
1526              wrapErr(0, WRAP_COLUMN,
1527                   ERR_PARALLEL_UPDATE_NO_PROGRESS_MULTIPLE.get(
1528                        remainingToRetry));
1529            }
1530          }
1531
1532          for (final
1533               Map.Entry<DN,List<ObjectPair<LDIFChangeRecord,LDAPException>>>
1534               e : retryQueueCopy.entrySet())
1535          {
1536            for (final ObjectPair<LDIFChangeRecord,LDAPException> p :
1537                 e.getValue())
1538            {
1539              reject(p.getFirst(), p.getSecond());
1540              retryQueueSize.decrementAndGet();
1541            }
1542          }
1543        }
1544      }
1545      finally
1546      {
1547        operationQueue.setEndOfLDIF();
1548        operationQueue.waitUntilIdle();
1549
1550        for (final ParallelUpdateOperationThread operationThread :
1551             operationThreadList)
1552        {
1553          try
1554          {
1555            operationThread.join();
1556          }
1557          catch (final Exception e)
1558          {
1559            Debug.debugException(e);
1560            logCompletionMessage(true,
1561                 ERR_PARALLEL_UPDATE_CANNOT_JOIN_THREAD.get(
1562                      operationThread.getName(),
1563                      StaticUtils.getExceptionMessage(e)));
1564            resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1565                 ResultCode.LOCAL_ERROR);
1566          }
1567        }
1568
1569        try
1570        {
1571          progressMonitor.stopRunning();
1572          progressMonitor.join();
1573        }
1574        catch (final Exception e)
1575        {
1576          Debug.debugException(e);
1577          logCompletionMessage(true,
1578               ERR_PARALLEL_UPDATE_CANNOT_JOIN_PROGRESS_MONITOR.get(
1579                    StaticUtils.getExceptionMessage(e)));
1580          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1581               ResultCode.LOCAL_ERROR);
1582        }
1583      }
1584    }
1585    finally
1586    {
1587      connectionPool.close();
1588
1589      if (rejectWriter != null)
1590      {
1591        try
1592        {
1593          rejectWriter.close();
1594        }
1595        catch (final Exception e)
1596        {
1597          Debug.debugException(e);
1598          logCompletionMessage(true,
1599               ERR_PARALLEL_UPDATE_ERROR_CLOSING_REJECT_WRITER.get(
1600                    rejectFileArg.getValue().getAbsolutePath(),
1601                    StaticUtils.getExceptionMessage(e)));
1602          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1603               ResultCode.LOCAL_ERROR);
1604        }
1605      }
1606
1607      if (logWriter != null)
1608      {
1609        try
1610        {
1611          logWriter.close();
1612        }
1613        catch (final Exception e)
1614        {
1615          Debug.debugException(e);
1616          logCompletionMessage(true,
1617               ERR_PARALLEL_UPDATE_ERROR_CLOSING_LOG_WRITER.get(
1618                    logFileArg.getValue().getAbsolutePath(),
1619                    StaticUtils.getExceptionMessage(e)));
1620          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1621               ResultCode.LOCAL_ERROR);
1622        }
1623      }
1624
1625      try
1626      {
1627        ldifReader.close();
1628      }
1629      catch (final Exception e)
1630      {
1631        Debug.debugException(e);
1632        logCompletionMessage(true,
1633             WARN_PARALLEL_UPDATE_ERROR_CLOSING_READER.get(
1634                  ldifFileArg.getValue().getAbsolutePath(),
1635                  StaticUtils.getExceptionMessage(e)));
1636        resultCodeRef.compareAndSet(ResultCode.SUCCESS, ResultCode.LOCAL_ERROR);
1637      }
1638    }
1639
1640
1641    // If we've gotten here, then processing has completed.  Print some summary
1642    // messages and return an appropriate result code.
1643    final long processingDurationMillis =
1644         System.currentTimeMillis() - processingStartTimeMillis;
1645    final long numAttempts = opsAttempted.get();
1646    final long numSuccesses = opsSucceeded.get();
1647    final long numRejects = opsRejected.get();
1648
1649    final long retryAttempts = numAttempts - initialAttempted;
1650    final long retrySuccesses = numSuccesses - initialSucceeded;
1651
1652    out(INFO_PARALLEL_UPDATE_DONE.get(getToolName()));
1653    out(INFO_PARALLEL_UPDATE_SUMMARY_OPS_ATTEMPTED.get(numAttempts));
1654
1655    if (retryAttempts > 0L)
1656    {
1657      out(INFO_PARALLEL_UPDATE_SUMMARY_INITIAL_ATTEMPTS.get(initialAttempted));
1658      out(INFO_PARALLEL_UPDATE_SUMMARY_RETRY_ATTEMPTS.get(retryAttempts));
1659    }
1660
1661    out(INFO_PARALLEL_UPDATE_SUMMARY_OPS_SUCCEEDED.get(numSuccesses));
1662
1663    if (retryAttempts > 0)
1664    {
1665      out(INFO_PARALLEL_UPDATED_OPS_SUCCEEDED_INITIAL.get(initialSucceeded));
1666      out(INFO_PARALLEL_UPDATED_OPS_SUCCEEDED_RETRY.get(retrySuccesses));
1667    }
1668
1669    out(INFO_PARALLEL_UPDATE_SUMMARY_OPS_REJECTED.get(numRejects));
1670    out(INFO_PARALLEL_UPDATE_SUMMARY_DURATION.get(
1671         StaticUtils.millisToHumanReadableDuration(processingDurationMillis)));
1672
1673    if ((numAttempts > 0L) && (processingDurationMillis > 0L))
1674    {
1675      final double attemptsPerSecond =
1676           numAttempts * 1_000.0d / processingDurationMillis;
1677      final DecimalFormat decimalFormat = new DecimalFormat("0.000");
1678      out(INFO_PARALLEL_UPDATE_SUMMARY_RATE.get(
1679           decimalFormat.format(attemptsPerSecond)));
1680    }
1681
1682
1683    if (numRejects == 0L)
1684    {
1685      completionMessage.compareAndSet(null,
1686           INFO_PARALLEL_UPDATE_COMPLETION_MESSAGE_ALL_SUCCEEDED.get(
1687                getToolName()));
1688    }
1689    else if (numRejects == 1L)
1690    {
1691      completionMessage.compareAndSet(null,
1692           INFO_PARALLEL_UPDATE_COMPLETION_MESSAGE_ONE_REJECTED.get(
1693                getToolName()));
1694    }
1695    else
1696    {
1697      completionMessage.compareAndSet(null,
1698           INFO_PARALLEL_UPDATE_COMPLETION_MESSAGE_MULTIPLE_REJECTED.get(
1699                getToolName(), numRejects));
1700    }
1701
1702
1703    ResultCode finalResultCode = resultCodeRef.get();
1704    if ((finalResultCode == ResultCode.SUCCESS) &&
1705         useFirstRejectResultCodeAsExitCodeArg.isPresent() &&
1706         (firstRejectResultCode.get() != null))
1707    {
1708      finalResultCode = firstRejectResultCode.get();
1709    }
1710
1711    return finalResultCode;
1712  }
1713
1714
1715
1716  /**
1717   * Updates the provided lists with the appropriate controls to include in
1718   * each type of request.
1719   *
1720   * @param  addControls       The list that should be updated with controls to
1721   *                           include in add requests.  It must not be
1722   *                           {@code null} and must be updatable.
1723   * @param  deleteControls    The list that should be updated with controls to
1724   *                           include in delete requests.  It must not be
1725   *                           {@code null} and must be updatable.
1726   * @param  modifyControls    The list that should be updated with controls to
1727   *                           include in modify requests.  It must not be
1728   *                           {@code null} and must be updatable.
1729   * @param  modifyDNControls  The list that should be updated with controls to
1730   *                           include in modify DN requests.  It must not be
1731   *                           {@code null} and must be updatable.
1732   *
1733   * @throws  LDAPException  If a problem is encountered while creating any of
1734   *                         the controls.
1735   */
1736  private void getOperationControls(
1737                    @NotNull final List<Control> addControls,
1738                    @NotNull final List<Control> deleteControls,
1739                    @NotNull final List<Control> modifyControls,
1740                    @NotNull final List<Control> modifyDNControls)
1741          throws LDAPException
1742  {
1743    if (addControlArg.isPresent())
1744    {
1745      addControls.addAll(addControlArg.getValues());
1746    }
1747
1748    if (deleteControlArg.isPresent())
1749    {
1750      deleteControls.addAll(deleteControlArg.getValues());
1751    }
1752
1753    if (modifyControlArg.isPresent())
1754    {
1755      modifyControls.addAll(modifyControlArg.getValues());
1756    }
1757
1758    if (modifyDNControlArg.isPresent())
1759    {
1760      modifyDNControls.addAll(modifyDNControlArg.getValues());
1761    }
1762
1763    if (proxyAsArg.isPresent())
1764    {
1765      final ProxiedAuthorizationV2RequestControl c =
1766           new ProxiedAuthorizationV2RequestControl(proxyAsArg.getValue());
1767      addControls.add(c);
1768      deleteControls.add(c);
1769      modifyControls.add(c);
1770      modifyDNControls.add(c);
1771    }
1772    else if (proxyV1AsArg.isPresent())
1773    {
1774      final ProxiedAuthorizationV1RequestControl c =
1775           new ProxiedAuthorizationV1RequestControl(proxyV1AsArg.getValue());
1776      addControls.add(c);
1777      deleteControls.add(c);
1778      modifyControls.add(c);
1779      modifyDNControls.add(c);
1780    }
1781
1782    if (usePermissiveModifyArg.isPresent())
1783    {
1784      modifyControls.add(new PermissiveModifyRequestControl(true));
1785    }
1786
1787    if (ignoreNoUserModificationArg.isPresent())
1788    {
1789      final IgnoreNoUserModificationRequestControl c =
1790           new IgnoreNoUserModificationRequestControl();
1791      addControls.add(c);
1792      modifyControls.add(c);
1793    }
1794
1795    if (useManageDsaITArg.isPresent())
1796    {
1797      final ManageDsaITRequestControl c = new ManageDsaITRequestControl(true);
1798      addControls.add(c);
1799      deleteControls.add(c);
1800      modifyControls.add(c);
1801      modifyDNControls.add(c);
1802    }
1803
1804    if (nameWithEntryUUIDArg.isPresent())
1805    {
1806      addControls.add(new NameWithEntryUUIDRequestControl(true));
1807    }
1808
1809    if (softDeleteArg.isPresent())
1810    {
1811      deleteControls.add(new SoftDeleteRequestControl(true, true));
1812    }
1813    else if (hardDeleteArg.isPresent())
1814    {
1815      deleteControls.add(new HardDeleteRequestControl(true));
1816    }
1817
1818    if (operationPurposeArg.isPresent())
1819    {
1820      final OperationPurposeRequestControl c =
1821           new OperationPurposeRequestControl(false, "parallel-update",
1822                Version.NUMERIC_VERSION_STRING,
1823                ParallelUpdate.class.getName() + ".getOperationControls",
1824                operationPurposeArg.getValue());
1825      addControls.add(c);
1826      deleteControls.add(c);
1827      modifyControls.add(c);
1828      modifyDNControls.add(c);
1829    }
1830
1831    if (replicationRepairArg.isPresent())
1832    {
1833      final ReplicationRepairRequestControl c =
1834           new ReplicationRepairRequestControl();
1835      addControls.add(c);
1836      deleteControls.add(c);
1837      modifyControls.add(c);
1838      modifyDNControls.add(c);
1839    }
1840
1841    if (suppressReferentialIntegrityUpdatesArg.isPresent())
1842    {
1843      final SuppressReferentialIntegrityUpdatesRequestControl c =
1844           new SuppressReferentialIntegrityUpdatesRequestControl(true);
1845      deleteControls.add(c);
1846      modifyDNControls.add(c);
1847    }
1848
1849
1850    if (useAssuredReplicationArg.isPresent())
1851    {
1852      final AssuredReplicationLocalLevel localLevel;
1853      if (assuredReplicationLocalLevelArg.isPresent())
1854      {
1855        final String localLevelStr = StaticUtils.toLowerCase(
1856             assuredReplicationLocalLevelArg.getValue());
1857        switch (localLevelStr)
1858        {
1859          case ASSURED_REPLICATION_LOCAL_LEVEL_NONE:
1860            localLevel = AssuredReplicationLocalLevel.NONE;
1861            break;
1862          case ASSURED_REPLICATION_LOCAL_LEVEL_RECEIVED_ANY_SERVER:
1863            localLevel = AssuredReplicationLocalLevel.RECEIVED_ANY_SERVER;
1864            break;
1865          case ASSURED_REPLICATION_LOCAL_LEVEL_PROCESSED_ALL_SERVERS:
1866            localLevel = AssuredReplicationLocalLevel.PROCESSED_ALL_SERVERS;
1867            break;
1868          default:
1869            // This should never happen.
1870            localLevel = null;
1871            break;
1872        }
1873      }
1874      else
1875      {
1876        localLevel = null;
1877      }
1878
1879      final AssuredReplicationRemoteLevel remoteLevel;
1880      if (assuredReplicationRemoteLevelArg.isPresent())
1881      {
1882        final String remoteLevelStr = StaticUtils.toLowerCase(
1883             assuredReplicationRemoteLevelArg.getValue());
1884        switch (remoteLevelStr)
1885        {
1886          case ASSURED_REPLICATION_REMOTE_LEVEL_NONE:
1887            remoteLevel = AssuredReplicationRemoteLevel.NONE;
1888            break;
1889          case ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ANY_REMOTE_LOCATION:
1890            remoteLevel = AssuredReplicationRemoteLevel.
1891                 RECEIVED_ANY_REMOTE_LOCATION;
1892            break;
1893          case ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ALL_REMOTE_LOCATIONS:
1894            remoteLevel = AssuredReplicationRemoteLevel.
1895                 RECEIVED_ALL_REMOTE_LOCATIONS;
1896            break;
1897          case ASSURED_REPLICATION_REMOTE_LEVEL_PROCESSED_ALL_REMOTE_SERVERS:
1898            remoteLevel = AssuredReplicationRemoteLevel.
1899                 PROCESSED_ALL_REMOTE_SERVERS;
1900            break;
1901          default:
1902            // This should never happen.
1903            remoteLevel = null;
1904            break;
1905        }
1906      }
1907      else
1908      {
1909        remoteLevel = null;
1910      }
1911
1912      final Long timeoutMillis;
1913      if (assuredReplicationTimeoutArg.isPresent())
1914      {
1915        timeoutMillis = assuredReplicationTimeoutArg.getValue(
1916             TimeUnit.MILLISECONDS);
1917      }
1918      else
1919      {
1920        timeoutMillis = null;
1921      }
1922
1923      final AssuredReplicationRequestControl c =
1924           new AssuredReplicationRequestControl(true, localLevel, localLevel,
1925                remoteLevel, remoteLevel, timeoutMillis, false);
1926      addControls.add(c);
1927      deleteControls.add(c);
1928      modifyControls.add(c);
1929      modifyDNControls.add(c);
1930    }
1931
1932
1933    if (passwordUpdateBehaviorArg.isPresent())
1934    {
1935      final PasswordUpdateBehaviorRequestControlProperties properties =
1936           new PasswordUpdateBehaviorRequestControlProperties();
1937      for (final String argValue : passwordUpdateBehaviorArg.getValues())
1938      {
1939        final int equalPos = argValue.indexOf('=');
1940        if (equalPos < 0)
1941        {
1942          throw new LDAPException(ResultCode.PARAM_ERROR,
1943               ERR_PARALLEL_UPDATE_MALFORMED_PW_UPDATE_VALUE.get(
1944                    argValue, passwordUpdateBehaviorArg.getIdentifierString()));
1945        }
1946
1947        final String propertyName = argValue.substring(0, equalPos).trim();
1948        final String lowerName = StaticUtils.toLowerCase(propertyName);
1949        switch (lowerName)
1950        {
1951          case PW_UPDATE_BEHAVIOR_NAME_IS_SELF_CHANGE:
1952            properties.setIsSelfChange(
1953                 getBooleanPWUpdateBehaviorValue(argValue));
1954            break;
1955          case PW_UPDATE_BEHAVIOR_NAME_ALLOW_PRE_ENCODED_PW:
1956            properties.setAllowPreEncodedPassword(
1957                 getBooleanPWUpdateBehaviorValue(argValue));
1958            break;
1959          case PW_UPDATE_BEHAVIOR_NAME_SKIP_PW_VALIDATION:
1960            properties.setSkipPasswordValidation(
1961                 getBooleanPWUpdateBehaviorValue(argValue));
1962            break;
1963          case PW_UPDATE_BEHAVIOR_NAME_IGNORE_PW_HISTORY:
1964            properties.setIgnorePasswordHistory(
1965                 getBooleanPWUpdateBehaviorValue(argValue));
1966            break;
1967          case PW_UPDATE_BEHAVIOR_NAME_IGNORE_MIN_PW_AGE:
1968            properties.setIgnoreMinimumPasswordAge(
1969                 getBooleanPWUpdateBehaviorValue(argValue));
1970            break;
1971          case PW_UPDATE_BEHAVIOR_NAME_MUST_CHANGE_PW:
1972            properties.setMustChangePassword(
1973                 getBooleanPWUpdateBehaviorValue(argValue));
1974            break;
1975          case PW_UPDATE_BEHAVIOR_NAME_PW_STORAGE_SCHEME:
1976            final String propertyValue = argValue.substring(equalPos+1).trim();
1977            properties.setPasswordStorageScheme(propertyValue);
1978            break;
1979          default:
1980            throw new LDAPException(ResultCode.PARAM_ERROR,
1981                 ERR_PARALLEL_UPDATE_UNKNOWN_PW_UPDATE_PROP.get(argValue,
1982                      passwordUpdateBehaviorArg.getIdentifierString(),
1983                      PW_UPDATE_BEHAVIOR_NAME_IS_SELF_CHANGE,
1984                      PW_UPDATE_BEHAVIOR_NAME_ALLOW_PRE_ENCODED_PW,
1985                      PW_UPDATE_BEHAVIOR_NAME_SKIP_PW_VALIDATION,
1986                      PW_UPDATE_BEHAVIOR_NAME_IGNORE_PW_HISTORY,
1987                      PW_UPDATE_BEHAVIOR_NAME_IGNORE_MIN_PW_AGE,
1988                      PW_UPDATE_BEHAVIOR_NAME_PW_STORAGE_SCHEME,
1989                      PW_UPDATE_BEHAVIOR_NAME_MUST_CHANGE_PW));
1990        }
1991      }
1992
1993      final PasswordUpdateBehaviorRequestControl c =
1994           new PasswordUpdateBehaviorRequestControl(properties, true);
1995      addControls.add(c);
1996      modifyControls.add(c);
1997    }
1998
1999
2000    if (suppressOperationalAttributeUpdatesArg.isPresent())
2001    {
2002      final EnumSet<SuppressType> suppressTypes =
2003           EnumSet.noneOf(SuppressType.class);
2004      for (final String s : suppressOperationalAttributeUpdatesArg.getValues())
2005      {
2006        if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_ACCESS_TIME))
2007        {
2008          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
2009        }
2010        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_TIME))
2011        {
2012          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
2013        }
2014        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_IP))
2015        {
2016          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
2017        }
2018      }
2019
2020      final SuppressOperationalAttributeUpdateRequestControl c =
2021           new SuppressOperationalAttributeUpdateRequestControl(true,
2022                suppressTypes);
2023      addControls.add(c);
2024      deleteControls.add(c);
2025      modifyControls.add(c);
2026      modifyDNControls.add(c);
2027    }
2028  }
2029
2030
2031
2032  /**
2033   * Retrieves the value from the provided name-value pair and parses it as a
2034   * boolean.
2035   *
2036   * @param  nameValuePair  The name-value pair to be parsed.  It must not be
2037   *                        {@code null} and it must contain an equal sign.
2038   *
2039   * @return  The boolean value parsed from the provided name-value pair.
2040   *
2041   * @throws  LDAPException  If the value could not be parsed as a boolean.
2042   */
2043  private boolean getBooleanPWUpdateBehaviorValue(
2044               @NotNull final String nameValuePair)
2045          throws LDAPException
2046  {
2047    final int equalPos = nameValuePair.indexOf('=');
2048    final String propertyValue = nameValuePair.substring(equalPos+1).trim();
2049    final String lowerValue = StaticUtils.toLowerCase(propertyValue);
2050    switch (lowerValue)
2051    {
2052      case "true":
2053      case "t":
2054      case "yes":
2055      case "on":
2056      case "1":
2057        return true;
2058      case "false":
2059      case "f":
2060      case "no":
2061      case "off":
2062      case "0":
2063        return false;
2064      default:
2065        final String propertyName = nameValuePair.substring(0, equalPos).trim();
2066        throw new LDAPException(ResultCode.PARAM_ERROR,
2067             ERR_PARALLEL_UPDATE_PW_UPDATE_VALUE_NOT_BOOLEAN.get(nameValuePair,
2068                  passwordUpdateBehaviorArg.getIdentifierString(),
2069                  propertyName));
2070    }
2071  }
2072
2073
2074
2075  /**
2076   * Creates the LDIF reader to use to read the changes to process.
2077   *
2078   * @return  An object pair in which the first element is the LDIF reader and
2079   *          the second element is the passphrase used to encrypt the contents
2080   *          of the LDIF file (or {@code null} if the LDIF file is not
2081   *          encrypted).
2082   *
2083   * @throws  LDAPException  If a problem occurs while trying to create the LDIF
2084   *                         reader.
2085   */
2086  @NotNull()
2087  private ObjectPair<LDIFReader,String> createLDIFReader()
2088          throws LDAPException
2089  {
2090    final File ldifFile = ldifFileArg.getValue();
2091
2092    try
2093    {
2094      final String encryptionPassphraseFromFile;
2095      if (encryptionPassphraseFileArg.isPresent())
2096      {
2097        final char[] pwChars = getPasswordFileReader().readPassword(
2098             encryptionPassphraseFileArg.getValue());
2099        encryptionPassphraseFromFile = new String(pwChars);
2100        Arrays.fill(pwChars, '\u0000');
2101      }
2102      else
2103      {
2104        encryptionPassphraseFromFile = null;
2105      }
2106
2107      final ObjectPair<InputStream,String> inputStreamPair =
2108           ToolUtils.getInputStreamForLDIFFiles(
2109                Collections.singletonList(ldifFile),
2110                encryptionPassphraseFromFile, getOut(), getErr());
2111
2112      final LDIFReader ldifReader = new LDIFReader(inputStreamPair.getFirst());
2113      final String encryptionPassphrase = inputStreamPair.getSecond();
2114      return new ObjectPair<>(ldifReader, encryptionPassphrase);
2115    }
2116    catch (final Exception e)
2117    {
2118      Debug.debugException(e);
2119      throw new LDAPException(ResultCode.LOCAL_ERROR,
2120           ERR_PARALLEL_UPDATE_CANNOT_CREATE_LDIF_READER.get(
2121                ldifFile.getAbsolutePath(),
2122                StaticUtils.getExceptionMessage(e)),
2123           e);
2124    }
2125  }
2126
2127
2128
2129  /**
2130   * Attempts to retrieve the ID of the encryption settings definition used to
2131   * encrypt the LDIF file.  This method should only be used if the LDIF file is
2132   * known to be encrypted.
2133   *
2134   * @return  The ID of the encryption settings definition used to encrypt the
2135   *          LDIF file, or {code null} if it was not encrypted with a
2136   *          passphrase obtained from an encryption settings definition (or if
2137   *          an error occurred while attempting to retrieve the ID).
2138   */
2139  @Nullable()
2140  private String getEncryptionSettingsDefinitionID()
2141  {
2142    try (FileInputStream inputStream =
2143              new FileInputStream(ldifFileArg.getValue()))
2144    {
2145      final PassphraseEncryptedStreamHeader encryptionHeader =
2146           PassphraseEncryptedStreamHeader.readFrom(inputStream, null);
2147      return encryptionHeader.getKeyIdentifier();
2148    }
2149    catch (final Exception e)
2150    {
2151      Debug.debugException(e);
2152      return null;
2153    }
2154  }
2155
2156
2157
2158  /**
2159   * Creates the LDIF writer that will be used to write information about
2160   * rejected entries.  If the LDIF input file was encrypted, then the reject
2161   * file will be encrypted with the same settings, and it will also be
2162   * compressed (regardless of whether the input file was compressed).
2163   *
2164   * @param  encryptionPassphrase            The passphrase used to encrypt the
2165   *                                         input file.  This may be
2166   *                                         {@code null} if the input file was
2167   *                                         not encrypted.
2168   * @param  encryptionSettingsDefinitionID  The ID for the encryption settings
2169   *                                         definition with which the
2170   *                                         passphrase is associated.  It may
2171   *                                         be {@code null} if the input file
2172   *                                         was not encrypted, or if the
2173   *                                         encryption passphrase was not
2174   *                                         obtained from an encryption
2175   *                                         settings definition.
2176   *
2177   * @return  The LDIF writer to which rejects should be written.
2178   *
2179   * @throws  LDAPException  If a problem occurs while creating the reject
2180   *                         writer.
2181   */
2182  @NotNull()
2183  private LDIFWriter createRejectWriter(
2184               @Nullable final String encryptionPassphrase,
2185               @Nullable final String encryptionSettingsDefinitionID)
2186          throws LDAPException
2187  {
2188    final File rejectFile = rejectFileArg.getValue();
2189
2190    OutputStream outputStream = null;
2191    try
2192    {
2193      outputStream = new FileOutputStream(rejectFile);
2194
2195      if (encryptionPassphrase != null)
2196      {
2197        outputStream = new PassphraseEncryptedOutputStream(encryptionPassphrase,
2198             outputStream, encryptionSettingsDefinitionID, true, true);
2199        outputStream = new GZIPOutputStream(outputStream);
2200      }
2201
2202
2203      final LDIFWriter ldifWriter = new LDIFWriter(outputStream);
2204
2205      // Set the wrap column to the maximum allowed value to ensure that
2206      // comments dont' get wrapped.
2207      ldifWriter.setWrapColumn(Integer.MAX_VALUE);
2208
2209      return ldifWriter;
2210    }
2211    catch (final Exception e)
2212    {
2213      Debug.debugException(e);
2214
2215      if (outputStream != null)
2216      {
2217        try
2218        {
2219          outputStream.close();
2220        }
2221        catch (final Exception e2)
2222        {
2223          Debug.debugException(e2);
2224        }
2225      }
2226
2227      throw new LDAPException(ResultCode.LOCAL_ERROR,
2228           ERR_PARALLEL_UPDATE_ERROR_CREATING_REJECT_WRITER.get(
2229                rejectFile.getAbsolutePath(),
2230                StaticUtils.getExceptionMessage(e)),
2231           e);
2232    }
2233  }
2234
2235
2236
2237  /**
2238   * Writes the provided message to standard output or standard error and sets
2239   * it as the completion message if there isn't one already.
2240   *
2241   * @param  isError  Indicates whether the message represents an error.
2242   * @param  message  The message to log.
2243   */
2244  private void logCompletionMessage(final boolean isError,
2245                                    @NotNull final String message)
2246  {
2247    completionMessage.compareAndSet(null, message);
2248    logMessage(message);
2249    if (isError)
2250    {
2251      wrapErr(0, WRAP_COLUMN, message);
2252    }
2253    else
2254    {
2255      wrapOut(0, WRAP_COLUMN, message);
2256    }
2257  }
2258
2259
2260
2261  /**
2262   * Logs the provided message if logging is enabled.
2263   *
2264   * @param  messageElements  The elements that make up the message to log.
2265   */
2266  private void logMessage(@NotNull final Object... messageElements)
2267  {
2268    if (logWriter != null)
2269    {
2270      SimpleDateFormat timestampFormatter = timestampFormatters.get();
2271      if (timestampFormatter == null)
2272      {
2273        timestampFormatter =
2274             new SimpleDateFormat("'['dd/MMM/yyyy:HH:mm:ss Z']'");
2275        timestampFormatters.set(timestampFormatter);
2276      }
2277
2278      final String timestamp = timestampFormatter.format(new Date());
2279
2280      final StringBuilder message = new StringBuilder();
2281      message.append('[');
2282      message.append(timestamp);
2283      message.append("] ");
2284      for (final Object o : messageElements)
2285      {
2286        message.append(String.valueOf(o));
2287      }
2288
2289      try
2290      {
2291        logWriter.println(message);
2292      }
2293      catch (final Exception e)
2294      {
2295        Debug.debugException(e);
2296        final String errorMessage =
2297             ERR_PARALLEL_UPDATE_CANNOT_WRITE_LOG_MESSAGE.get(
2298                  message.toString(), logFileArg.getValue().getAbsolutePath(),
2299                  StaticUtils.getExceptionMessage(e));
2300        wrapErr(0, WRAP_COLUMN, errorMessage);
2301        completionMessage.compareAndSet(null, errorMessage);
2302        shouldAbort.set(true);
2303      }
2304    }
2305  }
2306
2307
2308
2309  /**
2310   * Indicates that processing for an operation completed successfully.
2311   *
2312   * @param  changeRecord              The LDIF change record for the operation.
2313   *                                   It must not be {@code null}.
2314   * @param  processingDurationMillis  The length of time required to process
2315   *                                   the operation, in milliseconds.
2316   */
2317  void opCompletedSuccessfully(@NotNull final LDIFChangeRecord changeRecord,
2318                               final long processingDurationMillis)
2319  {
2320    opsAttempted.incrementAndGet();
2321    opsSucceeded.incrementAndGet();
2322    totalOpDurationMillis.addAndGet(processingDurationMillis);
2323    logMessage(changeRecord.getDN(), " ",
2324         changeRecord.getChangeType().getName(), " SUCCESS 0 ");
2325  }
2326
2327
2328
2329  /**
2330   * Indicates that processing for an operation failed.  Depending on the nature
2331   * of the failure, it either be added to the retry queue if it is potentially
2332   * an operation that could be retried (e.g., an operation that failed because
2333   * it depended on an entry that was added later in the LDIF), or will be added
2334   * to the reject file if it is determined that it is not a failure that may be
2335   * resolved by a later change.
2336   *
2337   * @param  changeRecord              The LDIF change record for the operation.
2338   *                                   It must not be {@code null}.
2339   * @param  ldapException             The LDAP exception that was caught to
2340   *                                   indicate that the operation failed.  It
2341   *                                   must not be {@code null}.
2342   * @param  processingDurationMillis  The length of time required to process
2343   *                                   the operation, in milliseconds.
2344   */
2345  void opFailed(@NotNull final LDIFChangeRecord changeRecord,
2346                @NotNull final LDAPException ldapException,
2347                final long processingDurationMillis)
2348  {
2349    opsAttempted.incrementAndGet();
2350    totalOpDurationMillis.addAndGet(processingDurationMillis);
2351
2352    switch (ldapException.getResultCode().intValue())
2353    {
2354      case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
2355      case ResultCode.BUSY_INT_VALUE:
2356      case ResultCode.UNAVAILABLE_INT_VALUE:
2357      case ResultCode.NOT_ALLOWED_ON_NONLEAF_INT_VALUE:
2358      case ResultCode.SERVER_DOWN_INT_VALUE:
2359      case ResultCode.CONNECT_ERROR_INT_VALUE:
2360        retry(changeRecord, ldapException);
2361        break;
2362      default:
2363        reject(changeRecord, ldapException);
2364        break;
2365    }
2366  }
2367
2368
2369
2370  /**
2371   * Adds the provided operation to the retry queue.
2372   *
2373   * @param  changeRecord   The LDIF change record for the operation.
2374   * @param  ldapException  The LDAP exception that was caught to indicate that
2375   *                        the operation failed.
2376   */
2377  private void retry(@NotNull final LDIFChangeRecord changeRecord,
2378                     @NotNull final LDAPException ldapException)
2379  {
2380    if (neverRetryArg.isPresent())
2381    {
2382      reject(changeRecord, ldapException);
2383      return;
2384    }
2385
2386    final DN parsedDN;
2387    try
2388    {
2389      parsedDN = changeRecord.getParsedDN();
2390    }
2391    catch (final LDAPException e)
2392    {
2393      Debug.debugException(e);
2394
2395      // This should never happen, but if it does, then reject the change.
2396      reject(changeRecord, ldapException);
2397      return;
2398    }
2399
2400    logMessage(changeRecord.getDN(), " ",
2401         changeRecord.getChangeType().getName(), " RETRY ",
2402         ldapException.getResultCode(), " ", ldapException.getMessage());
2403
2404    synchronized (retryQueue)
2405    {
2406      List<ObjectPair<LDIFChangeRecord,LDAPException>> changeList =
2407           retryQueue.get(parsedDN);
2408
2409      if (changeList == null)
2410      {
2411        changeList = new LinkedList<>();
2412        retryQueue.put(parsedDN, changeList);
2413      }
2414
2415      changeList.add(new ObjectPair<>(changeRecord, ldapException));
2416      retryQueueSize.incrementAndGet();
2417    }
2418  }
2419
2420
2421
2422  /**
2423   * Adds the provided operation to the reject file.
2424   *
2425   * @param  changeRecord   The LDIF change record for the operation.
2426   * @param  ldapException  The LDAP exception that was caught to indicate that
2427   *                        the operation failed.
2428   */
2429  void reject(@Nullable final LDIFChangeRecord changeRecord,
2430              @NotNull final LDAPException ldapException)
2431  {
2432    opsRejected.incrementAndGet();
2433
2434    final ResultCode resultCode = ldapException.getResultCode();
2435    if (resultCode != ResultCode.SUCCESS)
2436    {
2437      firstRejectResultCode.compareAndSet(null, resultCode);
2438    }
2439
2440    final StringBuilder commentBuffer = new StringBuilder();
2441    for (final String line :
2442         ResultUtils.formatResult(ldapException, false, 0, 0))
2443    {
2444      if (commentBuffer.length() > 0)
2445      {
2446        commentBuffer.append(StaticUtils.EOL);
2447      }
2448      commentBuffer.append(line);
2449    }
2450    final String comment = commentBuffer.toString();
2451
2452    try
2453    {
2454      if (changeRecord != null)
2455      {
2456        logMessage(changeRecord.getDN(), " ",
2457            changeRecord.getChangeType().getName(), " REJECT ", resultCode, " ",
2458             ldapException.getMessage());
2459
2460        synchronized (rejectWriter)
2461        {
2462          rejectWriter.writeChangeRecord(changeRecord, comment);
2463          rejectWriter.flush();
2464        }
2465      }
2466      else
2467      {
2468        synchronized (rejectWriter)
2469        {
2470          rejectWriter.writeComment(comment, true, true);
2471          rejectWriter.flush();
2472        }
2473      }
2474    }
2475    catch (final Exception e)
2476    {
2477      Debug.debugException(e);
2478
2479      final String errorMessage = ERR_PARALLEL_UPDATE_CANNOT_WRITE_REJECT.get(
2480           changeRecord.toString(),
2481           ERR_PARALLEL_UPDATE_REJECT_COMMENT.get(String.valueOf(resultCode),
2482                ldapException.getMessage()),
2483           StaticUtils.getExceptionMessage(e));
2484        wrapErr(0, WRAP_COLUMN, errorMessage);
2485        completionMessage.compareAndSet(null, errorMessage);
2486        shouldAbort.set(true);
2487    }
2488  }
2489
2490
2491
2492  /**
2493   * Prints information about the processing performed by this program to
2494   * standard output.
2495   */
2496  void printIntervalData()
2497  {
2498    final long currentAttempts = opsAttempted.get();
2499    final long currentSuccesses = opsSucceeded.get();
2500    final long currentReject = opsRejected.get();
2501    final long currentRetry = retryQueueSize.get();
2502    final long currentDurationMillis = totalOpDurationMillis.get();
2503    final long currentTimeMillis = System.currentTimeMillis();
2504    final long totalDurationMillis =
2505         currentTimeMillis - processingStartTimeMillis;
2506
2507    final long avgRate;
2508    if (totalDurationMillis == 0L)
2509    {
2510      avgRate = 0L;
2511    }
2512    else
2513    {
2514      avgRate = 1000L * currentAttempts / totalDurationMillis;
2515    }
2516
2517    final long avgDurationMillis;
2518    if (currentAttempts == 0L)
2519    {
2520      avgDurationMillis = 0L;
2521    }
2522    else
2523    {
2524      avgDurationMillis = currentDurationMillis / currentAttempts;
2525    }
2526
2527    final long recentDurationMillis;
2528    if (currentAttempts == lastOpsAttempted)
2529    {
2530      recentDurationMillis = 0L;
2531    }
2532    else
2533    {
2534      recentDurationMillis = (currentDurationMillis - lastTotalDurationMillis) /
2535           (currentAttempts - lastOpsAttempted);
2536    }
2537
2538    final long recentRate;
2539    if (lastOpsAttempted == 0)
2540    {
2541      out(" Attempts Successes   Rejects   ToRetry  AvgOps/S " +
2542          " RctOps/S  AvgDurMS  RctDurMS");
2543      out("--------- --------- --------- --------- --------- " +
2544          "--------- --------- ---------");
2545      recentRate = avgRate;
2546    }
2547    else if (currentTimeMillis == lastUpdateTimeMillis)
2548    {
2549      recentRate = 0L;
2550    }
2551    else
2552    {
2553      recentRate = 1000L * (currentAttempts - lastOpsAttempted) /
2554                   (currentTimeMillis - lastUpdateTimeMillis);
2555    }
2556
2557    final StringBuilder buffer = new StringBuilder(80);
2558    appendJustified(currentAttempts, buffer, true);
2559    appendJustified(currentSuccesses, buffer, true);
2560    appendJustified(currentReject, buffer, true);
2561    appendJustified(currentRetry, buffer, true);
2562    appendJustified(avgRate, buffer, true);
2563    appendJustified(recentRate, buffer, true);
2564    appendJustified(avgDurationMillis, buffer, true);
2565    appendJustified(recentDurationMillis, buffer, false);
2566
2567    out(buffer.toString());
2568
2569    lastOpsAttempted = currentAttempts;
2570    lastTotalDurationMillis = currentDurationMillis;
2571    lastUpdateTimeMillis = currentTimeMillis;
2572  }
2573
2574
2575
2576  /**
2577   * Appends the provided number to the buffer, right justified in nine columns.
2578   *
2579   * @param  value     The value to be appended to the buffer.
2580   * @param  buffer    The buffer to which the value should be appended.
2581   * @param  addSpace  Indicates whether to append a space after the number.
2582   */
2583  static void appendJustified(final long value,
2584                              @NotNull final StringBuilder buffer,
2585                              final boolean addSpace)
2586  {
2587    final String valueStr = String.valueOf(value);
2588    switch (valueStr.length())
2589    {
2590      case 1:
2591        buffer.append("        ");
2592        break;
2593      case 2:
2594        buffer.append("       ");
2595        break;
2596      case 3:
2597        buffer.append("      ");
2598        break;
2599      case 4:
2600        buffer.append("     ");
2601        break;
2602      case 5:
2603        buffer.append("    ");
2604        break;
2605      case 6:
2606        buffer.append("   ");
2607        break;
2608      case 7:
2609        buffer.append("  ");
2610        break;
2611      case 8:
2612        buffer.append(' ');
2613        break;
2614    }
2615
2616    buffer.append(value);
2617
2618    if (addSpace)
2619    {
2620      buffer.append(' ');
2621    }
2622  }
2623
2624
2625
2626  /**
2627   * Retrieves the total number of operations attempted.  This should only be
2628   * called after all tool processing has completed.
2629   *
2630   * @return  The total number of operations attempted.
2631   */
2632  public long getTotalAttemptCount()
2633  {
2634    return opsAttempted.get();
2635  }
2636
2637
2638
2639  /**
2640   * Retrieves the number of operations attempted on the initial pass through
2641   * the LDIF file (that is, operations for which no retry attempts was made).
2642   * This should only be called after all tool processing has completed.
2643   *
2644   * @return  The number of operations attempted on the initial pass through the
2645   *          LDIF file.
2646   */
2647  public long getInitialAttemptCount()
2648  {
2649    return initialAttempted;
2650  }
2651
2652
2653
2654  /**
2655   * Retrieves the number of retry attempts made for operations that did not
2656   * complete successfully on their first attempt.  This should only be called
2657   * after all tool processing has completed.
2658   *
2659   * @return  The number of retry attempts made for operations that did not
2660   *          complete successfully on their first attempt.
2661   */
2662  public long getRetryAttemptCount()
2663  {
2664    return opsAttempted.get() - initialAttempted;
2665  }
2666
2667
2668
2669  /**
2670   * Retrieves the total number of operations that completed successfully.  This
2671   * should only be called after all tool processing has completed.
2672   *
2673   * @return  The total number of operations that completed successfully.
2674   */
2675  public long getTotalSuccessCount()
2676  {
2677    return opsSucceeded.get();
2678  }
2679
2680
2681
2682  /**
2683   * Retrieves the number of operations that completed successfully on their
2684   * first attempt.  This should only be called after all tool processing has
2685   * completed.
2686   *
2687   * @return  The total number of operations that completed successfully on
2688   *          their first attempt.
2689   */
2690  public long getInitialSuccessCount()
2691  {
2692    return initialSucceeded;
2693  }
2694
2695
2696
2697  /**
2698   * Retrieves the number of operations that did not complete completed
2699   * successfully on their initial attempt but did succeed on a retry attempt.
2700   * This should only be called after all tool processing has completed.
2701   *
2702   * @return  The number of operations that completed successfully on a retry
2703   *          attempt.
2704   */
2705  public long getRetrySuccessCount()
2706  {
2707    return opsSucceeded.get() - initialSucceeded;
2708  }
2709
2710
2711
2712  /**
2713   * Retrieves the number of operations that were rejected and did not complete
2714   * successfully during any of the attempts.  This should only be called after
2715   * all tool processing has completed.
2716   *
2717   * @return  The number of operations that were rejected.
2718   */
2719  public long getRejectCount()
2720  {
2721    return opsRejected.get();
2722  }
2723
2724
2725
2726  /**
2727   * Retrieves the total length of time, in milliseconds, spent processing
2728   * operations.  This should only be called after all tool processing has
2729   * completed.  Note that when running with multiple threads, this can exceed
2730   * the length of time spent running the tool because multiple operations can
2731   * be processed in parallel.
2732   *
2733   * @return  The total length of time, in milliseconds, spent processing
2734   *          operations.
2735   */
2736  public long getTotalOpDurationMillis()
2737  {
2738    return totalOpDurationMillis.get();
2739  }
2740
2741
2742
2743  /**
2744   * {@inheritDoc}
2745   */
2746  @Override()
2747  protected boolean registerShutdownHook()
2748  {
2749    return true;
2750  }
2751
2752
2753
2754  /**
2755   * {@inheritDoc}
2756   */
2757  @Override()
2758  protected void doShutdownHookProcessing(@Nullable final ResultCode resultCode)
2759  {
2760    shouldAbort.set(true);
2761
2762    final FixedRateBarrier b = rateLimiter;
2763    if (b != null)
2764    {
2765      b.shutdownRequested();
2766    }
2767  }
2768
2769
2770
2771  /**
2772   * {@inheritDoc}
2773   */
2774  @Override()
2775  public void handleUnsolicitedNotification(
2776                   @NotNull final LDAPConnection connection,
2777                   @NotNull final ExtendedResult notification)
2778  {
2779    final String message;
2780    if (notification.getDiagnosticMessage() == null)
2781    {
2782      message = INFO_PARALLEL_UPDATE_UNSOLICITED_NOTIFICATION_NO_MESSAGE.get(
2783           getToolName(), String.valueOf(notification.getResultCode()),
2784           notification.getOID());
2785    }
2786    else
2787    {
2788      message = INFO_PARALLEL_UPDATE_UNSOLICITED_NOTIFICATION_NO_MESSAGE.get(
2789           getToolName(), String.valueOf(notification.getResultCode()),
2790           notification.getOID(), notification.getDiagnosticMessage());
2791    }
2792
2793    out();
2794    wrapOut(0, WRAP_COLUMN, message);
2795    out();
2796  }
2797
2798
2799
2800  /**
2801   * {@inheritDoc}
2802   */
2803  @Override()
2804  @NotNull()
2805  public LinkedHashMap<String[],String> getExampleUsages()
2806  {
2807    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
2808
2809    examples.put(
2810         new String[]
2811         {
2812           "--hostname", "server.example.com",
2813           "--port", "636",
2814           "--useSSL",
2815           "--bindDN", "uid=admin,dc=example,dc=com",
2816           "--promptForBindPassword",
2817           "--ldifFile", "changes.ldif",
2818           "--rejectFile", "rejects.ldif",
2819           "--defaultAdd",
2820           "--numThreads", "10",
2821           "--ratePerSecond", "5000"
2822         },
2823         INFO_PARALLEL_UPDATE_EXAMPLE_DESC.get());
2824
2825    return examples;
2826  }
2827}