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 supportsAuthentication()
1017  {
1018    return true;
1019  }
1020
1021
1022
1023  /**
1024   * {@inheritDoc}
1025   */
1026  @Override()
1027  protected boolean defaultToPromptForBindPassword()
1028  {
1029    return true;
1030  }
1031
1032
1033
1034  /**
1035   * {@inheritDoc}
1036   */
1037  @Override()
1038  protected boolean supportsSASLHelp()
1039  {
1040    return true;
1041  }
1042
1043
1044
1045  /**
1046   * {@inheritDoc}
1047   */
1048  @Override()
1049  protected boolean includeAlternateLongIdentifiers()
1050  {
1051    return true;
1052  }
1053
1054
1055
1056  /**
1057   * {@inheritDoc}
1058   */
1059  @Override()
1060  @NotNull()
1061  protected List<Control> getBindControls()
1062  {
1063    final List<Control> bindControls = new ArrayList<>();
1064
1065    if ((bindControlArg != null) && bindControlArg.isPresent())
1066    {
1067      bindControls.addAll(bindControlArg.getValues());
1068    }
1069
1070    if ((suppressOperationalAttributeUpdatesArg != null) &&
1071         suppressOperationalAttributeUpdatesArg.isPresent())
1072    {
1073      final EnumSet<SuppressType> suppressTypes =
1074           EnumSet.noneOf(SuppressType.class);
1075      for (final String s : suppressOperationalAttributeUpdatesArg.getValues())
1076      {
1077        if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_ACCESS_TIME))
1078        {
1079          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
1080        }
1081        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_TIME))
1082        {
1083          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
1084        }
1085        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_IP))
1086        {
1087          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
1088        }
1089      }
1090
1091      bindControls.add(new SuppressOperationalAttributeUpdateRequestControl(
1092           true, suppressTypes));
1093    }
1094
1095    return Collections.emptyList();
1096  }
1097
1098
1099
1100  /**
1101   * {@inheritDoc}
1102   */
1103  @Override()
1104  protected boolean supportsMultipleServers()
1105  {
1106    return true;
1107  }
1108
1109
1110
1111  /**
1112   * {@inheritDoc}
1113   */
1114  @Override()
1115  protected boolean supportsSSLDebugging()
1116  {
1117    return true;
1118  }
1119
1120
1121
1122  /**
1123   * {@inheritDoc}
1124   */
1125  @Override()
1126  @NotNull()
1127  public LDAPConnectionOptions getConnectionOptions()
1128  {
1129    final LDAPConnectionOptions options = new LDAPConnectionOptions();
1130    options.setUseSynchronousMode(true);
1131    options.setFollowReferrals(
1132         ((followReferralsArg != null) && followReferralsArg.isPresent()));
1133    options.setUnsolicitedNotificationHandler(this);
1134    options.setResponseTimeoutMillis(0L);
1135    return options;
1136  }
1137
1138
1139
1140  /**
1141   * {@inheritDoc}
1142   */
1143  @Override()
1144  protected boolean logToolInvocationByDefault()
1145  {
1146    return true;
1147  }
1148
1149
1150
1151  /**
1152   * {@inheritDoc}
1153   */
1154  @Override()
1155  @Nullable()
1156  public String getToolCompletionMessage()
1157  {
1158    return completionMessage.get();
1159  }
1160
1161
1162
1163  /**
1164   * {@inheritDoc}
1165   */
1166  @Override()
1167  @NotNull()
1168  public ResultCode doToolProcessing()
1169  {
1170    // Create the sets of controls to include in each type of request.
1171    final Control[] addControls;
1172    final Control[] deleteControls;
1173    final Control[] modifyControls;
1174    final Control[] modifyDNControls;
1175    try
1176    {
1177      final List<Control> addControlList = new ArrayList<>();
1178      final List<Control> deleteControlList = new ArrayList<>();
1179      final List<Control> modifyControlList = new ArrayList<>();
1180      final List<Control> modifyDNControlList = new ArrayList<>();
1181
1182      getOperationControls(addControlList, deleteControlList,
1183           modifyControlList, modifyDNControlList);
1184
1185      addControls = StaticUtils.toArray(addControlList, Control.class);
1186      deleteControls = StaticUtils.toArray(deleteControlList, Control.class);
1187      modifyControls = StaticUtils.toArray(modifyControlList, Control.class);
1188      modifyDNControls =
1189           StaticUtils.toArray(modifyDNControlList, Control.class);
1190    }
1191    catch (final LDAPException e)
1192    {
1193      Debug.debugException(e);
1194      logCompletionMessage(true, e.getMessage());
1195      return e.getResultCode();
1196    }
1197
1198
1199    // Get the connection pool to use to communicate with the directory
1200    // server(s).
1201    final LDAPConnectionPool connectionPool;
1202    final int numThreads = numThreadsArg.getValue();
1203    try
1204    {
1205      connectionPool = getConnectionPool(numThreads, numThreads, 1, null, null,
1206           true, null);
1207      connectionPool.setConnectionPoolName("parallel-update");
1208      connectionPool.setRetryFailedOperationsDueToInvalidConnections(
1209           (! neverRetryArg.isPresent()));
1210    }
1211    catch (final LDAPException e)
1212    {
1213      Debug.debugException(e);
1214      logCompletionMessage(true,
1215           ERR_PARALLEL_UPDATE_CANNOT_CREATE_POOL.get(
1216                StaticUtils.getExceptionMessage(e)));
1217      return e.getResultCode();
1218    }
1219
1220
1221    // Create the LDIF reader that will read the changes to process.
1222    final LDIFReader ldifReader;
1223    final String encryptionPassphrase;
1224    try
1225    {
1226      final ObjectPair<LDIFReader,String> ldifReaderPair = createLDIFReader();
1227      ldifReader = ldifReaderPair.getFirst();
1228      encryptionPassphrase = ldifReaderPair.getSecond();
1229    }
1230    catch (final LDAPException e)
1231    {
1232      Debug.debugException(e);
1233      logCompletionMessage(true, e.getMessage());
1234      connectionPool.close();
1235      return e.getResultCode();
1236    }
1237
1238    final AtomicReference<ResultCode> resultCodeRef =
1239         new AtomicReference<>(ResultCode.SUCCESS);
1240    try
1241    {
1242      // If the LDIF file is encrypted, then get the ID of the encryption
1243      // settings definition (if any) used to generate the encryption key.
1244      final String encryptionSettingsDefinitionID;
1245      if (encryptionPassphrase == null)
1246      {
1247        encryptionSettingsDefinitionID = null;
1248      }
1249      else
1250      {
1251        encryptionSettingsDefinitionID = getEncryptionSettingsDefinitionID();
1252      }
1253
1254
1255      // Create the LDIF writer that will be used to write rejects.
1256      try
1257      {
1258        rejectWriter = createRejectWriter(encryptionPassphrase,
1259             encryptionSettingsDefinitionID);
1260      }
1261      catch (final LDAPException e)
1262      {
1263        Debug.debugException(e);
1264        logCompletionMessage(true, e.getMessage());
1265        return ResultCode.LOCAL_ERROR;
1266      }
1267
1268
1269      // If appropriate, create the log writer that will be used to provide a
1270      // log of the changes that are attempted.
1271      if (logFileArg.isPresent())
1272      {
1273        try
1274        {
1275          logWriter = new PrintWriter(logFileArg.getValue());
1276        }
1277        catch (final Exception e)
1278        {
1279          Debug.debugException(e);
1280          logCompletionMessage(true,
1281               ERR_PARALLEL_UPDATE_ERROR_CREATING_LOG_WRITER.get(
1282                    logFileArg.getValue().getAbsolutePath(),
1283                    StaticUtils.getExceptionMessage(e)));
1284          return ResultCode.LOCAL_ERROR;
1285        }
1286      }
1287
1288
1289      // Create The queue that will hold the operations to process.
1290      final ParallelUpdateOperationQueue operationQueue =
1291           new ParallelUpdateOperationQueue(this, numThreads,
1292                (2 * numThreads));
1293
1294
1295      // Create the rate limiter, if appropriate.
1296      if (ratePerSecondArg.isPresent())
1297      {
1298        rateLimiter = new FixedRateBarrier(1000L, ratePerSecondArg.getValue());
1299      }
1300      else
1301      {
1302        rateLimiter = null;
1303      }
1304
1305
1306      // Create and start all of the threads that will be used to process
1307      // requests.
1308      final List<ParallelUpdateOperationThread> operationThreadList =
1309           new ArrayList<>(numThreads);
1310      for (int i=1; i <= numThreads; i++)
1311      {
1312        final ParallelUpdateOperationThread operationThread =
1313             new ParallelUpdateOperationThread(this, connectionPool,
1314                  operationQueue, i, rateLimiter, addControls, deleteControls,
1315                  modifyControls, modifyDNControls,
1316                  allowUndeleteArg.isPresent());
1317        operationThreadList.add(operationThread);
1318        operationThread.start();
1319      }
1320
1321
1322      // Create a progress monitor that will be used to report periodic status
1323      // updates about the processing that has been performed.
1324      final ParallelUpdateProgressMonitor progressMonitor =
1325           new ParallelUpdateProgressMonitor(this);
1326      try
1327      {
1328        processingStartTimeMillis = System.currentTimeMillis();
1329        progressMonitor.start();
1330
1331        while (! shouldAbort.get())
1332        {
1333          final LDIFChangeRecord changeRecord;
1334          try
1335          {
1336            changeRecord = ldifReader.readChangeRecord(
1337                 defaultAddArg.isPresent());
1338          }
1339          catch (final LDIFException e)
1340          {
1341            Debug.debugException(e);
1342            if (e.mayContinueReading())
1343            {
1344              final String message =
1345                   ERR_PARALLEL_UPDATE_RECOVERABLE_LDIF_EXCEPTION.get(
1346                        ldifFileArg.getValue().getAbsolutePath(),
1347                        e.getMessage());
1348              logMessage(message);
1349              reject(null,
1350                   new LDAPException(ResultCode.DECODING_ERROR, message, e));
1351              opsAttempted.incrementAndGet();
1352              continue;
1353            }
1354            else
1355            {
1356              shouldAbort.set(true);
1357              final String message =
1358                   ERR_PARALLEL_UPDATE_UNRECOVERABLE_LDIF_EXCEPTION.get(
1359                        ldifFileArg.getValue().getAbsolutePath(),
1360                        StaticUtils.getExceptionMessage(e));
1361              reject(null,
1362                   new LDAPException(ResultCode.DECODING_ERROR, message, e));
1363              logCompletionMessage(true, message);
1364              return ResultCode.DECODING_ERROR;
1365            }
1366          }
1367          catch (final Exception e)
1368          {
1369            Debug.debugException(e);
1370            shouldAbort.set(true);
1371            final String message =
1372                 ERR_PARALLEL_UPDATE_ERROR_READING_LDIF_FILE.get(
1373                      ldifFileArg.getValue().getAbsolutePath(),
1374                      StaticUtils.getExceptionMessage(e));
1375            reject(null,
1376                 new LDAPException(ResultCode.LOCAL_ERROR, message, e));
1377            logCompletionMessage(true, message);
1378            return ResultCode.LOCAL_ERROR;
1379          }
1380
1381          if (changeRecord == null)
1382          {
1383            // We've reached the end of the LDIF file.
1384            break;
1385          }
1386          else
1387          {
1388            try
1389            {
1390              operationQueue.addChangeRecord(changeRecord);
1391            }
1392            catch (final Exception e)
1393            {
1394              Debug.debugException(e);
1395
1396              // This indicates that the attempt to enqueue the change record
1397              // was interrupted.  This shouldn't happen, but if it does, then
1398              // mark it to be retried.
1399              final LDAPException le = new LDAPException(ResultCode.LOCAL_ERROR,
1400                   ERR_PARALLEL_UPDATE_ENQUEUE_FAILED.get(
1401                        StaticUtils.getExceptionMessage(e)),
1402                   e);
1403              retry(changeRecord, le);
1404            }
1405          }
1406        }
1407
1408
1409        // If a failure was encountered, then abort.
1410        if (shouldAbort.get())
1411        {
1412          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1413               ResultCode.LOCAL_ERROR);
1414          return resultCodeRef.get();
1415        }
1416
1417
1418        // Indicate that we've reached the end of the LDIF file.
1419        out();
1420        wrapOut(0, WRAP_COLUMN,
1421             INFO_PARALLEL_UPDATE_END_OF_LDIF.get());
1422        out();
1423
1424
1425        // Wait for the operation queue to become idle so that we know there
1426        // are no more outstanding operations to be processed.
1427        operationQueue.waitUntilIdle();
1428        initialAttempted = opsAttempted.get();
1429        initialSucceeded = opsSucceeded.get();
1430
1431
1432        // If there are any operations to retry, then do so now.
1433        Map<DN,List<ObjectPair<LDIFChangeRecord,LDAPException>>> retryQueueCopy;
1434        synchronized (retryQueue)
1435        {
1436          retryQueueCopy = new TreeMap<>(retryQueue);
1437          retryQueue.clear();
1438        }
1439
1440        int lastRetryQueueSize = 0;
1441        while ((! retryQueueCopy.isEmpty()) &&
1442             (retryQueueCopy.size() != lastRetryQueueSize))
1443        {
1444          out();
1445          wrapOut(0, WRAP_COLUMN,
1446               INFO_PARALLEL_UPDATE_BEGINNING_RETRY.get(retryQueueCopy.size()));
1447          out();
1448
1449
1450          for (final
1451               Map.Entry<DN,List<ObjectPair<LDIFChangeRecord,LDAPException>>>
1452               e : retryQueueCopy.entrySet())
1453          {
1454            for (final ObjectPair<LDIFChangeRecord,LDAPException> p :
1455              e.getValue())
1456            {
1457              if (shouldAbort.get())
1458              {
1459                resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1460                     ResultCode.LOCAL_ERROR);
1461                return resultCodeRef.get();
1462              }
1463
1464              final LDIFChangeRecord changeRecord = p.getFirst();
1465              try
1466              {
1467                operationQueue.addChangeRecord(changeRecord);
1468                retryQueueSize.decrementAndGet();
1469              }
1470              catch (final Exception ex)
1471              {
1472                Debug.debugException(ex);
1473
1474                // This indicates that the attempt to enqueue the change record
1475                // was interrupted.  This shouldn't happen, but if it does, then
1476                // mark it to be retried.
1477                final LDAPException le = new LDAPException(
1478                     ResultCode.LOCAL_ERROR,
1479                     ERR_PARALLEL_UPDATE_ENQUEUE_FAILED.get(
1480                          StaticUtils.getExceptionMessage(ex)),
1481                     ex);
1482                retry(changeRecord, le);
1483              }
1484            }
1485          }
1486
1487
1488          operationQueue.waitUntilIdle();
1489          lastRetryQueueSize = retryQueueCopy.size();
1490
1491          synchronized (retryQueue)
1492          {
1493            retryQueueCopy = new TreeMap<>(retryQueue);
1494            retryQueue.clear();
1495          }
1496        }
1497
1498
1499        // If we've gotten here, then it means that either the retry queue
1500        // (NOTE:  we actually need to use retryQueueCopy) is empty or none of
1501        // the retry attempts succeeded on the last pass.  If it's the latter,
1502        // then reject any of the remaining operations.
1503        synchronized (retryQueue)
1504        {
1505          final int remainingToRetry = retryQueueCopy.size();
1506          if (remainingToRetry > 0)
1507          {
1508            if (remainingToRetry == 1)
1509            {
1510              wrapErr(0, WRAP_COLUMN,
1511                   ERR_PARALLEL_UPDATE_NO_PROGRESS_ONE.get());
1512            }
1513            else
1514            {
1515              wrapErr(0, WRAP_COLUMN,
1516                   ERR_PARALLEL_UPDATE_NO_PROGRESS_MULTIPLE.get(
1517                        remainingToRetry));
1518            }
1519          }
1520
1521          for (final
1522               Map.Entry<DN,List<ObjectPair<LDIFChangeRecord,LDAPException>>>
1523               e : retryQueueCopy.entrySet())
1524          {
1525            for (final ObjectPair<LDIFChangeRecord,LDAPException> p :
1526                 e.getValue())
1527            {
1528              reject(p.getFirst(), p.getSecond());
1529              retryQueueSize.decrementAndGet();
1530            }
1531          }
1532        }
1533      }
1534      finally
1535      {
1536        operationQueue.setEndOfLDIF();
1537        operationQueue.waitUntilIdle();
1538
1539        for (final ParallelUpdateOperationThread operationThread :
1540             operationThreadList)
1541        {
1542          try
1543          {
1544            operationThread.join();
1545          }
1546          catch (final Exception e)
1547          {
1548            Debug.debugException(e);
1549            logCompletionMessage(true,
1550                 ERR_PARALLEL_UPDATE_CANNOT_JOIN_THREAD.get(
1551                      operationThread.getName(),
1552                      StaticUtils.getExceptionMessage(e)));
1553            resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1554                 ResultCode.LOCAL_ERROR);
1555          }
1556        }
1557
1558        try
1559        {
1560          progressMonitor.stopRunning();
1561          progressMonitor.join();
1562        }
1563        catch (final Exception e)
1564        {
1565          Debug.debugException(e);
1566          logCompletionMessage(true,
1567               ERR_PARALLEL_UPDATE_CANNOT_JOIN_PROGRESS_MONITOR.get(
1568                    StaticUtils.getExceptionMessage(e)));
1569          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1570               ResultCode.LOCAL_ERROR);
1571        }
1572      }
1573    }
1574    finally
1575    {
1576      connectionPool.close();
1577
1578      if (rejectWriter != null)
1579      {
1580        try
1581        {
1582          rejectWriter.close();
1583        }
1584        catch (final Exception e)
1585        {
1586          Debug.debugException(e);
1587          logCompletionMessage(true,
1588               ERR_PARALLEL_UPDATE_ERROR_CLOSING_REJECT_WRITER.get(
1589                    rejectFileArg.getValue().getAbsolutePath(),
1590                    StaticUtils.getExceptionMessage(e)));
1591          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1592               ResultCode.LOCAL_ERROR);
1593        }
1594      }
1595
1596      if (logWriter != null)
1597      {
1598        try
1599        {
1600          logWriter.close();
1601        }
1602        catch (final Exception e)
1603        {
1604          Debug.debugException(e);
1605          logCompletionMessage(true,
1606               ERR_PARALLEL_UPDATE_ERROR_CLOSING_LOG_WRITER.get(
1607                    logFileArg.getValue().getAbsolutePath(),
1608                    StaticUtils.getExceptionMessage(e)));
1609          resultCodeRef.compareAndSet(ResultCode.SUCCESS,
1610               ResultCode.LOCAL_ERROR);
1611        }
1612      }
1613
1614      try
1615      {
1616        ldifReader.close();
1617      }
1618      catch (final Exception e)
1619      {
1620        Debug.debugException(e);
1621        logCompletionMessage(true,
1622             WARN_PARALLEL_UPDATE_ERROR_CLOSING_READER.get(
1623                  ldifFileArg.getValue().getAbsolutePath(),
1624                  StaticUtils.getExceptionMessage(e)));
1625        resultCodeRef.compareAndSet(ResultCode.SUCCESS, ResultCode.LOCAL_ERROR);
1626      }
1627    }
1628
1629
1630    // If we've gotten here, then processing has completed.  Print some summary
1631    // messages and return an appropriate result code.
1632    final long processingDurationMillis =
1633         System.currentTimeMillis() - processingStartTimeMillis;
1634    final long numAttempts = opsAttempted.get();
1635    final long numSuccesses = opsSucceeded.get();
1636    final long numRejects = opsRejected.get();
1637
1638    final long retryAttempts = numAttempts - initialAttempted;
1639    final long retrySuccesses = numSuccesses - initialSucceeded;
1640
1641    out(INFO_PARALLEL_UPDATE_DONE.get(getToolName()));
1642    out(INFO_PARALLEL_UPDATE_SUMMARY_OPS_ATTEMPTED.get(numAttempts));
1643
1644    if (retryAttempts > 0L)
1645    {
1646      out(INFO_PARALLEL_UPDATE_SUMMARY_INITIAL_ATTEMPTS.get(initialAttempted));
1647      out(INFO_PARALLEL_UPDATE_SUMMARY_RETRY_ATTEMPTS.get(retryAttempts));
1648    }
1649
1650    out(INFO_PARALLEL_UPDATE_SUMMARY_OPS_SUCCEEDED.get(numSuccesses));
1651
1652    if (retryAttempts > 0)
1653    {
1654      out(INFO_PARALLEL_UPDATED_OPS_SUCCEEDED_INITIAL.get(initialSucceeded));
1655      out(INFO_PARALLEL_UPDATED_OPS_SUCCEEDED_RETRY.get(retrySuccesses));
1656    }
1657
1658    out(INFO_PARALLEL_UPDATE_SUMMARY_OPS_REJECTED.get(numRejects));
1659    out(INFO_PARALLEL_UPDATE_SUMMARY_DURATION.get(
1660         StaticUtils.millisToHumanReadableDuration(processingDurationMillis)));
1661
1662    if ((numAttempts > 0L) && (processingDurationMillis > 0L))
1663    {
1664      final double attemptsPerSecond =
1665           numAttempts * 1_000.0d / processingDurationMillis;
1666      final DecimalFormat decimalFormat = new DecimalFormat("0.000");
1667      out(INFO_PARALLEL_UPDATE_SUMMARY_RATE.get(
1668           decimalFormat.format(attemptsPerSecond)));
1669    }
1670
1671
1672    if (numRejects == 0L)
1673    {
1674      completionMessage.compareAndSet(null,
1675           INFO_PARALLEL_UPDATE_COMPLETION_MESSAGE_ALL_SUCCEEDED.get(
1676                getToolName()));
1677    }
1678    else if (numRejects == 1L)
1679    {
1680      completionMessage.compareAndSet(null,
1681           INFO_PARALLEL_UPDATE_COMPLETION_MESSAGE_ONE_REJECTED.get(
1682                getToolName()));
1683    }
1684    else
1685    {
1686      completionMessage.compareAndSet(null,
1687           INFO_PARALLEL_UPDATE_COMPLETION_MESSAGE_MULTIPLE_REJECTED.get(
1688                getToolName(), numRejects));
1689    }
1690
1691
1692    ResultCode finalResultCode = resultCodeRef.get();
1693    if ((finalResultCode == ResultCode.SUCCESS) &&
1694         useFirstRejectResultCodeAsExitCodeArg.isPresent() &&
1695         (firstRejectResultCode.get() != null))
1696    {
1697      finalResultCode = firstRejectResultCode.get();
1698    }
1699
1700    return finalResultCode;
1701  }
1702
1703
1704
1705  /**
1706   * Updates the provided lists with the appropriate controls to include in
1707   * each type of request.
1708   *
1709   * @param  addControls       The list that should be updated with controls to
1710   *                           include in add requests.  It must not be
1711   *                           {@code null} and must be updatable.
1712   * @param  deleteControls    The list that should be updated with controls to
1713   *                           include in delete requests.  It must not be
1714   *                           {@code null} and must be updatable.
1715   * @param  modifyControls    The list that should be updated with controls to
1716   *                           include in modify requests.  It must not be
1717   *                           {@code null} and must be updatable.
1718   * @param  modifyDNControls  The list that should be updated with controls to
1719   *                           include in modify DN requests.  It must not be
1720   *                           {@code null} and must be updatable.
1721   *
1722   * @throws  LDAPException  If a problem is encountered while creating any of
1723   *                         the controls.
1724   */
1725  private void getOperationControls(
1726                    @NotNull final List<Control> addControls,
1727                    @NotNull final List<Control> deleteControls,
1728                    @NotNull final List<Control> modifyControls,
1729                    @NotNull final List<Control> modifyDNControls)
1730          throws LDAPException
1731  {
1732    if (addControlArg.isPresent())
1733    {
1734      addControls.addAll(addControlArg.getValues());
1735    }
1736
1737    if (deleteControlArg.isPresent())
1738    {
1739      deleteControls.addAll(deleteControlArg.getValues());
1740    }
1741
1742    if (modifyControlArg.isPresent())
1743    {
1744      modifyControls.addAll(modifyControlArg.getValues());
1745    }
1746
1747    if (modifyDNControlArg.isPresent())
1748    {
1749      modifyDNControls.addAll(modifyDNControlArg.getValues());
1750    }
1751
1752    if (proxyAsArg.isPresent())
1753    {
1754      final ProxiedAuthorizationV2RequestControl c =
1755           new ProxiedAuthorizationV2RequestControl(proxyAsArg.getValue());
1756      addControls.add(c);
1757      deleteControls.add(c);
1758      modifyControls.add(c);
1759      modifyDNControls.add(c);
1760    }
1761    else if (proxyV1AsArg.isPresent())
1762    {
1763      final ProxiedAuthorizationV1RequestControl c =
1764           new ProxiedAuthorizationV1RequestControl(proxyV1AsArg.getValue());
1765      addControls.add(c);
1766      deleteControls.add(c);
1767      modifyControls.add(c);
1768      modifyDNControls.add(c);
1769    }
1770
1771    if (usePermissiveModifyArg.isPresent())
1772    {
1773      modifyControls.add(new PermissiveModifyRequestControl(true));
1774    }
1775
1776    if (ignoreNoUserModificationArg.isPresent())
1777    {
1778      final IgnoreNoUserModificationRequestControl c =
1779           new IgnoreNoUserModificationRequestControl();
1780      addControls.add(c);
1781      modifyControls.add(c);
1782    }
1783
1784    if (useManageDsaITArg.isPresent())
1785    {
1786      final ManageDsaITRequestControl c = new ManageDsaITRequestControl(true);
1787      addControls.add(c);
1788      deleteControls.add(c);
1789      modifyControls.add(c);
1790      modifyDNControls.add(c);
1791    }
1792
1793    if (nameWithEntryUUIDArg.isPresent())
1794    {
1795      addControls.add(new NameWithEntryUUIDRequestControl(true));
1796    }
1797
1798    if (softDeleteArg.isPresent())
1799    {
1800      deleteControls.add(new SoftDeleteRequestControl(true, true));
1801    }
1802    else if (hardDeleteArg.isPresent())
1803    {
1804      deleteControls.add(new HardDeleteRequestControl(true));
1805    }
1806
1807    if (operationPurposeArg.isPresent())
1808    {
1809      final OperationPurposeRequestControl c =
1810           new OperationPurposeRequestControl(false, "parallel-update",
1811                Version.NUMERIC_VERSION_STRING,
1812                ParallelUpdate.class.getName() + ".getOperationControls",
1813                operationPurposeArg.getValue());
1814      addControls.add(c);
1815      deleteControls.add(c);
1816      modifyControls.add(c);
1817      modifyDNControls.add(c);
1818    }
1819
1820    if (replicationRepairArg.isPresent())
1821    {
1822      final ReplicationRepairRequestControl c =
1823           new ReplicationRepairRequestControl();
1824      addControls.add(c);
1825      deleteControls.add(c);
1826      modifyControls.add(c);
1827      modifyDNControls.add(c);
1828    }
1829
1830    if (suppressReferentialIntegrityUpdatesArg.isPresent())
1831    {
1832      final SuppressReferentialIntegrityUpdatesRequestControl c =
1833           new SuppressReferentialIntegrityUpdatesRequestControl(true);
1834      deleteControls.add(c);
1835      modifyDNControls.add(c);
1836    }
1837
1838
1839    if (useAssuredReplicationArg.isPresent())
1840    {
1841      final AssuredReplicationLocalLevel localLevel;
1842      if (assuredReplicationLocalLevelArg.isPresent())
1843      {
1844        final String localLevelStr = StaticUtils.toLowerCase(
1845             assuredReplicationLocalLevelArg.getValue());
1846        switch (localLevelStr)
1847        {
1848          case ASSURED_REPLICATION_LOCAL_LEVEL_NONE:
1849            localLevel = AssuredReplicationLocalLevel.NONE;
1850            break;
1851          case ASSURED_REPLICATION_LOCAL_LEVEL_RECEIVED_ANY_SERVER:
1852            localLevel = AssuredReplicationLocalLevel.RECEIVED_ANY_SERVER;
1853            break;
1854          case ASSURED_REPLICATION_LOCAL_LEVEL_PROCESSED_ALL_SERVERS:
1855            localLevel = AssuredReplicationLocalLevel.PROCESSED_ALL_SERVERS;
1856            break;
1857          default:
1858            // This should never happen.
1859            localLevel = null;
1860            break;
1861        }
1862      }
1863      else
1864      {
1865        localLevel = null;
1866      }
1867
1868      final AssuredReplicationRemoteLevel remoteLevel;
1869      if (assuredReplicationRemoteLevelArg.isPresent())
1870      {
1871        final String remoteLevelStr = StaticUtils.toLowerCase(
1872             assuredReplicationRemoteLevelArg.getValue());
1873        switch (remoteLevelStr)
1874        {
1875          case ASSURED_REPLICATION_REMOTE_LEVEL_NONE:
1876            remoteLevel = AssuredReplicationRemoteLevel.NONE;
1877            break;
1878          case ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ANY_REMOTE_LOCATION:
1879            remoteLevel = AssuredReplicationRemoteLevel.
1880                 RECEIVED_ANY_REMOTE_LOCATION;
1881            break;
1882          case ASSURED_REPLICATION_REMOTE_LEVEL_RECEIVED_ALL_REMOTE_LOCATIONS:
1883            remoteLevel = AssuredReplicationRemoteLevel.
1884                 RECEIVED_ALL_REMOTE_LOCATIONS;
1885            break;
1886          case ASSURED_REPLICATION_REMOTE_LEVEL_PROCESSED_ALL_REMOTE_SERVERS:
1887            remoteLevel = AssuredReplicationRemoteLevel.
1888                 PROCESSED_ALL_REMOTE_SERVERS;
1889            break;
1890          default:
1891            // This should never happen.
1892            remoteLevel = null;
1893            break;
1894        }
1895      }
1896      else
1897      {
1898        remoteLevel = null;
1899      }
1900
1901      final Long timeoutMillis;
1902      if (assuredReplicationTimeoutArg.isPresent())
1903      {
1904        timeoutMillis = assuredReplicationTimeoutArg.getValue(
1905             TimeUnit.MILLISECONDS);
1906      }
1907      else
1908      {
1909        timeoutMillis = null;
1910      }
1911
1912      final AssuredReplicationRequestControl c =
1913           new AssuredReplicationRequestControl(true, localLevel, localLevel,
1914                remoteLevel, remoteLevel, timeoutMillis, false);
1915      addControls.add(c);
1916      deleteControls.add(c);
1917      modifyControls.add(c);
1918      modifyDNControls.add(c);
1919    }
1920
1921
1922    if (passwordUpdateBehaviorArg.isPresent())
1923    {
1924      final PasswordUpdateBehaviorRequestControlProperties properties =
1925           new PasswordUpdateBehaviorRequestControlProperties();
1926      for (final String argValue : passwordUpdateBehaviorArg.getValues())
1927      {
1928        final int equalPos = argValue.indexOf('=');
1929        if (equalPos < 0)
1930        {
1931          throw new LDAPException(ResultCode.PARAM_ERROR,
1932               ERR_PARALLEL_UPDATE_MALFORMED_PW_UPDATE_VALUE.get(
1933                    argValue, passwordUpdateBehaviorArg.getIdentifierString()));
1934        }
1935
1936        final String propertyName = argValue.substring(0, equalPos).trim();
1937        final String lowerName = StaticUtils.toLowerCase(propertyName);
1938        switch (lowerName)
1939        {
1940          case PW_UPDATE_BEHAVIOR_NAME_IS_SELF_CHANGE:
1941            properties.setIsSelfChange(
1942                 getBooleanPWUpdateBehaviorValue(argValue));
1943            break;
1944          case PW_UPDATE_BEHAVIOR_NAME_ALLOW_PRE_ENCODED_PW:
1945            properties.setAllowPreEncodedPassword(
1946                 getBooleanPWUpdateBehaviorValue(argValue));
1947            break;
1948          case PW_UPDATE_BEHAVIOR_NAME_SKIP_PW_VALIDATION:
1949            properties.setSkipPasswordValidation(
1950                 getBooleanPWUpdateBehaviorValue(argValue));
1951            break;
1952          case PW_UPDATE_BEHAVIOR_NAME_IGNORE_PW_HISTORY:
1953            properties.setIgnorePasswordHistory(
1954                 getBooleanPWUpdateBehaviorValue(argValue));
1955            break;
1956          case PW_UPDATE_BEHAVIOR_NAME_IGNORE_MIN_PW_AGE:
1957            properties.setIgnoreMinimumPasswordAge(
1958                 getBooleanPWUpdateBehaviorValue(argValue));
1959            break;
1960          case PW_UPDATE_BEHAVIOR_NAME_MUST_CHANGE_PW:
1961            properties.setMustChangePassword(
1962                 getBooleanPWUpdateBehaviorValue(argValue));
1963            break;
1964          case PW_UPDATE_BEHAVIOR_NAME_PW_STORAGE_SCHEME:
1965            final String propertyValue = argValue.substring(equalPos+1).trim();
1966            properties.setPasswordStorageScheme(propertyValue);
1967            break;
1968          default:
1969            throw new LDAPException(ResultCode.PARAM_ERROR,
1970                 ERR_PARALLEL_UPDATE_UNKNOWN_PW_UPDATE_PROP.get(argValue,
1971                      passwordUpdateBehaviorArg.getIdentifierString(),
1972                      PW_UPDATE_BEHAVIOR_NAME_IS_SELF_CHANGE,
1973                      PW_UPDATE_BEHAVIOR_NAME_ALLOW_PRE_ENCODED_PW,
1974                      PW_UPDATE_BEHAVIOR_NAME_SKIP_PW_VALIDATION,
1975                      PW_UPDATE_BEHAVIOR_NAME_IGNORE_PW_HISTORY,
1976                      PW_UPDATE_BEHAVIOR_NAME_IGNORE_MIN_PW_AGE,
1977                      PW_UPDATE_BEHAVIOR_NAME_PW_STORAGE_SCHEME,
1978                      PW_UPDATE_BEHAVIOR_NAME_MUST_CHANGE_PW));
1979        }
1980      }
1981
1982      final PasswordUpdateBehaviorRequestControl c =
1983           new PasswordUpdateBehaviorRequestControl(properties, true);
1984      addControls.add(c);
1985      modifyControls.add(c);
1986    }
1987
1988
1989    if (suppressOperationalAttributeUpdatesArg.isPresent())
1990    {
1991      final EnumSet<SuppressType> suppressTypes =
1992           EnumSet.noneOf(SuppressType.class);
1993      for (final String s : suppressOperationalAttributeUpdatesArg.getValues())
1994      {
1995        if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_ACCESS_TIME))
1996        {
1997          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
1998        }
1999        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_TIME))
2000        {
2001          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
2002        }
2003        else if (s.equalsIgnoreCase(SUPPRESS_OP_ATTR_LAST_LOGIN_IP))
2004        {
2005          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
2006        }
2007      }
2008
2009      final SuppressOperationalAttributeUpdateRequestControl c =
2010           new SuppressOperationalAttributeUpdateRequestControl(true,
2011                suppressTypes);
2012      addControls.add(c);
2013      deleteControls.add(c);
2014      modifyControls.add(c);
2015      modifyDNControls.add(c);
2016    }
2017  }
2018
2019
2020
2021  /**
2022   * Retrieves the value from the provided name-value pair and parses it as a
2023   * boolean.
2024   *
2025   * @param  nameValuePair  The name-value pair to be parsed.  It must not be
2026   *                        {@code null} and it must contain an equal sign.
2027   *
2028   * @return  The boolean value parsed from the provided name-value pair.
2029   *
2030   * @throws  LDAPException  If the value could not be parsed as a boolean.
2031   */
2032  private boolean getBooleanPWUpdateBehaviorValue(
2033               @NotNull final String nameValuePair)
2034          throws LDAPException
2035  {
2036    final int equalPos = nameValuePair.indexOf('=');
2037    final String propertyValue = nameValuePair.substring(equalPos+1).trim();
2038    final String lowerValue = StaticUtils.toLowerCase(propertyValue);
2039    switch (lowerValue)
2040    {
2041      case "true":
2042      case "t":
2043      case "yes":
2044      case "on":
2045      case "1":
2046        return true;
2047      case "false":
2048      case "f":
2049      case "no":
2050      case "off":
2051      case "0":
2052        return false;
2053      default:
2054        final String propertyName = nameValuePair.substring(0, equalPos).trim();
2055        throw new LDAPException(ResultCode.PARAM_ERROR,
2056             ERR_PARALLEL_UPDATE_PW_UPDATE_VALUE_NOT_BOOLEAN.get(nameValuePair,
2057                  passwordUpdateBehaviorArg.getIdentifierString(),
2058                  propertyName));
2059    }
2060  }
2061
2062
2063
2064  /**
2065   * Creates the LDIF reader to use to read the changes to process.
2066   *
2067   * @return  An object pair in which the first element is the LDIF reader and
2068   *          the second element is the passphrase used to encrypt the contents
2069   *          of the LDIF file (or {@code null} if the LDIF file is not
2070   *          encrypted).
2071   *
2072   * @throws  LDAPException  If a problem occurs while trying to create the LDIF
2073   *                         reader.
2074   */
2075  @NotNull()
2076  private ObjectPair<LDIFReader,String> createLDIFReader()
2077          throws LDAPException
2078  {
2079    final File ldifFile = ldifFileArg.getValue();
2080
2081    try
2082    {
2083      final String encryptionPassphraseFromFile;
2084      if (encryptionPassphraseFileArg.isPresent())
2085      {
2086        final char[] pwChars = getPasswordFileReader().readPassword(
2087             encryptionPassphraseFileArg.getValue());
2088        encryptionPassphraseFromFile = new String(pwChars);
2089        Arrays.fill(pwChars, '\u0000');
2090      }
2091      else
2092      {
2093        encryptionPassphraseFromFile = null;
2094      }
2095
2096      final ObjectPair<InputStream,String> inputStreamPair =
2097           ToolUtils.getInputStreamForLDIFFiles(
2098                Collections.singletonList(ldifFile),
2099                encryptionPassphraseFromFile, getOut(), getErr());
2100
2101      final LDIFReader ldifReader = new LDIFReader(inputStreamPair.getFirst());
2102      final String encryptionPassphrase = inputStreamPair.getSecond();
2103      return new ObjectPair<>(ldifReader, encryptionPassphrase);
2104    }
2105    catch (final Exception e)
2106    {
2107      Debug.debugException(e);
2108      throw new LDAPException(ResultCode.LOCAL_ERROR,
2109           ERR_PARALLEL_UPDATE_CANNOT_CREATE_LDIF_READER.get(
2110                ldifFile.getAbsolutePath(),
2111                StaticUtils.getExceptionMessage(e)),
2112           e);
2113    }
2114  }
2115
2116
2117
2118  /**
2119   * Attempts to retrieve the ID of the encryption settings definition used to
2120   * encrypt the LDIF file.  This method should only be used if the LDIF file is
2121   * known to be encrypted.
2122   *
2123   * @return  The ID of the encryption settings definition used to encrypt the
2124   *          LDIF file, or {code null} if it was not encrypted with a
2125   *          passphrase obtained from an encryption settings definition (or if
2126   *          an error occurred while attempting to retrieve the ID).
2127   */
2128  @Nullable()
2129  private String getEncryptionSettingsDefinitionID()
2130  {
2131    try (FileInputStream inputStream =
2132              new FileInputStream(ldifFileArg.getValue()))
2133    {
2134      final PassphraseEncryptedStreamHeader encryptionHeader =
2135           PassphraseEncryptedStreamHeader.readFrom(inputStream, null);
2136      return encryptionHeader.getKeyIdentifier();
2137    }
2138    catch (final Exception e)
2139    {
2140      Debug.debugException(e);
2141      return null;
2142    }
2143  }
2144
2145
2146
2147  /**
2148   * Creates the LDIF writer that will be used to write information about
2149   * rejected entries.  If the LDIF input file was encrypted, then the reject
2150   * file will be encrypted with the same settings, and it will also be
2151   * compressed (regardless of whether the input file was compressed).
2152   *
2153   * @param  encryptionPassphrase            The passphrase used to encrypt the
2154   *                                         input file.  This may be
2155   *                                         {@code null} if the input file was
2156   *                                         not encrypted.
2157   * @param  encryptionSettingsDefinitionID  The ID for the encryption settings
2158   *                                         definition with which the
2159   *                                         passphrase is associated.  It may
2160   *                                         be {@code null} if the input file
2161   *                                         was not encrypted, or if the
2162   *                                         encryption passphrase was not
2163   *                                         obtained from an encryption
2164   *                                         settings definition.
2165   *
2166   * @return  The LDIF writer to which rejects should be written.
2167   *
2168   * @throws  LDAPException  If a problem occurs while creating the reject
2169   *                         writer.
2170   */
2171  @NotNull()
2172  private LDIFWriter createRejectWriter(
2173               @Nullable final String encryptionPassphrase,
2174               @Nullable final String encryptionSettingsDefinitionID)
2175          throws LDAPException
2176  {
2177    final File rejectFile = rejectFileArg.getValue();
2178
2179    OutputStream outputStream = null;
2180    try
2181    {
2182      outputStream = new FileOutputStream(rejectFile);
2183
2184      if (encryptionPassphrase != null)
2185      {
2186        outputStream = new PassphraseEncryptedOutputStream(encryptionPassphrase,
2187             outputStream, encryptionSettingsDefinitionID, true, true);
2188        outputStream = new GZIPOutputStream(outputStream);
2189      }
2190
2191
2192      final LDIFWriter ldifWriter = new LDIFWriter(outputStream);
2193
2194      // Set the wrap column to the maximum allowed value to ensure that
2195      // comments dont' get wrapped.
2196      ldifWriter.setWrapColumn(Integer.MAX_VALUE);
2197
2198      return ldifWriter;
2199    }
2200    catch (final Exception e)
2201    {
2202      Debug.debugException(e);
2203
2204      if (outputStream != null)
2205      {
2206        try
2207        {
2208          outputStream.close();
2209        }
2210        catch (final Exception e2)
2211        {
2212          Debug.debugException(e2);
2213        }
2214      }
2215
2216      throw new LDAPException(ResultCode.LOCAL_ERROR,
2217           ERR_PARALLEL_UPDATE_ERROR_CREATING_REJECT_WRITER.get(
2218                rejectFile.getAbsolutePath(),
2219                StaticUtils.getExceptionMessage(e)),
2220           e);
2221    }
2222  }
2223
2224
2225
2226  /**
2227   * Writes the provided message to standard output or standard error and sets
2228   * it as the completion message if there isn't one already.
2229   *
2230   * @param  isError  Indicates whether the message represents an error.
2231   * @param  message  The message to log.
2232   */
2233  private void logCompletionMessage(final boolean isError,
2234                                    @NotNull final String message)
2235  {
2236    completionMessage.compareAndSet(null, message);
2237    logMessage(message);
2238    if (isError)
2239    {
2240      wrapErr(0, WRAP_COLUMN, message);
2241    }
2242    else
2243    {
2244      wrapOut(0, WRAP_COLUMN, message);
2245    }
2246  }
2247
2248
2249
2250  /**
2251   * Logs the provided message if logging is enabled.
2252   *
2253   * @param  messageElements  The elements that make up the message to log.
2254   */
2255  private void logMessage(@NotNull final Object... messageElements)
2256  {
2257    if (logWriter != null)
2258    {
2259      SimpleDateFormat timestampFormatter = timestampFormatters.get();
2260      if (timestampFormatter == null)
2261      {
2262        timestampFormatter =
2263             new SimpleDateFormat("'['dd/MMM/yyyy:HH:mm:ss Z']'");
2264        timestampFormatters.set(timestampFormatter);
2265      }
2266
2267      final String timestamp = timestampFormatter.format(new Date());
2268
2269      final StringBuilder message = new StringBuilder();
2270      message.append('[');
2271      message.append(timestamp);
2272      message.append("] ");
2273      for (final Object o : messageElements)
2274      {
2275        message.append(String.valueOf(o));
2276      }
2277
2278      try
2279      {
2280        logWriter.println(message);
2281      }
2282      catch (final Exception e)
2283      {
2284        Debug.debugException(e);
2285        final String errorMessage =
2286             ERR_PARALLEL_UPDATE_CANNOT_WRITE_LOG_MESSAGE.get(
2287                  message.toString(), logFileArg.getValue().getAbsolutePath(),
2288                  StaticUtils.getExceptionMessage(e));
2289        wrapErr(0, WRAP_COLUMN, errorMessage);
2290        completionMessage.compareAndSet(null, errorMessage);
2291        shouldAbort.set(true);
2292      }
2293    }
2294  }
2295
2296
2297
2298  /**
2299   * Indicates that processing for an operation completed successfully.
2300   *
2301   * @param  changeRecord              The LDIF change record for the operation.
2302   *                                   It must not be {@code null}.
2303   * @param  processingDurationMillis  The length of time required to process
2304   *                                   the operation, in milliseconds.
2305   */
2306  void opCompletedSuccessfully(@NotNull final LDIFChangeRecord changeRecord,
2307                               final long processingDurationMillis)
2308  {
2309    opsAttempted.incrementAndGet();
2310    opsSucceeded.incrementAndGet();
2311    totalOpDurationMillis.addAndGet(processingDurationMillis);
2312    logMessage(changeRecord.getDN(), " ",
2313         changeRecord.getChangeType().getName(), " SUCCESS 0 ");
2314  }
2315
2316
2317
2318  /**
2319   * Indicates that processing for an operation failed.  Depending on the nature
2320   * of the failure, it either be added to the retry queue if it is potentially
2321   * an operation that could be retried (e.g., an operation that failed because
2322   * it depended on an entry that was added later in the LDIF), or will be added
2323   * to the reject file if it is determined that it is not a failure that may be
2324   * resolved by a later change.
2325   *
2326   * @param  changeRecord              The LDIF change record for the operation.
2327   *                                   It must not be {@code null}.
2328   * @param  ldapException             The LDAP exception that was caught to
2329   *                                   indicate that the operation failed.  It
2330   *                                   must not be {@code null}.
2331   * @param  processingDurationMillis  The length of time required to process
2332   *                                   the operation, in milliseconds.
2333   */
2334  void opFailed(@NotNull final LDIFChangeRecord changeRecord,
2335                @NotNull final LDAPException ldapException,
2336                final long processingDurationMillis)
2337  {
2338    opsAttempted.incrementAndGet();
2339    totalOpDurationMillis.addAndGet(processingDurationMillis);
2340
2341    switch (ldapException.getResultCode().intValue())
2342    {
2343      case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
2344      case ResultCode.BUSY_INT_VALUE:
2345      case ResultCode.UNAVAILABLE_INT_VALUE:
2346      case ResultCode.NOT_ALLOWED_ON_NONLEAF_INT_VALUE:
2347      case ResultCode.SERVER_DOWN_INT_VALUE:
2348      case ResultCode.CONNECT_ERROR_INT_VALUE:
2349        retry(changeRecord, ldapException);
2350        break;
2351      default:
2352        reject(changeRecord, ldapException);
2353        break;
2354    }
2355  }
2356
2357
2358
2359  /**
2360   * Adds the provided operation to the retry queue.
2361   *
2362   * @param  changeRecord   The LDIF change record for the operation.
2363   * @param  ldapException  The LDAP exception that was caught to indicate that
2364   *                        the operation failed.
2365   */
2366  private void retry(@NotNull final LDIFChangeRecord changeRecord,
2367                     @NotNull final LDAPException ldapException)
2368  {
2369    if (neverRetryArg.isPresent())
2370    {
2371      reject(changeRecord, ldapException);
2372      return;
2373    }
2374
2375    final DN parsedDN;
2376    try
2377    {
2378      parsedDN = changeRecord.getParsedDN();
2379    }
2380    catch (final LDAPException e)
2381    {
2382      Debug.debugException(e);
2383
2384      // This should never happen, but if it does, then reject the change.
2385      reject(changeRecord, ldapException);
2386      return;
2387    }
2388
2389    logMessage(changeRecord.getDN(), " ",
2390         changeRecord.getChangeType().getName(), " RETRY ",
2391         ldapException.getResultCode(), " ", ldapException.getMessage());
2392
2393    synchronized (retryQueue)
2394    {
2395      List<ObjectPair<LDIFChangeRecord,LDAPException>> changeList =
2396           retryQueue.get(parsedDN);
2397
2398      if (changeList == null)
2399      {
2400        changeList = new LinkedList<>();
2401        retryQueue.put(parsedDN, changeList);
2402      }
2403
2404      changeList.add(new ObjectPair<>(changeRecord, ldapException));
2405      retryQueueSize.incrementAndGet();
2406    }
2407  }
2408
2409
2410
2411  /**
2412   * Adds the provided operation to the reject file.
2413   *
2414   * @param  changeRecord   The LDIF change record for the operation.
2415   * @param  ldapException  The LDAP exception that was caught to indicate that
2416   *                        the operation failed.
2417   */
2418  void reject(@Nullable final LDIFChangeRecord changeRecord,
2419              @NotNull final LDAPException ldapException)
2420  {
2421    opsRejected.incrementAndGet();
2422
2423    final ResultCode resultCode = ldapException.getResultCode();
2424    if (resultCode != ResultCode.SUCCESS)
2425    {
2426      firstRejectResultCode.compareAndSet(null, resultCode);
2427    }
2428
2429    final StringBuilder commentBuffer = new StringBuilder();
2430    for (final String line :
2431         ResultUtils.formatResult(ldapException, false, 0, 0))
2432    {
2433      if (commentBuffer.length() > 0)
2434      {
2435        commentBuffer.append(StaticUtils.EOL);
2436      }
2437      commentBuffer.append(line);
2438    }
2439    final String comment = commentBuffer.toString();
2440
2441    try
2442    {
2443      if (changeRecord != null)
2444      {
2445        logMessage(changeRecord.getDN(), " ",
2446            changeRecord.getChangeType().getName(), " REJECT ", resultCode, " ",
2447             ldapException.getMessage());
2448
2449        synchronized (rejectWriter)
2450        {
2451          rejectWriter.writeChangeRecord(changeRecord, comment);
2452        }
2453      }
2454      else
2455      {
2456        synchronized (rejectWriter)
2457        {
2458          rejectWriter.writeComment(comment, true, true);
2459        }
2460      }
2461    }
2462    catch (final Exception e)
2463    {
2464      Debug.debugException(e);
2465
2466      final String errorMessage = ERR_PARALLEL_UPDATE_CANNOT_WRITE_REJECT.get(
2467           changeRecord.toString(),
2468           ERR_PARALLEL_UPDATE_REJECT_COMMENT.get(String.valueOf(resultCode),
2469                ldapException.getMessage()),
2470           StaticUtils.getExceptionMessage(e));
2471        wrapErr(0, WRAP_COLUMN, errorMessage);
2472        completionMessage.compareAndSet(null, errorMessage);
2473        shouldAbort.set(true);
2474    }
2475  }
2476
2477
2478
2479  /**
2480   * Prints information about the processing performed by this program to
2481   * standard output.
2482   */
2483  void printIntervalData()
2484  {
2485    final long currentAttempts = opsAttempted.get();
2486    final long currentSuccesses = opsSucceeded.get();
2487    final long currentReject = opsRejected.get();
2488    final long currentRetry = retryQueueSize.get();
2489    final long currentDurationMillis = totalOpDurationMillis.get();
2490    final long currentTimeMillis = System.currentTimeMillis();
2491    final long totalDurationMillis =
2492         currentTimeMillis - processingStartTimeMillis;
2493
2494    final long avgRate;
2495    if (totalDurationMillis == 0L)
2496    {
2497      avgRate = 0L;
2498    }
2499    else
2500    {
2501      avgRate = 1000L * currentAttempts / totalDurationMillis;
2502    }
2503
2504    final long avgDurationMillis;
2505    if (currentAttempts == 0L)
2506    {
2507      avgDurationMillis = 0L;
2508    }
2509    else
2510    {
2511      avgDurationMillis = currentDurationMillis / currentAttempts;
2512    }
2513
2514    final long recentDurationMillis;
2515    if (currentAttempts == lastOpsAttempted)
2516    {
2517      recentDurationMillis = 0L;
2518    }
2519    else
2520    {
2521      recentDurationMillis = (currentDurationMillis - lastTotalDurationMillis) /
2522           (currentAttempts - lastOpsAttempted);
2523    }
2524
2525    final long recentRate;
2526    if (lastOpsAttempted == 0)
2527    {
2528      out(" Attempts Successes   Rejects   ToRetry  AvgOps/S " +
2529          " RctOps/S  AvgDurMS  RctDurMS");
2530      out("--------- --------- --------- --------- --------- " +
2531          "--------- --------- ---------");
2532      recentRate = avgRate;
2533    }
2534    else if (currentTimeMillis == lastUpdateTimeMillis)
2535    {
2536      recentRate = 0L;
2537    }
2538    else
2539    {
2540      recentRate = 1000L * (currentAttempts - lastOpsAttempted) /
2541                   (currentTimeMillis - lastUpdateTimeMillis);
2542    }
2543
2544    final StringBuilder buffer = new StringBuilder(80);
2545    appendJustified(currentAttempts, buffer, true);
2546    appendJustified(currentSuccesses, buffer, true);
2547    appendJustified(currentReject, buffer, true);
2548    appendJustified(currentRetry, buffer, true);
2549    appendJustified(avgRate, buffer, true);
2550    appendJustified(recentRate, buffer, true);
2551    appendJustified(avgDurationMillis, buffer, true);
2552    appendJustified(recentDurationMillis, buffer, false);
2553
2554    out(buffer.toString());
2555
2556    lastOpsAttempted = currentAttempts;
2557    lastTotalDurationMillis = currentDurationMillis;
2558    lastUpdateTimeMillis = currentTimeMillis;
2559  }
2560
2561
2562
2563  /**
2564   * Appends the provided number to the buffer, right justified in nine columns.
2565   *
2566   * @param  value     The value to be appended to the buffer.
2567   * @param  buffer    The buffer to which the value should be appended.
2568   * @param  addSpace  Indicates whether to append a space after the number.
2569   */
2570  static void appendJustified(final long value,
2571                              @NotNull final StringBuilder buffer,
2572                              final boolean addSpace)
2573  {
2574    final String valueStr = String.valueOf(value);
2575    switch (valueStr.length())
2576    {
2577      case 1:
2578        buffer.append("        ");
2579        break;
2580      case 2:
2581        buffer.append("       ");
2582        break;
2583      case 3:
2584        buffer.append("      ");
2585        break;
2586      case 4:
2587        buffer.append("     ");
2588        break;
2589      case 5:
2590        buffer.append("    ");
2591        break;
2592      case 6:
2593        buffer.append("   ");
2594        break;
2595      case 7:
2596        buffer.append("  ");
2597        break;
2598      case 8:
2599        buffer.append(' ');
2600        break;
2601    }
2602
2603    buffer.append(value);
2604
2605    if (addSpace)
2606    {
2607      buffer.append(' ');
2608    }
2609  }
2610
2611
2612
2613  /**
2614   * Retrieves the total number of operations attempted.  This should only be
2615   * called after all tool processing has completed.
2616   *
2617   * @return  The total number of operations attempted.
2618   */
2619  public long getTotalAttemptCount()
2620  {
2621    return opsAttempted.get();
2622  }
2623
2624
2625
2626  /**
2627   * Retrieves the number of operations attempted on the initial pass through
2628   * the LDIF file (that is, operations for which no retry attempts was made).
2629   * This should only be called after all tool processing has completed.
2630   *
2631   * @return  The number of operations attempted on the initial pass through the
2632   *          LDIF file.
2633   */
2634  public long getInitialAttemptCount()
2635  {
2636    return initialAttempted;
2637  }
2638
2639
2640
2641  /**
2642   * Retrieves the number of retry attempts made for operations that did not
2643   * complete successfully on their first attempt.  This should only be called
2644   * after all tool processing has completed.
2645   *
2646   * @return  The number of retry attempts made for operations that did not
2647   *          complete successfully on their first attempt.
2648   */
2649  public long getRetryAttemptCount()
2650  {
2651    return opsAttempted.get() - initialAttempted;
2652  }
2653
2654
2655
2656  /**
2657   * Retrieves the total number of operations that completed successfully.  This
2658   * should only be called after all tool processing has completed.
2659   *
2660   * @return  The total number of operations that completed successfully.
2661   */
2662  public long getTotalSuccessCount()
2663  {
2664    return opsSucceeded.get();
2665  }
2666
2667
2668
2669  /**
2670   * Retrieves the number of operations that completed successfully on their
2671   * first attempt.  This should only be called after all tool processing has
2672   * completed.
2673   *
2674   * @return  The total number of operations that completed successfully on
2675   *          their first attempt.
2676   */
2677  public long getInitialSuccessCount()
2678  {
2679    return initialSucceeded;
2680  }
2681
2682
2683
2684  /**
2685   * Retrieves the number of operations that did not complete completed
2686   * successfully on their initial attempt but did succeed on a retry attempt.
2687   * This should only be called after all tool processing has completed.
2688   *
2689   * @return  The number of operations that completed successfully on a retry
2690   *          attempt.
2691   */
2692  public long getRetrySuccessCount()
2693  {
2694    return opsSucceeded.get() - initialSucceeded;
2695  }
2696
2697
2698
2699  /**
2700   * Retrieves the number of operations that were rejected and did not complete
2701   * successfully during any of the attempts.  This should only be called after
2702   * all tool processing has completed.
2703   *
2704   * @return  The number of operations that were rejected.
2705   */
2706  public long getRejectCount()
2707  {
2708    return opsRejected.get();
2709  }
2710
2711
2712
2713  /**
2714   * Retrieves the total length of time, in milliseconds, spent processing
2715   * operations.  This should only be called after all tool processing has
2716   * completed.  Note that when running with multiple threads, this can exceed
2717   * the length of time spent running the tool because multiple operations can
2718   * be processed in parallel.
2719   *
2720   * @return  The total length of time, in milliseconds, spent processing
2721   *          operations.
2722   */
2723  public long getTotalOpDurationMillis()
2724  {
2725    return totalOpDurationMillis.get();
2726  }
2727
2728
2729
2730  /**
2731   * {@inheritDoc}
2732   */
2733  @Override()
2734  protected boolean registerShutdownHook()
2735  {
2736    return true;
2737  }
2738
2739
2740
2741  /**
2742   * {@inheritDoc}
2743   */
2744  @Override()
2745  protected void doShutdownHookProcessing(@Nullable final ResultCode resultCode)
2746  {
2747    shouldAbort.set(true);
2748
2749    final FixedRateBarrier b = rateLimiter;
2750    if (b != null)
2751    {
2752      b.shutdownRequested();
2753    }
2754  }
2755
2756
2757
2758  /**
2759   * {@inheritDoc}
2760   */
2761  @Override()
2762  public void handleUnsolicitedNotification(
2763                   @NotNull final LDAPConnection connection,
2764                   @NotNull final ExtendedResult notification)
2765  {
2766    final String message;
2767    if (notification.getDiagnosticMessage() == null)
2768    {
2769      message = INFO_PARALLEL_UPDATE_UNSOLICITED_NOTIFICATION_NO_MESSAGE.get(
2770           getToolName(), String.valueOf(notification.getResultCode()),
2771           notification.getOID());
2772    }
2773    else
2774    {
2775      message = INFO_PARALLEL_UPDATE_UNSOLICITED_NOTIFICATION_NO_MESSAGE.get(
2776           getToolName(), String.valueOf(notification.getResultCode()),
2777           notification.getOID(), notification.getDiagnosticMessage());
2778    }
2779
2780    out();
2781    wrapOut(0, WRAP_COLUMN, message);
2782    out();
2783  }
2784
2785
2786
2787  /**
2788   * {@inheritDoc}
2789   */
2790  @Override()
2791  @NotNull()
2792  public LinkedHashMap<String[],String> getExampleUsages()
2793  {
2794    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
2795
2796    examples.put(
2797         new String[]
2798         {
2799           "--hostname", "server.example.com",
2800           "--port", "636",
2801           "--useSSL",
2802           "--bindDN", "uid=admin,dc=example,dc=com",
2803           "--promptForBindPassword",
2804           "--ldifFile", "changes.ldif",
2805           "--rejectFile", "rejects.ldif",
2806           "--defaultAdd",
2807           "--numThreads", "10",
2808           "--ratePerSecond", "5000"
2809         },
2810         INFO_PARALLEL_UPDATE_EXAMPLE_DESC.get());
2811
2812    return examples;
2813  }
2814}