001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-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) 2020-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.ldif;
037
038
039
040import java.io.File;
041import java.io.FileInputStream;
042import java.io.FileOutputStream;
043import java.io.IOException;
044import java.io.InputStream;
045import java.io.OutputStream;
046import java.util.ArrayList;
047import java.util.Arrays;
048import java.util.HashMap;
049import java.util.Iterator;
050import java.util.LinkedHashMap;
051import java.util.List;
052import java.util.Map;
053import java.util.TreeMap;
054import java.util.concurrent.atomic.AtomicBoolean;
055import java.util.concurrent.atomic.AtomicLong;
056import java.util.concurrent.atomic.AtomicReference;
057import java.util.zip.GZIPOutputStream;
058
059import com.unboundid.ldap.sdk.Attribute;
060import com.unboundid.ldap.sdk.ChangeType;
061import com.unboundid.ldap.sdk.DN;
062import com.unboundid.ldap.sdk.Entry;
063import com.unboundid.ldap.sdk.InternalSDKHelper;
064import com.unboundid.ldap.sdk.LDAPException;
065import com.unboundid.ldap.sdk.RDN;
066import com.unboundid.ldap.sdk.ResultCode;
067import com.unboundid.ldap.sdk.Version;
068import com.unboundid.ldap.sdk.schema.Schema;
069import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils;
070import com.unboundid.util.CommandLineTool;
071import com.unboundid.util.Debug;
072import com.unboundid.util.NotNull;
073import com.unboundid.util.Nullable;
074import com.unboundid.util.ObjectPair;
075import com.unboundid.util.PassphraseEncryptedOutputStream;
076import com.unboundid.util.StaticUtils;
077import com.unboundid.util.ThreadSafety;
078import com.unboundid.util.ThreadSafetyLevel;
079import com.unboundid.util.Validator;
080import com.unboundid.util.args.ArgumentException;
081import com.unboundid.util.args.ArgumentParser;
082import com.unboundid.util.args.BooleanArgument;
083import com.unboundid.util.args.FileArgument;
084import com.unboundid.util.args.IntegerArgument;
085
086import static com.unboundid.ldif.LDIFMessages.*;
087
088
089
090/**
091 * This class provides a command-line tool that can be used to apply a set of
092 * changes to data in an LDIF file.
093 */
094@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
095public final class LDIFModify
096       extends CommandLineTool
097{
098  /**
099   * The server root directory for the Ping Identity Directory Server (or
100   * related Ping Identity server product) that contains this tool, if
101   * applicable.
102   */
103  @NotNull private static final File PING_SERVER_ROOT =
104       InternalSDKHelper.getPingIdentityServerRoot();
105
106
107
108  /**
109   * Indicates whether the tool is running as part of a Ping Identity Directory
110   * Server (or related Ping Identity Server Product) installation.
111   */
112  private static final boolean PING_SERVER_AVAILABLE =
113       (PING_SERVER_ROOT != null);
114
115
116
117  /**
118   * The column at which to wrap long lines.
119   */
120  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
121
122
123
124  // The completion message for this tool.
125  @NotNull private final AtomicReference<String> completionMessage;
126
127  // Encryption passphrases used thus far.
128  @NotNull private final List<char[]> inputEncryptionPassphrases;
129
130  // The command-line arguments supported by this tool.
131  @Nullable private BooleanArgument compressTarget;
132  @Nullable private BooleanArgument doNotWrap;
133  @Nullable private BooleanArgument encryptTarget;
134  @Nullable private BooleanArgument ignoreDeletesOfNonexistentEntries;
135  @Nullable private BooleanArgument ignoreDuplicateDeletes;
136  @Nullable private BooleanArgument ignoreModifiesOfNonexistentEntries;
137  @Nullable private BooleanArgument lenientModifications;
138  @Nullable private BooleanArgument strictModifications;
139  @Nullable private BooleanArgument noSchemaCheck;
140  @Nullable private BooleanArgument stripTrailingSpaces;
141  @Nullable private BooleanArgument suppressComments;
142  @Nullable private FileArgument changesEncryptionPassphraseFile;
143  @Nullable private FileArgument changesLDIF;
144  @Nullable private FileArgument sourceEncryptionPassphraseFile;
145  @Nullable private FileArgument sourceLDIF;
146  @Nullable private FileArgument targetEncryptionPassphraseFile;
147  @Nullable private FileArgument targetLDIF;
148  @Nullable private IntegerArgument wrapColumn;
149
150  // Variables that may be used by support for a legacy implementation.
151  @Nullable private LDIFReader changesReader;
152  @Nullable private LDIFReader sourceReader;
153  @Nullable private LDIFWriter targetWriter;
154  @Nullable private List<String> errorMessages;
155
156
157
158  /**
159   * Invokes this tool with the provided set of command-line arguments.
160   *
161   * @param  args  The set of arguments provided to this tool.  It may be
162   *               empty but must not be {@code null}.
163   */
164  public static void main(@NotNull final String... args)
165  {
166    final ResultCode resultCode = main(System.out, System.err, args);
167    if (resultCode != ResultCode.SUCCESS)
168    {
169      System.exit(resultCode.intValue());
170    }
171  }
172
173
174
175  /**
176   * Invokes this tool with the provided set of command-line arguments, using
177   * the given output and error streams.
178   *
179   * @param  out   The output stream to use for standard output.  It may be
180   *               {@code null} if standard output should be suppressed.
181   * @param  err   The output stream to use for standard error.  It may be
182   *               {@code null} if standard error should be suppressed.
183   * @param  args  The set of arguments provided to this tool.  It may be
184   *               empty but must not be {@code null}.
185   *
186   * @return  A result code indicating the status of processing.  Any result
187   *          code other than {@link ResultCode#SUCCESS} should be considered
188   *          an error.
189   */
190  @NotNull()
191  public static ResultCode main(@Nullable final OutputStream out,
192                                @Nullable final OutputStream err,
193                                @NotNull final String... args)
194  {
195    final LDIFModify tool = new LDIFModify(out, err);
196    return tool.runTool(args);
197  }
198
199
200
201  /**
202   * Invokes this tool with the provided readers and writer.  This method is
203   * primarily intended for legacy backward compatibility with the Ping Identity
204   * Directory Server and does not provide access to all functionality offered
205   * by this tool.
206   *
207   * @param  sourceReader   An LDIF reader that may be used to read the entries
208   *                        to be updated.  It must not be {@code null}.  Note
209   *                        this the reader will be closed when the tool
210   *                        completes.
211   * @param  changesReader  An LDIF reader that may be used to read the changes
212   *                        to apply.  It must not be {@code null}.  Note that
213   *                        this reader will be closed when the tool completes.
214   * @param  targetWriter   An LDIF writer that may be used to write the updated
215   *                        entries.  It must not be {@code null}.  Note that
216   *                        this writer will be closed when the tool completes.
217   * @param  errorMessages  A list that will be updated with any errors
218   *                        encountered during processing.  It must not be
219   *                        {@code null} and must be updatable.
220   *
221   * @return  {@code true} if processing completed successfully, or
222   *          {@code false} if one or more errors were encountered.
223   */
224  public static boolean main(@NotNull final LDIFReader sourceReader,
225                             @NotNull final LDIFReader changesReader,
226                             @NotNull final LDIFWriter targetWriter,
227                             @NotNull final List<String> errorMessages)
228  {
229    Validator.ensureNotNull(sourceReader, changesReader, targetWriter,
230         errorMessages);
231
232    final LDIFModify tool = new LDIFModify(null, null);
233    tool.sourceReader = sourceReader;
234    tool.changesReader = changesReader;
235    tool.targetWriter = targetWriter;
236    tool.errorMessages = errorMessages;
237
238    try
239    {
240      final ResultCode resultCode =
241           tool.runTool("--suppressComments", "--lenientModifications");
242      return (resultCode == ResultCode.SUCCESS);
243    }
244    finally
245    {
246      try
247      {
248        sourceReader.close();
249      }
250      catch (final Exception e)
251      {
252        Debug.debugException(e);
253      }
254
255      try
256      {
257        changesReader.close();
258      }
259      catch (final Exception e)
260      {
261        Debug.debugException(e);
262      }
263
264      try
265      {
266        targetWriter.close();
267      }
268      catch (final Exception e)
269      {
270        Debug.debugException(e);
271      }
272    }
273  }
274
275
276
277  /**
278   * Creates a new instance of this tool with the provided output and error
279   * streams.
280   *
281   * @param  out  The output stream to use for standard output.  It may be
282   *              {@code null} if standard output should be suppressed.
283   * @param  err  The output stream to use for standard error.  It may be
284   *              {@code null} if standard error should be suppressed.
285   */
286  public LDIFModify(@Nullable final OutputStream out,
287                    @Nullable final OutputStream err)
288  {
289    super(out, err);
290
291    completionMessage = new AtomicReference<>();
292    inputEncryptionPassphrases = new ArrayList<>(5);
293
294    compressTarget = null;
295    doNotWrap = null;
296    encryptTarget = null;
297    ignoreDeletesOfNonexistentEntries = null;
298    ignoreDuplicateDeletes = null;
299    ignoreModifiesOfNonexistentEntries = null;
300    lenientModifications = null;
301    noSchemaCheck = null;
302    strictModifications = null;
303    stripTrailingSpaces = null;
304    suppressComments = null;
305    changesEncryptionPassphraseFile = null;
306    changesLDIF = null;
307    sourceEncryptionPassphraseFile = null;
308    sourceLDIF = null;
309    targetEncryptionPassphraseFile = null;
310    targetLDIF = null;
311    wrapColumn = null;
312
313    changesReader = null;
314    sourceReader = null;
315    targetWriter = null;
316    errorMessages = null;
317  }
318
319
320
321  /**
322   * {@inheritDoc}
323   */
324  @Override()
325  @NotNull()
326  public String getToolName()
327  {
328    return "ldifmodify";
329  }
330
331
332
333  /**
334   * {@inheritDoc}
335   */
336  @Override()
337  @NotNull()
338  public String getToolDescription()
339  {
340    return INFO_LDIFMODIFY_TOOL_DESCRIPTION.get();
341  }
342
343
344
345  /**
346   * {@inheritDoc}
347   */
348  @Override()
349  @NotNull()
350  public List<String> getAdditionalDescriptionParagraphs()
351  {
352    return Arrays.asList(
353         INFO_LDIFMODIFY_TOOL_DESCRIPTION_2.get(),
354         INFO_LDIFMODIFY_TOOL_DESCRIPTION_3.get(),
355         INFO_LDIFMODIFY_TOOL_DESCRIPTION_4.get(),
356         INFO_LDIFMODIFY_TOOL_DESCRIPTION_5.get());
357  }
358
359
360
361  /**
362   * {@inheritDoc}
363   */
364  @Override()
365  @NotNull()
366  public String getToolVersion()
367  {
368    return Version.NUMERIC_VERSION_STRING;
369  }
370
371
372
373  /**
374   * {@inheritDoc}
375   */
376  @Override()
377  public boolean supportsInteractiveMode()
378  {
379    return true;
380  }
381
382
383
384  /**
385   * {@inheritDoc}
386   */
387  @Override()
388  public boolean defaultsToInteractiveMode()
389  {
390    return true;
391  }
392
393
394
395  /**
396   * {@inheritDoc}
397   */
398  @Override()
399  public boolean supportsPropertiesFile()
400  {
401    return true;
402  }
403
404
405
406  /**
407   * {@inheritDoc}
408   */
409  @Override()
410  protected boolean supportsDebugLogging()
411  {
412    return true;
413  }
414
415
416
417  /**
418   * {@inheritDoc}
419   */
420  @Override()
421  @Nullable()
422  protected String getToolCompletionMessage()
423  {
424    return completionMessage.get();
425  }
426
427
428
429  /**
430   * {@inheritDoc}
431   */
432  @Override()
433  public void addToolArguments(@NotNull final ArgumentParser parser)
434         throws ArgumentException
435  {
436    sourceLDIF = new FileArgument('s', "sourceLDIF", (sourceReader == null), 1,
437         null, INFO_LDIFMODIFY_ARG_DESC_SOURCE_LDIF.get(), true, true, true,
438         false);
439    sourceLDIF.addLongIdentifier("source-ldif", true);
440    sourceLDIF.addLongIdentifier("sourceFile", true);
441    sourceLDIF.addLongIdentifier("source-file", true);
442    sourceLDIF.addLongIdentifier("source", true);
443    sourceLDIF.addLongIdentifier("inputLDIF", true);
444    sourceLDIF.addLongIdentifier("input-ldif", true);
445    sourceLDIF.addLongIdentifier("inputFile", true);
446    sourceLDIF.addLongIdentifier("input-file", true);
447    sourceLDIF.addLongIdentifier("input", true);
448    sourceLDIF.addLongIdentifier("ldifFile", true);
449    sourceLDIF.addLongIdentifier("ldif-file", true);
450    sourceLDIF.addLongIdentifier("ldif", true);
451    sourceLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
452    parser.addArgument(sourceLDIF);
453
454
455    final String sourcePWDesc;
456    if (PING_SERVER_AVAILABLE)
457    {
458      sourcePWDesc = INFO_LDIFMODIFY_ARG_DESC_SOURCE_PW_FILE_PING_SERVER.get();
459    }
460    else
461    {
462      sourcePWDesc = INFO_LDIFMODIFY_ARG_DESC_SOURCE_PW_FILE_STANDALONE.get();
463    }
464    sourceEncryptionPassphraseFile = new FileArgument(null,
465         "sourceEncryptionPassphraseFile", false, 1, null, sourcePWDesc, true,
466         true, true, false);
467    sourceEncryptionPassphraseFile.addLongIdentifier(
468         "source-encryption-passphrase-file", true);
469    sourceEncryptionPassphraseFile.addLongIdentifier("sourcePassphraseFile",
470         true);
471    sourceEncryptionPassphraseFile.addLongIdentifier("source-passphrase-file",
472         true);
473    sourceEncryptionPassphraseFile.addLongIdentifier(
474         "sourceEncryptionPasswordFile", true);
475    sourceEncryptionPassphraseFile.addLongIdentifier(
476         "source-encryption-password-file", true);
477    sourceEncryptionPassphraseFile.addLongIdentifier("sourcePasswordFile",
478         true);
479    sourceEncryptionPassphraseFile.addLongIdentifier("source-password-file",
480         true);
481    sourceEncryptionPassphraseFile.addLongIdentifier(
482         "inputEncryptionPassphraseFile", true);
483    sourceEncryptionPassphraseFile.addLongIdentifier(
484         "input-encryption-passphrase-file", true);
485    sourceEncryptionPassphraseFile.addLongIdentifier("inputPassphraseFile",
486         true);
487    sourceEncryptionPassphraseFile.addLongIdentifier("input-passphrase-file",
488         true);
489    sourceEncryptionPassphraseFile.addLongIdentifier(
490         "inputEncryptionPasswordFile", true);
491    sourceEncryptionPassphraseFile.addLongIdentifier(
492         "input-encryption-password-file", true);
493    sourceEncryptionPassphraseFile.addLongIdentifier("inputPasswordFile", true);
494    sourceEncryptionPassphraseFile.addLongIdentifier("input-password-file",
495         true);
496    sourceEncryptionPassphraseFile.setArgumentGroupName(
497         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
498    parser.addArgument(sourceEncryptionPassphraseFile);
499
500
501    changesLDIF = new FileArgument('m', "changesLDIF", (changesReader == null),
502         1, null, INFO_LDIFMODIFY_ARG_DESC_CHANGES_LDIF.get(), true, true, true,
503         false);
504    changesLDIF.addLongIdentifier("changes-ldif", true);
505    changesLDIF.addLongIdentifier("changesFile", true);
506    changesLDIF.addLongIdentifier("changes-file", true);
507    changesLDIF.addLongIdentifier("changes", true);
508    changesLDIF.addLongIdentifier("updatesLDIF", true);
509    changesLDIF.addLongIdentifier("updates-ldif", true);
510    changesLDIF.addLongIdentifier("updatesFile", true);
511    changesLDIF.addLongIdentifier("updates-file", true);
512    changesLDIF.addLongIdentifier("updates", true);
513    changesLDIF.addLongIdentifier("modificationsLDIF", true);
514    changesLDIF.addLongIdentifier("modifications-ldif", true);
515    changesLDIF.addLongIdentifier("modificationsFile", true);
516    changesLDIF.addLongIdentifier("modifications-file", true);
517    changesLDIF.addLongIdentifier("modifications", true);
518    changesLDIF.addLongIdentifier("modsLDIF", true);
519    changesLDIF.addLongIdentifier("mods-ldif", true);
520    changesLDIF.addLongIdentifier("modsFile", true);
521    changesLDIF.addLongIdentifier("mods-file", true);
522    changesLDIF.addLongIdentifier("mods", true);
523    changesLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
524    parser.addArgument(changesLDIF);
525
526
527    final String changesPWDesc;
528    if (PING_SERVER_AVAILABLE)
529    {
530      changesPWDesc =
531           INFO_LDIFMODIFY_ARG_DESC_CHANGES_PW_FILE_PING_SERVER.get();
532    }
533    else
534    {
535      changesPWDesc = INFO_LDIFMODIFY_ARG_DESC_CHANGES_PW_FILE_STANDALONE.get();
536    }
537    changesEncryptionPassphraseFile = new FileArgument(null,
538         "changesEncryptionPassphraseFile", false, 1, null, changesPWDesc, true,
539         true, true, false);
540    changesEncryptionPassphraseFile.addLongIdentifier(
541         "changes-encryption-passphrase-file", true);
542    changesEncryptionPassphraseFile.addLongIdentifier("changesPassphraseFile",
543         true);
544    changesEncryptionPassphraseFile.addLongIdentifier("changes-passphrase-file",
545         true);
546    changesEncryptionPassphraseFile.addLongIdentifier(
547         "changesEncryptionPasswordFile", true);
548    changesEncryptionPassphraseFile.addLongIdentifier(
549         "changes-encryption-password-file", true);
550    changesEncryptionPassphraseFile.addLongIdentifier("changesPasswordFile",
551         true);
552    changesEncryptionPassphraseFile.addLongIdentifier("changes-password-file",
553         true);
554    changesEncryptionPassphraseFile.addLongIdentifier(
555         "updatesEncryptionPassphraseFile", true);
556    changesEncryptionPassphraseFile.addLongIdentifier(
557         "updates-encryption-passphrase-file", true);
558    changesEncryptionPassphraseFile.addLongIdentifier(
559         "updatesPassphraseFile", true);
560    changesEncryptionPassphraseFile.addLongIdentifier(
561         "updates-passphrase-file", true);
562    changesEncryptionPassphraseFile.addLongIdentifier(
563         "updatesEncryptionPasswordFile", true);
564    changesEncryptionPassphraseFile.addLongIdentifier(
565         "updates-encryption-password-file", true);
566    changesEncryptionPassphraseFile.addLongIdentifier(
567         "updatesPasswordFile", true);
568    changesEncryptionPassphraseFile.addLongIdentifier(
569         "updates-password-file", true);
570    changesEncryptionPassphraseFile.addLongIdentifier(
571         "modificationsEncryptionPassphraseFile", true);
572    changesEncryptionPassphraseFile.addLongIdentifier(
573         "modifications-encryption-passphrase-file", true);
574    changesEncryptionPassphraseFile.addLongIdentifier(
575         "modificationsPassphraseFile", true);
576    changesEncryptionPassphraseFile.addLongIdentifier(
577         "modifications-passphrase-file", true);
578    changesEncryptionPassphraseFile.addLongIdentifier(
579         "modificationsEncryptionPasswordFile", true);
580    changesEncryptionPassphraseFile.addLongIdentifier(
581         "modifications-encryption-password-file", true);
582    changesEncryptionPassphraseFile.addLongIdentifier(
583         "modificationsPasswordFile", true);
584    changesEncryptionPassphraseFile.addLongIdentifier(
585         "modifications-password-file", true);
586    changesEncryptionPassphraseFile.addLongIdentifier(
587         "modsEncryptionPassphraseFile", true);
588    changesEncryptionPassphraseFile.addLongIdentifier(
589         "mods-encryption-passphrase-file", true);
590    changesEncryptionPassphraseFile.addLongIdentifier(
591         "modsPassphraseFile", true);
592    changesEncryptionPassphraseFile.addLongIdentifier(
593         "mods-passphrase-file", true);
594    changesEncryptionPassphraseFile.addLongIdentifier(
595         "modsEncryptionPasswordFile", true);
596    changesEncryptionPassphraseFile.addLongIdentifier(
597         "mods-encryption-password-file", true);
598    changesEncryptionPassphraseFile.addLongIdentifier(
599         "modsPasswordFile", true);
600    changesEncryptionPassphraseFile.addLongIdentifier(
601         "mods-password-file", true);
602    changesEncryptionPassphraseFile.setArgumentGroupName(
603         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
604    parser.addArgument(changesEncryptionPassphraseFile);
605
606
607    stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1,
608         INFO_LDIFMODIFY_ARG_DESC_STRIP_TRAILING_SPACES.get());
609    stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true);
610    stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true);
611    stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true);
612    stripTrailingSpaces.setArgumentGroupName(
613         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
614    parser.addArgument(stripTrailingSpaces);
615
616
617    lenientModifications = new BooleanArgument(null, "lenientModifications", 1,
618         INFO_LDIFMODIFY_ARG_DESC_LENIENT_MODIFICATIONS.get());
619    lenientModifications.addLongIdentifier("lenient-modifications", true);
620    lenientModifications.addLongIdentifier("lenientModification", true);
621    lenientModifications.addLongIdentifier("lenient-modification", true);
622    lenientModifications.addLongIdentifier("lenientMods", true);
623    lenientModifications.addLongIdentifier("lenient-mods", true);
624    lenientModifications.addLongIdentifier("lenientMod", true);
625    lenientModifications.addLongIdentifier("lenient-mod", true);
626    lenientModifications.addLongIdentifier("lenient", true);
627    lenientModifications.setArgumentGroupName(
628         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
629    lenientModifications.setHidden(true);
630    parser.addArgument(lenientModifications);
631
632
633    strictModifications = new BooleanArgument(null, "strictModifications", 1,
634         INFO_LDIFMODIFY_ARG_DESC_STRICT_MODIFICATIONS.get());
635    strictModifications.addLongIdentifier("strict-modifications", true);
636    strictModifications.addLongIdentifier("strictModification", true);
637    strictModifications.addLongIdentifier("strict-modification", true);
638    strictModifications.addLongIdentifier("strictMods", true);
639    strictModifications.addLongIdentifier("strict-mods", true);
640    strictModifications.addLongIdentifier("strictMod", true);
641    strictModifications.addLongIdentifier("strict-mod", true);
642    strictModifications.addLongIdentifier("strict", true);
643    strictModifications.setArgumentGroupName(
644         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
645    parser.addArgument(strictModifications);
646
647
648    ignoreDuplicateDeletes = new BooleanArgument(null, "ignoreDuplicateDeletes",
649         1, INFO_LDIFMODIFY_ARG_DESC_IGNORE_DUPLICATE_DELETES.get());
650    ignoreDuplicateDeletes.addLongIdentifier("ignore-duplicate-deletes", true);
651    ignoreDuplicateDeletes.addLongIdentifier("ignoreRepeatedDeletes", true);
652    ignoreDuplicateDeletes.addLongIdentifier("ignore-repeated-deletes", true);
653    ignoreDuplicateDeletes.addLongIdentifier("ignoreRepeatDeletes", true);
654    ignoreDuplicateDeletes.addLongIdentifier("ignore-repeat-deletes", true);
655    ignoreDuplicateDeletes.setArgumentGroupName(
656         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
657    parser.addArgument(ignoreDuplicateDeletes);
658
659
660    ignoreDeletesOfNonexistentEntries = new BooleanArgument(null,
661         "ignoreDeletesOfNonexistentEntries", 1,
662         INFO_LDIFMODIFY_ARG_DESC_IGNORE_NONEXISTENT_DELETES.get());
663    ignoreDeletesOfNonexistentEntries.addLongIdentifier(
664         "ignore-deletes-of-nonexistent-entries", true);
665    ignoreDeletesOfNonexistentEntries.addLongIdentifier(
666         "ignoreNonexistentDeletes", true);
667    ignoreDeletesOfNonexistentEntries.addLongIdentifier(
668         "ignore-nonexistent-deletes", true);
669    ignoreDeletesOfNonexistentEntries.setArgumentGroupName(
670         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
671    parser.addArgument(ignoreDeletesOfNonexistentEntries);
672
673
674    ignoreModifiesOfNonexistentEntries = new BooleanArgument(null,
675         "ignoreModifiesOfNonexistentEntries", 1,
676         INFO_LDIFMODIFY_ARG_DESC_IGNORE_NONEXISTENT_MODIFIES.get());
677    ignoreModifiesOfNonexistentEntries.addLongIdentifier(
678         "ignore-modifies-of-nonexistent-entries", true);
679    ignoreModifiesOfNonexistentEntries.addLongIdentifier(
680         "ignoreNonexistentModifies", true);
681    ignoreModifiesOfNonexistentEntries.addLongIdentifier(
682         "ignore-nonexistent-modifies", true);
683    ignoreModifiesOfNonexistentEntries.setArgumentGroupName(
684         INFO_LDIFMODIFY_ARG_GROUP_INPUT.get());
685    parser.addArgument(ignoreModifiesOfNonexistentEntries);
686
687
688    targetLDIF = new FileArgument('t', "targetLDIF", (targetWriter == null), 1,
689         null, INFO_LDIFMODIFY_ARG_DESC_TARGET_LDIF.get(), false, true, true,
690         false);
691    targetLDIF.addLongIdentifier("target-ldif", true);
692    targetLDIF.addLongIdentifier("targetFile", true);
693    targetLDIF.addLongIdentifier("target-file", true);
694    targetLDIF.addLongIdentifier("target", true);
695    targetLDIF.addLongIdentifier("outputLDIF", true);
696    targetLDIF.addLongIdentifier("output-ldif", true);
697    targetLDIF.addLongIdentifier("outputFile", true);
698    targetLDIF.addLongIdentifier("output-file", true);
699    targetLDIF.addLongIdentifier("output", true);
700    targetLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
701    parser.addArgument(targetLDIF);
702
703
704    compressTarget = new BooleanArgument(null, "compressTarget", 1,
705         INFO_LDIFMODIFY_ARG_DESC_COMPRESS_TARGET.get());
706    compressTarget.addLongIdentifier("compress-target", true);
707    compressTarget.addLongIdentifier("compressOutput", true);
708    compressTarget.addLongIdentifier("compress-output", true);
709    compressTarget.addLongIdentifier("compress", true);
710    compressTarget.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
711    parser.addArgument(compressTarget);
712
713
714    encryptTarget = new BooleanArgument(null, "encryptTarget", 1,
715         INFO_LDIFMODIFY_ARG_DESC_ENCRYPT_TARGET.get());
716    encryptTarget.addLongIdentifier("encrypt-target", true);
717    encryptTarget.addLongIdentifier("encryptOutput", true);
718    encryptTarget.addLongIdentifier("encrypt-output", true);
719    encryptTarget.addLongIdentifier("encrypt", true);
720    encryptTarget.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
721    parser.addArgument(encryptTarget);
722
723
724    targetEncryptionPassphraseFile = new FileArgument(null,
725         "targetEncryptionPassphraseFile", false, 1, null,
726         INFO_LDIFMODIFY_ARG_DESC_TARGET_PW_FILE.get(), true, true, true,
727         false);
728    targetEncryptionPassphraseFile.addLongIdentifier(
729         "target-encryption-passphrase-file", true);
730    targetEncryptionPassphraseFile.addLongIdentifier("targetPassphraseFile",
731         true);
732    targetEncryptionPassphraseFile.addLongIdentifier("target-passphrase-file",
733         true);
734    targetEncryptionPassphraseFile.addLongIdentifier(
735         "targetEncryptionPasswordFile", true);
736    targetEncryptionPassphraseFile.addLongIdentifier(
737         "target-encryption-password-file", true);
738    targetEncryptionPassphraseFile.addLongIdentifier("targetPasswordFile",
739         true);
740    targetEncryptionPassphraseFile.addLongIdentifier("target-password-file",
741         true);
742    targetEncryptionPassphraseFile.addLongIdentifier(
743         "outputEncryptionPassphraseFile", true);
744    targetEncryptionPassphraseFile.addLongIdentifier(
745         "output-encryption-passphrase-file", true);
746    targetEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile",
747         true);
748    targetEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file",
749         true);
750    targetEncryptionPassphraseFile.addLongIdentifier(
751         "outputEncryptionPasswordFile", true);
752    targetEncryptionPassphraseFile.addLongIdentifier(
753         "output-encryption-password-file", true);
754    targetEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile",
755         true);
756    targetEncryptionPassphraseFile.addLongIdentifier("output-password-file",
757         true);
758    targetEncryptionPassphraseFile.setArgumentGroupName(
759         INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
760
761    parser.addArgument(targetEncryptionPassphraseFile);
762
763
764    wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null,
765         INFO_LDIFMODIFY_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE);
766    wrapColumn.addLongIdentifier("wrap-column", true);
767    wrapColumn.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
768    parser.addArgument(wrapColumn);
769
770
771    doNotWrap = new BooleanArgument('T', "doNotWrap", 1,
772         INFO_LDIFMODIFY_ARG_DESC_DO_NOT_WRAP.get());
773    doNotWrap.addLongIdentifier("do-not-wrap", true);
774    doNotWrap.addLongIdentifier("dontWrap", true);
775    doNotWrap.addLongIdentifier("dont-wrap", true);
776    doNotWrap.addLongIdentifier("noWrap", true);
777    doNotWrap.addLongIdentifier("no-wrap", true);
778    doNotWrap.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
779    parser.addArgument(doNotWrap);
780
781
782    suppressComments = new BooleanArgument(null, "suppressComments", 1,
783         INFO_LDIFMODIFY_ARG_DESC_SUPPRESS_COMMENTS.get());
784    suppressComments.addLongIdentifier("suppress-comments", true);
785    suppressComments.addLongIdentifier("excludeComments", true);
786    suppressComments.addLongIdentifier("exclude-comments", true);
787    suppressComments.addLongIdentifier("noComments", true);
788    suppressComments.addLongIdentifier("no-comments", true);
789    suppressComments.setArgumentGroupName(
790         INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get());
791    parser.addArgument(suppressComments);
792
793
794    noSchemaCheck = new BooleanArgument(null, "noSchemaCheck", 1,
795         INFO_LDIFMODIFY_ARG_DESC_NO_SCHEMA_CHECK.get());
796    noSchemaCheck.addLongIdentifier("no-schema-check", true);
797    noSchemaCheck.setHidden(true);
798    parser.addArgument(noSchemaCheck);
799
800
801    parser.addExclusiveArgumentSet(lenientModifications, strictModifications);
802
803    parser.addExclusiveArgumentSet(wrapColumn, doNotWrap);
804
805    parser.addDependentArgumentSet(targetEncryptionPassphraseFile,
806         encryptTarget);
807  }
808
809
810
811  /**
812   * {@inheritDoc}
813   */
814  @Override()
815  @NotNull()
816  public ResultCode doToolProcessing()
817  {
818    // Read all of the changes into memory.
819    final Map<DN,List<LDIFChangeRecord>> addAndSubsequentChangeRecords =
820         new TreeMap<>();
821    final Map<DN,Boolean> deletedEntryDNs = new TreeMap<>();
822    final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords =
823         new HashMap<>();
824    final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>>
825         modifyDNAndSubsequentChangeRecords = new TreeMap<>();
826    final AtomicReference<ResultCode> resultCode = new AtomicReference<>();
827    try
828    {
829      readChangeRecords(addAndSubsequentChangeRecords, deletedEntryDNs,
830           modifyChangeRecords, modifyDNAndSubsequentChangeRecords, resultCode);
831    }
832    catch (final LDAPException e)
833    {
834      Debug.debugException(e);
835      logCompletionMessage(true, e.getMessage());
836      resultCode.compareAndSet(null, e.getResultCode());
837      return resultCode.get();
838    }
839
840
841    boolean changesIgnored = false;
842    LDIFReader ldifReader = null;
843    LDIFWriter ldifWriter = null;
844    final AtomicLong entriesRead = new AtomicLong(0L);
845    final AtomicLong entriesUpdated = new AtomicLong(0L);
846    try
847    {
848      // Open the source LDIF file for reading.
849      try
850      {
851        ldifReader = getLDIFReader(sourceReader, sourceLDIF.getValue(),
852             sourceEncryptionPassphraseFile.getValue());
853      }
854      catch (final LDAPException e)
855      {
856        Debug.debugException(e);
857        logCompletionMessage(true, e.getMessage());
858        return e.getResultCode();
859      }
860
861
862      // Open the target LDIF file for writing.
863      try
864      {
865        ldifWriter = getLDIFWriter(targetWriter);
866      }
867      catch (final LDAPException e)
868      {
869        Debug.debugException(e);
870        logCompletionMessage(true, e.getMessage());
871        return e.getResultCode();
872      }
873
874
875      // Iterate through the source LDIF file and apply changes as appropriate.
876      final StringBuilder comment = new StringBuilder();
877      while (true)
878      {
879        final LDIFRecord sourceRecord;
880        try
881        {
882          sourceRecord = ldifReader.readLDIFRecord();
883        }
884        catch (final LDIFException e)
885        {
886          Debug.debugException(e);
887
888          if (e.mayContinueReading())
889          {
890            resultCode.compareAndSet(null, ResultCode.DECODING_ERROR);
891            wrapErr(ERR_LDIFMODIFY_RECOVERABLE_DECODE_ERROR.get(
892                 sourceLDIF.getValue(), StaticUtils.getExceptionMessage(e)));
893            continue;
894          }
895          else
896          {
897            logCompletionMessage(true,
898                 ERR_LDIFMODIFY_UNRECOVERABLE_DECODE_ERROR.get(
899                      sourceLDIF.getValue(),
900                      StaticUtils.getExceptionMessage(e)));
901            return ResultCode.DECODING_ERROR;
902          }
903        }
904        catch (final IOException e)
905        {
906          Debug.debugException(e);
907          logCompletionMessage(true,
908               ERR_LDIFMODIFY_READ_ERROR.get(sourceLDIF.getValue(),
909                    StaticUtils.getExceptionMessage(e)));
910          return ResultCode.LOCAL_ERROR;
911        }
912
913
914        // If the record we read was null, then we've hit the end of the source
915        // content.
916        if (sourceRecord == null)
917        {
918          break;
919        }
920
921
922        // If the record we read was an entry, then apply changes to it.  If it
923        // was not, then that's an error.
924        comment.setLength(0);
925
926        final LDIFRecord targetRecord;
927        if (sourceRecord instanceof Entry)
928        {
929          entriesRead.incrementAndGet();
930          targetRecord = updateEntry((Entry) sourceRecord,
931               addAndSubsequentChangeRecords, deletedEntryDNs,
932               modifyChangeRecords, modifyDNAndSubsequentChangeRecords, comment,
933               resultCode, entriesUpdated);
934        }
935        else
936        {
937          targetRecord = sourceRecord;
938          // NOTE:  We're using false for the isError flag in this case because
939          // a better error will be recorded by the createChangeRecordComment
940          // call below.
941          appendComment(comment,
942               ERR_LDIFMODIFY_COMMENT_SOURCE_RECORD_NOT_ENTRY.get(), false);
943
944          final StringBuilder msgBuffer = new StringBuilder();
945          createChangeRecordComment(msgBuffer,
946               ERR_LDIFMODIFY_OUTPUT_SOURCE_RECORD_NOT_ENTRY.get(
947                    sourceLDIF.getValue().getAbsolutePath()),
948               sourceRecord, true);
949          wrapErr(msgBuffer.toString());
950          resultCode.compareAndSet(null, ResultCode.DECODING_ERROR);
951        }
952
953
954        // Write the potentially updated entry to the target LDIF file.  If the
955        // target record is null, then that means the entry has been deleted,
956        // but we still may want to write a comment about the deleted entry to
957        // the target file.
958        try
959        {
960          if (targetRecord == null)
961          {
962            if ((comment.length() > 0) && (! suppressComments.isPresent()))
963            {
964              writeLDIFComment(ldifWriter, comment, false);
965            }
966          }
967          else
968          {
969            writeLDIFRecord(ldifWriter, targetRecord, comment);
970          }
971        }
972        catch (final IOException e)
973        {
974          Debug.debugException(e);
975          logCompletionMessage(true,
976               ERR_LDIFMODIFY_WRITE_ERROR.get(targetLDIF.getValue(),
977                    StaticUtils.getExceptionMessage(e)));
978          return ResultCode.LOCAL_ERROR;
979        }
980      }
981
982
983      try
984      {
985        // If there are any remaining add records, then process them.
986        final AtomicBoolean isUpdated = new AtomicBoolean();
987        for (final List<LDIFChangeRecord> records :
988             addAndSubsequentChangeRecords.values())
989        {
990          final Iterator<LDIFChangeRecord> iterator = records.iterator();
991          final LDIFAddChangeRecord addChangeRecord =
992               (LDIFAddChangeRecord) iterator.next();
993          Entry entry = addChangeRecord.getEntryToAdd();
994          comment.setLength(0);
995          if (iterator.hasNext())
996          {
997            createChangeRecordComment(comment,
998                 INFO_LDIFMODIFY_ADDING_ENTRY_WITH_MODS.get(), addChangeRecord,
999                 false);
1000            while (iterator.hasNext())
1001            {
1002              entry = applyModification(entry,
1003                   (LDIFModifyChangeRecord) iterator.next(), isUpdated,
1004                   resultCode, comment);
1005            }
1006          }
1007          else
1008          {
1009            appendComment(comment,
1010                 INFO_LDIFMODIFY_ADDING_ENTRY_NO_MODS.get(), false);
1011          }
1012
1013          writeLDIFRecord(ldifWriter, entry, comment);
1014          entriesUpdated.incrementAndGet();
1015        }
1016
1017
1018        // If there are any remaining DNs to delete, then those entries must not
1019        // have been in the source LDIF.
1020        for (final Map.Entry<DN,Boolean> e : deletedEntryDNs.entrySet())
1021        {
1022          if (e.getValue() == Boolean.FALSE)
1023          {
1024            if (ignoreDeletesOfNonexistentEntries.isPresent())
1025            {
1026              changesIgnored = true;
1027            }
1028            else
1029            {
1030              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1031              writeLDIFComment(ldifWriter,
1032                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_DELETE.get(
1033                        e.getKey().toString()),
1034                   true);
1035            }
1036          }
1037        }
1038
1039
1040        // If there are any remaining modify change records, then those entries
1041        // must not have been in the source LDIF.
1042        for (final List<LDIFModifyChangeRecord> l :
1043             modifyChangeRecords.values())
1044        {
1045          for (final LDIFChangeRecord r : l)
1046          {
1047            if (ignoreModifiesOfNonexistentEntries.isPresent())
1048            {
1049              changesIgnored = true;
1050            }
1051            else
1052            {
1053              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1054              comment.setLength(0);
1055              createChangeRecordComment(comment,
1056                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true);
1057              writeLDIFComment(ldifWriter, comment, false);
1058            }
1059          }
1060        }
1061
1062
1063        // If there are any remaining modify DN change records, then those
1064        // entries must not have been in the source LDIF.
1065        for (final ObjectPair<DN,List<LDIFChangeRecord>> l :
1066             modifyDNAndSubsequentChangeRecords.values())
1067        {
1068          for (final LDIFChangeRecord r : l.getSecond())
1069          {
1070            resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1071            comment.setLength(0);
1072            if (r instanceof LDIFModifyDNChangeRecord)
1073            {
1074              createChangeRecordComment(comment,
1075                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_RENAME.get(), r, true);
1076            }
1077            else
1078            {
1079              createChangeRecordComment(comment,
1080                   ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true);
1081            }
1082            writeLDIFComment(ldifWriter, comment, false);
1083          }
1084        }
1085      }
1086      catch (final IOException e)
1087      {
1088        Debug.debugException(e);
1089        logCompletionMessage(true,
1090             ERR_LDIFMODIFY_WRITE_ERROR.get(
1091                  targetLDIF.getValue().getAbsolutePath(),
1092                  StaticUtils.getExceptionMessage(e)));
1093        return ResultCode.LOCAL_ERROR;
1094      }
1095    }
1096    finally
1097    {
1098      if (ldifReader != null)
1099      {
1100        try
1101        {
1102          ldifReader.close();
1103        }
1104        catch (final Exception e)
1105        {
1106          Debug.debugException(e);
1107          resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
1108          logCompletionMessage(true,
1109               ERR_LDIFMODIFY_ERROR_CLOSING_READER.get(
1110                    sourceLDIF.getValue().getAbsolutePath(),
1111                    StaticUtils.getExceptionMessage(e)));
1112        }
1113      }
1114
1115      if (ldifWriter != null)
1116      {
1117        try
1118        {
1119          ldifWriter.close();
1120        }
1121        catch (final Exception e)
1122        {
1123          Debug.debugException(e);
1124          resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
1125          logCompletionMessage(true,
1126               ERR_LDIFMODIFY_ERROR_CLOSING_WRITER.get(
1127                    sourceLDIF.getValue().getAbsolutePath(),
1128                    StaticUtils.getExceptionMessage(e)));
1129        }
1130      }
1131    }
1132
1133
1134    // If no entries were read and no updates were applied, then we'll consider
1135    // that an error, regardless of whether a read error was encountered.
1136    if ((entriesRead.get() == 0L) && (entriesUpdated.get() == 0L))
1137    {
1138      if (resultCode.get() == null)
1139      {
1140        logCompletionMessage(true,
1141             ERR_LDIFMODIFY_NO_SOURCE_ENTRIES.get(
1142                  sourceLDIF.getValue().getAbsolutePath()));
1143        return ResultCode.PARAM_ERROR;
1144      }
1145      else
1146      {
1147        logCompletionMessage(true,
1148             ERR_LDIFMODIFY_COULD_NOT_READ_SOURCE_ENTRIES.get(
1149                  sourceLDIF.getValue().getAbsolutePath()));
1150        return resultCode.get();
1151      }
1152    }
1153
1154
1155    // If no entries were updated, then we'll also consider that an error.
1156    if ((entriesUpdated.get() == 0L) && (! changesIgnored))
1157    {
1158      logCompletionMessage(true,
1159           ERR_LDIFMODIFY_NO_CHANGES_APPLIED_WITH_ERRORS.get(
1160                changesLDIF.getValue().getAbsolutePath(),
1161                sourceLDIF.getValue().getAbsolutePath()));
1162      resultCode.compareAndSet(null, ResultCode.PARAM_ERROR);
1163      return resultCode.get();
1164    }
1165
1166
1167    // Create the final completion message that will be used.
1168    final long entriesNotUpdated =
1169         Math.max((entriesRead.get() - entriesUpdated.get()), 0);
1170    if (resultCode.get() == null)
1171    {
1172      logCompletionMessage(false,
1173           INFO_LDIFMODIFY_COMPLETED_SUCCESSFULLY.get(entriesRead.get(),
1174                entriesUpdated.get(), entriesNotUpdated));
1175      return ResultCode.SUCCESS;
1176    }
1177    else
1178    {
1179      logCompletionMessage(true,
1180           ERR_LDIFMODIFY_COMPLETED_WITH_ERRORS.get(entriesRead.get(),
1181                entriesUpdated.get(), entriesNotUpdated));
1182      return resultCode.get();
1183    }
1184  }
1185
1186
1187
1188  /**
1189   * Reads all of the LDIF change records from the changes file into a list.
1190   *
1191   * @param  addAndSubsequentChangeRecords
1192   *              A map that will be updated with add change records for a given
1193   *              entry, along with any subsequent change records that apply to
1194   *              the entry after it has been added.  It must not be
1195   *              {@code null}, must be empty, and must be updatable.
1196   * @param  deletedEntryDNs
1197   *              A map that will be updated with the DNs of any entries that
1198   *              are targeted by delete modifications and that have not been
1199   *              previously added or renamed.  It must not be {@code null},
1200   *              must be empty, and must be updatable.
1201   * @param  modifyChangeRecords
1202   *              A map that will be updated with any modify change records
1203   *              that target an entry that has not been targeted by any other
1204   *              type of change.  It must not be {@code null}, must be empty,
1205   *              and must be updatable.
1206   * @param  modifyDNAndSubsequentChangeRecords
1207   *              A map that will be updated with any change records for modify
1208   *              DN operations that target a given entry, and any subsequent
1209   *              operations that target the entry with its new DN.  It must not
1210   *              be {@code null}, must be empty, and must be updatable.
1211   * @param  resultCode
1212   *              A reference to the final result code that should be used for
1213   *              the tool.  This may be updated if an error occurred during
1214   *              processing and no value is already set.  It must not be
1215   *              {@code null}, but is allowed to have no value assigned.
1216   *
1217   * @throws  LDAPException  If an unrecoverable error occurs during processing.
1218   */
1219  private void readChangeRecords(
1220       @NotNull final Map<DN,List<LDIFChangeRecord>>
1221            addAndSubsequentChangeRecords,
1222       @NotNull final Map<DN,Boolean> deletedEntryDNs,
1223       @NotNull final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords,
1224       @NotNull final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>>
1225            modifyDNAndSubsequentChangeRecords,
1226       @NotNull final AtomicReference<ResultCode> resultCode)
1227       throws LDAPException
1228  {
1229    LDIFException firstRecoverableException = null;
1230    try (LDIFReader ldifReader = getLDIFReader(changesReader,
1231         changesLDIF.getValue(), changesEncryptionPassphraseFile.getValue()))
1232    {
1233changeRecordLoop:
1234      while (true)
1235      {
1236        // Read the next record from the changes file.
1237        final LDIFRecord ldifRecord;
1238        try
1239        {
1240          ldifRecord = ldifReader.readLDIFRecord();
1241        }
1242        catch (final LDIFException e)
1243        {
1244          Debug.debugException(e);
1245
1246          if (e.mayContinueReading())
1247          {
1248            if (firstRecoverableException == null)
1249            {
1250              firstRecoverableException = e;
1251            }
1252
1253            err();
1254            wrapErr(ERR_LDIFMODIFY_CANNOT_READ_RECORD_CAN_CONTINUE.get(
1255                 changesLDIF.getValue().getAbsolutePath(),
1256                 StaticUtils.getExceptionMessage(e)));
1257            resultCode.compareAndSet(null, ResultCode.DECODING_ERROR);
1258            continue changeRecordLoop;
1259          }
1260          else
1261          {
1262            throw new LDAPException(ResultCode.DECODING_ERROR,
1263                 ERR_LDIFMODIFY_CANNOT_READ_RECORD_CANNOT_CONTINUE.get(
1264                      changesLDIF.getValue().getAbsolutePath(),
1265                      StaticUtils.getExceptionMessage(e)),
1266                 e);
1267          }
1268        }
1269
1270        if (ldifRecord == null)
1271        {
1272          break;
1273        }
1274
1275
1276        // Make sure that we can parse the DN for the change record.  If not,
1277        // then that's an error.
1278        final DN parsedDN;
1279        try
1280        {
1281          parsedDN = ldifRecord.getParsedDN();
1282        }
1283        catch (final LDAPException e)
1284        {
1285          Debug.debugException(e);
1286
1287          err();
1288          wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_CHANGE_RECORD_DN.get(
1289               String.valueOf(ldifRecord),
1290               changesLDIF.getValue().getAbsolutePath(), e.getMessage()));
1291          resultCode.compareAndSet(null, e.getResultCode());
1292          continue changeRecordLoop;
1293        }
1294
1295
1296        // Get the LDIF record as a change record.  If the record is an entry
1297        // rather than a change record, then we'll treat it as an add change
1298        // record.
1299        final LDIFChangeRecord changeRecord;
1300        if (ldifRecord instanceof Entry)
1301        {
1302          changeRecord = new LDIFAddChangeRecord((Entry) ldifRecord);
1303        }
1304        else
1305        {
1306          changeRecord = (LDIFChangeRecord) ldifRecord;
1307        }
1308
1309
1310        // If the change record is for a modify DN, then make sure that we can
1311        // parse the new DN.
1312        final DN parsedNewDN;
1313        if (changeRecord.getChangeType() == ChangeType.MODIFY_DN)
1314        {
1315          try
1316          {
1317            parsedNewDN = ((LDIFModifyDNChangeRecord) changeRecord).getNewDN();
1318          }
1319          catch (final LDAPException e)
1320          {
1321            Debug.debugException(e);
1322
1323            err();
1324            wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_NEW_DN.get(
1325                 String.valueOf(changeRecord),
1326                 changesLDIF.getValue().getAbsolutePath(), e.getMessage()));
1327            resultCode.compareAndSet(null, e.getResultCode());
1328            continue changeRecordLoop;
1329          }
1330        }
1331        else
1332        {
1333          parsedNewDN = parsedDN;
1334        }
1335
1336
1337        // Look at the change type and determine how to handle the operation.
1338        switch (changeRecord.getChangeType())
1339        {
1340          case ADD:
1341            // Make sure that we haven't already seen an add for an entry with
1342            // the same DN (unless that add was subsequently deleted).
1343            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
1344            {
1345              err();
1346              wrapErr(ERR_LDIFMODIFY_MULTIPLE_ADDS_FOR_DN.get(
1347                   changesLDIF.getValue().getAbsolutePath(),
1348                   parsedDN.toString()));
1349              resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
1350              continue changeRecordLoop;
1351            }
1352
1353            // Make sure that there are no modifies targeting an entry with the
1354            // same DN.
1355            if (modifyChangeRecords.containsKey(parsedDN))
1356            {
1357              err();
1358              wrapErr(ERR_LDIFMODIFY_ADD_TARGETS_MODIFIED_ENTRY.get(
1359                   changesLDIF.getValue().getAbsolutePath(),
1360                   parsedDN.toString()));
1361              resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
1362              continue changeRecordLoop;
1363            }
1364
1365            // Make sure that there aren't any modify DN operations that will
1366            // create an entry with the same or a subordinate DN.
1367            for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e :
1368                 modifyDNAndSubsequentChangeRecords.entrySet())
1369            {
1370              final DN newDN = e.getValue().getFirst();
1371              if (parsedDN.isAncestorOf(newDN, true))
1372              {
1373                err();
1374                wrapErr(ERR_LDIFMODIFY_ADD_CONFLICTS_WITH_MOD_DN.get(
1375                     changesLDIF.getValue().getAbsolutePath(),
1376                     parsedDN.toString(), e.getKey().toString(),
1377                     newDN.toString()));
1378                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
1379                continue changeRecordLoop;
1380              }
1381            }
1382
1383            final List<LDIFChangeRecord> addList = new ArrayList<>();
1384            addList.add(changeRecord);
1385            addAndSubsequentChangeRecords.put(parsedDN, addList);
1386            break;
1387
1388
1389          case DELETE:
1390            // If the set of changes already included an add for this entry,
1391            // then remove that add and any subsequent changes for it.  This
1392            // isn't an error, so we don't need to set a result code.
1393            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
1394            {
1395              addAndSubsequentChangeRecords.remove(parsedDN);
1396              err();
1397              wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUS_ADD.get(
1398                   changesLDIF.getValue().getAbsolutePath(),
1399                   parsedDN.toString()));
1400              continue changeRecordLoop;
1401            }
1402
1403            // If the set of changes already included a modify DN that targeted
1404            // the entry, then reject the change.
1405            if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN))
1406            {
1407              final DN newDN =
1408                   modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst();
1409
1410              err();
1411              wrapErr(ERR_LDIFMODIFY_DELETE_OF_PREVIOUS_RENAME.get(
1412                   changesLDIF.getValue().getAbsolutePath(),
1413                   parsedDN.toString(), newDN.toString()));
1414              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1415              continue changeRecordLoop;
1416            }
1417
1418            // If the set of changes already included a modify DN whose new DN
1419            // equals or is subordinate to the DN for the delete change
1420            // record, then remove that modify DN operation and any subsequent
1421            // changes for it, and instead add a delete for the original DN.
1422            // This isn't an error, so we don't need to set a result code.
1423            final Iterator<Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>>>
1424                 deleteModDNIterator =
1425                 modifyDNAndSubsequentChangeRecords.entrySet().iterator();
1426            while (deleteModDNIterator.hasNext())
1427            {
1428              final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e =
1429                   deleteModDNIterator.next();
1430              final DN newDN = e.getValue().getFirst();
1431              if (parsedDN.isAncestorOf(newDN, true))
1432              {
1433                final DN originalDN = e.getKey();
1434                deleteModDNIterator.remove();
1435                deletedEntryDNs.put(originalDN, Boolean.FALSE);
1436
1437                err();
1438                wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_RENAMED.get(
1439                     changesLDIF.getValue().getAbsolutePath(),
1440                     parsedDN.toString(), originalDN.toString(),
1441                     newDN.toString()));
1442                continue changeRecordLoop;
1443              }
1444            }
1445
1446            // If the set of changes already included a delete for the same
1447            // DN, then reject the new change.
1448            if (deletedEntryDNs.containsKey(parsedDN))
1449            {
1450              if (! ignoreDuplicateDeletes.isPresent())
1451              {
1452                err();
1453                wrapErr(ERR_LDIFMODIFY_MULTIPLE_DELETES_FOR_DN.get(
1454                     changesLDIF.getValue().getAbsolutePath(),
1455                     parsedDN.toString()));
1456                resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1457              }
1458              continue changeRecordLoop;
1459            }
1460
1461            // If the set of changes included any modifications for the same DN,
1462            // then remove those modifications.  This isn't an error, so we
1463            // don't need to set a result code.
1464            if (modifyChangeRecords.containsKey(parsedDN))
1465            {
1466              modifyChangeRecords.remove(parsedDN);
1467              err();
1468              wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_MODIFIED.get(
1469                   changesLDIF.getValue().getAbsolutePath(),
1470                   parsedDN.toString()));
1471            }
1472
1473            deletedEntryDNs.put(parsedDN, Boolean.FALSE);
1474            break;
1475
1476
1477          case MODIFY:
1478            // If the set of changes already included an add for an entry with
1479            // the same DN, then add the modify change record to the set of
1480            // changes following that add.
1481            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
1482            {
1483              addAndSubsequentChangeRecords.get(parsedDN).add(changeRecord);
1484              continue changeRecordLoop;
1485            }
1486
1487            // If the set of changes already included a modify DN for an entry
1488            // with the same DN, then reject the new change.
1489            if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN))
1490            {
1491              final DN newDN =
1492                   modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst();
1493
1494              err();
1495              wrapErr(ERR_LDIFMODIFY_MODIFY_OF_RENAMED_ENTRY.get(
1496                   changesLDIF.getValue().getAbsolutePath(),
1497                   parsedDN.toString(), newDN.toString()));
1498              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1499              continue changeRecordLoop;
1500            }
1501
1502            // If the set of changes already included a modify DN that would
1503            // result in an entry with the same DN as the modify, then add
1504            // the modify change record to the modify DN record's change list.
1505            for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e :
1506                 modifyDNAndSubsequentChangeRecords.entrySet())
1507            {
1508              if (parsedDN.equals(e.getValue().getFirst()))
1509              {
1510                e.getValue().getSecond().add(changeRecord);
1511                continue changeRecordLoop;
1512              }
1513            }
1514
1515            // If the set of changes already included a delete for an entry with
1516            // the same DN, then reject the new change.
1517            if (deletedEntryDNs.containsKey(parsedDN))
1518            {
1519              err();
1520              wrapErr(ERR_LDIFMODIFY_MODIFY_OF_DELETED_ENTRY.get(
1521                   changesLDIF.getValue().getAbsolutePath(),
1522                   parsedDN.toString()));
1523              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1524              continue changeRecordLoop;
1525            }
1526
1527            // If the set of changes already included a modify for an entry with
1528            // the same DN, then add the new change to that list.
1529            if (modifyChangeRecords.containsKey(parsedDN))
1530            {
1531              modifyChangeRecords.get(parsedDN).add(
1532                   (LDIFModifyChangeRecord) changeRecord);
1533              continue changeRecordLoop;
1534            }
1535
1536            // Start a new change record list for the modify operation.
1537            final List<LDIFModifyChangeRecord> modList = new ArrayList<>();
1538            modList.add((LDIFModifyChangeRecord) changeRecord);
1539            modifyChangeRecords.put(parsedDN, modList);
1540            break;
1541
1542
1543          case MODIFY_DN:
1544            // If the set of changes already included an add for an entry with
1545            // the same DN, then reject the modify DN.
1546            if (addAndSubsequentChangeRecords.containsKey(parsedDN))
1547            {
1548              err();
1549              wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_ADDED_ENTRY.get(
1550                   changesLDIF.getValue().getAbsolutePath(),
1551                   parsedDN.toString()));
1552              resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM);
1553              continue changeRecordLoop;
1554            }
1555
1556            // If the set of changes already included an add for an entry with
1557            // an entry at or below the new DN, then reject the modify DN.
1558            for (final DN addedDN : addAndSubsequentChangeRecords.keySet())
1559            {
1560              if (addedDN.isDescendantOf(parsedNewDN, true))
1561              {
1562                err();
1563                wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_ADD.get(
1564                     changesLDIF.getValue().getAbsolutePath(),
1565                     parsedDN.toString(), parsedNewDN.toString(),
1566                     addedDN.toString()));
1567                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
1568                continue changeRecordLoop;
1569              }
1570            }
1571
1572            // If the set of changes already included a modify DN for an entry
1573            // with the same DN, then reject the modify DN.
1574            if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN))
1575            {
1576              err();
1577              wrapErr(ERR_LDIFMODIFY_MULTIPLE_MOD_DN_WITH_DN.get(
1578                   changesLDIF.getValue().getAbsolutePath(),
1579                   parsedDN.toString()));
1580              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1581              continue changeRecordLoop;
1582            }
1583
1584            // If the set of changes already included a modify DN for an entry
1585            // that set a new DN that matches the DN of the new record, then
1586            // reject the modify DN.
1587            for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e :
1588                 modifyDNAndSubsequentChangeRecords.entrySet())
1589            {
1590              final DN newDN = e.getValue().getFirst();
1591              if (newDN.isDescendantOf(parsedDN, true))
1592              {
1593                err();
1594                wrapErr(
1595                     ERR_LDIFMODIFY_UNWILLING_TO_MODIFY_DN_MULTIPLE_TIMES.get(
1596                          changesLDIF.getValue().getAbsolutePath(),
1597                          parsedDN.toString(), parsedNewDN.toString(),
1598                          e.getKey().toString()));
1599                resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM);
1600                continue changeRecordLoop;
1601              }
1602            }
1603
1604            // If the set of changes already included a modify DN that set a
1605            // new DN that is at or below the new DN, then reject the modify DN.
1606            for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e :
1607                 modifyDNAndSubsequentChangeRecords.entrySet())
1608            {
1609              final DN newDN = e.getValue().getFirst();
1610              if (newDN.isDescendantOf(parsedNewDN, true))
1611              {
1612                err();
1613                wrapErr(ERR_LDIFMODIFY_MOD_DN_CONFLICTS_WITH_MOD_DN.get(
1614                     changesLDIF.getValue().getAbsolutePath(),
1615                     parsedDN.toString(), parsedNewDN.toString(),
1616                     e.getKey().toString(), newDN.toString()));
1617                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
1618                continue changeRecordLoop;
1619              }
1620            }
1621
1622            // If the set of changes already included a delete for an entry with
1623            //t he same DN, then reject the modify DN.
1624            if (deletedEntryDNs.containsKey(parsedDN))
1625            {
1626              err();
1627              wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_DELETED_ENTRY.get(
1628                   changesLDIF.getValue().getAbsolutePath(),
1629                   parsedDN.toString()));
1630              resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT);
1631              continue changeRecordLoop;
1632            }
1633
1634            // If the set of changes already included a modify for an entry that
1635            // is at or below the new DN, then reject the modify DN.
1636            for (final DN dn : modifyChangeRecords.keySet())
1637            {
1638              if (dn.isDescendantOf(parsedNewDN, true))
1639              {
1640                err();
1641                wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_MOD.get(
1642                     changesLDIF.getValue().getAbsolutePath(),
1643                     parsedDN.toString(), parsedNewDN.toString(),
1644                     dn.toString()));
1645                resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
1646                continue changeRecordLoop;
1647              }
1648            }
1649
1650            final List<LDIFChangeRecord> modDNList = new ArrayList<>();
1651            modDNList.add(changeRecord);
1652            modifyDNAndSubsequentChangeRecords.put(parsedDN,
1653                 new ObjectPair<DN,List<LDIFChangeRecord>>(parsedNewDN,
1654                      modDNList));
1655            break;
1656        }
1657      }
1658    }
1659    catch (final LDAPException e)
1660    {
1661      Debug.debugException(e);
1662      throw new LDAPException(e.getResultCode(),
1663           ERR_LDIFMODIFY_ERROR_OPENING_CHANGES_FILE.get(
1664                changesLDIF.getValue().getAbsolutePath(), e.getMessage()),
1665           e);
1666    }
1667    catch (final IOException e)
1668    {
1669      Debug.debugException(e);
1670      throw new LDAPException(ResultCode.LOCAL_ERROR,
1671           ERR_LDIFMODIFY_ERROR_READING_CHANGES_FILE.get(
1672                changesLDIF.getValue().getAbsolutePath(),
1673                StaticUtils.getExceptionMessage(e)),
1674           e);
1675    }
1676
1677    if (addAndSubsequentChangeRecords.isEmpty() && deletedEntryDNs.isEmpty() &&
1678         modifyChangeRecords.isEmpty() &&
1679         modifyDNAndSubsequentChangeRecords.isEmpty())
1680    {
1681      if (firstRecoverableException == null)
1682      {
1683        throw new LDAPException(ResultCode.PARAM_ERROR,
1684             ERR_LDIFMODIFY_NO_CHANGES.get(
1685                  changesLDIF.getValue().getAbsolutePath()));
1686      }
1687      else
1688      {
1689        throw new LDAPException(ResultCode.PARAM_ERROR,
1690             ERR_LDIFMODIFY_NO_CHANGES_WITH_ERROR.get(
1691                  changesLDIF.getValue().getAbsolutePath()),
1692             firstRecoverableException);
1693      }
1694    }
1695  }
1696
1697
1698
1699  /**
1700   * Retrieves an LDIF reader that may be used to read LDIF records (either
1701   * entries or change records) from the specified LDIF file.
1702   *
1703   * @param  existingReader  An LDIF reader that was already provided to the
1704   *                         tool for this purpose.  It may be {@code null} if
1705   *                         the LDIF reader should be created with the given
1706   *                         LDIF file and passphrase file.
1707   * @param  ldifFile        The LDIF file for which to create the reader.  It
1708   *                         may be {@code null} only if {@code existingReader}
1709   *                         is non-{@code null}.
1710   * @param  passphraseFile  The file containing the encryption passphrase
1711   *                         needed to decrypt the contents of the provided LDIF
1712   *                         file.  It may be {@code null} if the LDIF file is
1713   *                         not encrypted or if the user should be
1714   *                         interactively prompted for the passphrase.
1715   *
1716   * @return  The LDIF reader that was created.
1717   *
1718   * @throws  LDAPException  If a problem occurs while creating the LDIF reader.
1719   */
1720  @NotNull()
1721  private LDIFReader getLDIFReader(@Nullable final LDIFReader existingReader,
1722                                   @Nullable final File ldifFile,
1723                                   @Nullable final File passphraseFile)
1724          throws LDAPException
1725  {
1726    if (existingReader != null)
1727    {
1728      return existingReader;
1729    }
1730
1731    if (passphraseFile != null)
1732    {
1733      readPassphraseFile(passphraseFile);
1734    }
1735
1736
1737    boolean closeStream = true;
1738    InputStream inputStream = null;
1739    try
1740    {
1741      inputStream = new FileInputStream(ldifFile);
1742
1743      final ObjectPair<InputStream,char[]> p =
1744           ToolUtils.getPossiblyPassphraseEncryptedInputStream(
1745                inputStream, inputEncryptionPassphrases,
1746                (passphraseFile != null),
1747                INFO_LDIFMODIFY_ENTER_INPUT_ENCRYPTION_PW.get(
1748                     ldifFile.getName()),
1749                ERR_LDIFMODIFY_WRONG_ENCRYPTION_PW.get(), getOut(), getErr());
1750      inputStream = p.getFirst();
1751      addPassphrase(p.getSecond());
1752
1753      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
1754
1755      final LDIFReader ldifReader = new LDIFReader(inputStream);
1756      if (stripTrailingSpaces.isPresent())
1757      {
1758        ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP);
1759      }
1760      else
1761      {
1762        ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT);
1763      }
1764
1765      ldifReader.setSchema(Schema.getDefaultStandardSchema());
1766
1767      closeStream = false;
1768      return ldifReader;
1769    }
1770    catch (final Exception e)
1771    {
1772      Debug.debugException(e);
1773      throw new LDAPException(ResultCode.LOCAL_ERROR,
1774           ERR_LDIFMODIFY_ERROR_OPENING_INPUT_FILE.get(
1775                ldifFile.getAbsolutePath(),
1776                StaticUtils.getExceptionMessage(e)),
1777           e);
1778    }
1779    finally
1780    {
1781      if ((inputStream != null) && closeStream)
1782      {
1783        try
1784        {
1785          inputStream.close();
1786        }
1787        catch (final Exception e)
1788        {
1789          Debug.debugException(e);
1790        }
1791      }
1792    }
1793  }
1794
1795
1796
1797  /**
1798   * Reads the contents of the specified passphrase file and adds it to the list
1799   * of passphrases.
1800   *
1801   * @param  f  The passphrase file to read.
1802   *
1803   * @throws  LDAPException  If a problem is encountered while trying to read
1804   *                         the passphrase from the provided file.
1805   */
1806  private void readPassphraseFile(@NotNull final File f)
1807          throws LDAPException
1808  {
1809    try
1810    {
1811      addPassphrase(getPasswordFileReader().readPassword(f));
1812    }
1813    catch (final Exception e)
1814    {
1815      Debug.debugException(e);
1816      throw new LDAPException(ResultCode.LOCAL_ERROR,
1817           ERR_LDIFMODIFY_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(),
1818                StaticUtils.getExceptionMessage(e)),
1819           e);
1820    }
1821  }
1822
1823
1824
1825  /**
1826   * Updates the list of encryption passphrases with the provided passphrase, if
1827   * it is not already present.
1828   *
1829   * @param  passphrase  The passphrase to be added.  It may optionally be
1830   *                     {@code null} (in which case no action will be taken).
1831   */
1832  private void addPassphrase(@Nullable final char[] passphrase)
1833  {
1834    if (passphrase == null)
1835    {
1836      return;
1837    }
1838
1839    for (final char[] existingPassphrase : inputEncryptionPassphrases)
1840    {
1841      if (Arrays.equals(existingPassphrase, passphrase))
1842      {
1843        return;
1844      }
1845    }
1846
1847    inputEncryptionPassphrases.add(passphrase);
1848  }
1849
1850
1851
1852  /**
1853   * Creates the LDIF writer to use to write the output.
1854   *
1855   * @param  existingWriter  An LDIF writer that was already provided to the
1856   *                         tool for this purpose.  It may be {@code null} if
1857   *                         the LDIF writer should be created using the
1858   *                         provided arguments.
1859   *
1860   * @return  The LDIF writer that was created.
1861   *
1862   * @throws  LDAPException  If a problem occurs while creating the LDIF writer.
1863   */
1864  @NotNull()
1865  private LDIFWriter getLDIFWriter(@Nullable final LDIFWriter existingWriter)
1866          throws LDAPException
1867  {
1868    if (existingWriter != null)
1869    {
1870      return existingWriter;
1871    }
1872
1873    final File outputFile = targetLDIF.getValue();
1874    final File passphraseFile = targetEncryptionPassphraseFile.getValue();
1875
1876
1877    OutputStream outputStream = null;
1878    boolean closeOutputStream = true;
1879    try
1880    {
1881      try
1882      {
1883
1884        outputStream = new FileOutputStream(targetLDIF.getValue());
1885      }
1886      catch (final Exception e)
1887      {
1888        Debug.debugException(e);
1889        throw new LDAPException(ResultCode.LOCAL_ERROR,
1890             ERR_LDIFMODIFY_CANNOT_OPEN_OUTPUT_FILE.get(
1891                  outputFile.getAbsolutePath(),
1892                  StaticUtils.getExceptionMessage(e)),
1893             e);
1894      }
1895
1896      if (encryptTarget.isPresent())
1897      {
1898        try
1899        {
1900          final char[] passphrase;
1901          if (passphraseFile != null)
1902          {
1903            passphrase = getPasswordFileReader().readPassword(passphraseFile);
1904          }
1905          else
1906          {
1907            passphrase = ToolUtils.promptForEncryptionPassphrase(false, true,
1908                 INFO_LDIFMODIFY_ENTER_OUTPUT_ENCRYPTION_PW.get(),
1909                 INFO_LDIFMODIFY_CONFIRM_OUTPUT_ENCRYPTION_PW.get(), getOut(),
1910                 getErr()).toCharArray();
1911          }
1912
1913          outputStream = new PassphraseEncryptedOutputStream(passphrase,
1914               outputStream, null, true, true);
1915        }
1916        catch (final Exception e)
1917        {
1918          Debug.debugException(e);
1919          throw new LDAPException(ResultCode.LOCAL_ERROR,
1920               ERR_LDIFMODIFY_CANNOT_ENCRYPT_OUTPUT_FILE.get(
1921                    outputFile.getAbsolutePath(),
1922                    StaticUtils.getExceptionMessage(e)),
1923               e);
1924        }
1925      }
1926
1927      if (compressTarget.isPresent())
1928      {
1929        try
1930        {
1931          outputStream = new GZIPOutputStream(outputStream);
1932        }
1933        catch (final Exception e)
1934        {
1935          Debug.debugException(e);
1936          throw new LDAPException(ResultCode.LOCAL_ERROR,
1937               ERR_LDIFMODIFY_CANNOT_COMPRESS_OUTPUT_FILE.get(
1938                    outputFile.getAbsolutePath(),
1939                    StaticUtils.getExceptionMessage(e)),
1940               e);
1941        }
1942      }
1943
1944      final LDIFWriter ldifWriter = new LDIFWriter(outputStream);
1945      if (doNotWrap.isPresent())
1946      {
1947        ldifWriter.setWrapColumn(0);
1948      }
1949      else if (wrapColumn.isPresent())
1950      {
1951        ldifWriter.setWrapColumn(wrapColumn.getValue());
1952      }
1953      else
1954      {
1955        ldifWriter.setWrapColumn(WRAP_COLUMN);
1956      }
1957
1958      closeOutputStream = false;
1959      return ldifWriter;
1960    }
1961    finally
1962    {
1963      if (closeOutputStream && (outputStream != null))
1964      {
1965        try
1966        {
1967          outputStream.close();
1968        }
1969        catch (final Exception e)
1970        {
1971          Debug.debugException(e);
1972        }
1973      }
1974    }
1975  }
1976
1977
1978
1979  /**
1980   * Updates the provided entry with any appropriate changes.
1981   *
1982   * @param  entry
1983   *              The entry to be processed.  It must not be {@code null}.
1984   * @param  addAndSubsequentChangeRecords
1985   *              A map that will be updated with add change records for a given
1986   *              entry, along with any subsequent change records that apply to
1987   *              the entry after it has been added.  It must not be
1988   *              {@code null}, must be empty, and must be updatable.
1989   * @param  deletedEntryDNs
1990   *              A map that will be updated with the DNs of any entries that
1991   *              are targeted by delete modifications and that have not been
1992   *              previously added or renamed.  It must not be {@code null},
1993   *              must be empty, and must be updatable.
1994   * @param  modifyChangeRecords
1995   *              A map that will be updated with any modify change records
1996   *              that target an entry that has not been targeted by any other
1997   *              type of change.  It must not be {@code null}, must be empty,
1998   *              and must be updatable.
1999   * @param  modifyDNAndSubsequentChangeRecords
2000   *              A map that will be updated with any change records for modify
2001   *              DN operations that target a given entry, and any subsequent
2002   *              operations that target the entry with its new DN.  It must not
2003   *              be {@code null}, must be empty, and must be updatable.
2004   * @param  comment
2005   *              A buffer that should be updated with any comment to be
2006   *              included in the output, even if the entry is not altered.  It
2007   *              must not be {@code null}, but it should be empty.
2008   * @param  resultCode
2009   *              A reference to the final result code that should be used for
2010   *              the tool.  This may be updated if an error occurred during
2011   *              processing and no value is already set.  It must not be
2012   *              {@code null}, but is allowed to have no value assigned.
2013   * @param  entriesUpdated
2014   *              A counter that should be incremented if any changes are
2015   *              applied (including deleting the entry).  It should  not be
2016   *              updated if none of the changes are applicable to the provided
2017   *              entry.  It must not be {@code null}.
2018   *
2019   * @return  The provided entry if none of the changes are applicable, an
2020   *          updated entry if changes are applied, or {@code null} if the entry
2021   *          should be deleted and therefore omitted from the target LDIF file.
2022   */
2023  @Nullable()
2024  private Entry updateEntry(@NotNull final Entry entry,
2025       @NotNull final Map<DN,List<LDIFChangeRecord>>
2026            addAndSubsequentChangeRecords,
2027       @NotNull final Map<DN,Boolean> deletedEntryDNs,
2028       @NotNull final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords,
2029       @NotNull final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>>
2030            modifyDNAndSubsequentChangeRecords,
2031       @NotNull final StringBuilder comment,
2032       @NotNull final AtomicReference<ResultCode> resultCode,
2033       @NotNull final AtomicLong entriesUpdated)
2034  {
2035    // Get the parsed DN for the entry.  If that fails, then we'll just return
2036    // the provided entry along with a comment explaining that its DN could not
2037    // be parsed.
2038    final DN entryDN;
2039    try
2040    {
2041      entryDN = entry.getParsedDN();
2042
2043    }
2044    catch (final LDAPException e)
2045    {
2046      Debug.debugException(e);
2047      resultCode.compareAndSet(null, e.getResultCode());
2048      appendComment(comment,
2049           ERR_LDIFMODIFY_CANNOT_PARSE_ENTRY_DN.get(e.getMessage()), true);
2050      return entry;
2051    }
2052
2053
2054    // See if there is a delete change record for the entry.  If so, then mark
2055    // the entry as deleted and return null.
2056    if (deletedEntryDNs.containsKey(entryDN))
2057    {
2058      deletedEntryDNs.put(entryDN, Boolean.TRUE);
2059      createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_DELETE.get(),
2060           entry, false);
2061      entriesUpdated.incrementAndGet();
2062      return null;
2063    }
2064
2065
2066    // See if there is a delete change record for one of the entry's superiors.
2067    // If so, then mark the entry as deleted and return null.
2068    DN parentDN = entryDN.getParent();
2069    while (parentDN != null)
2070    {
2071      if (deletedEntryDNs.containsKey(parentDN))
2072      {
2073        createChangeRecordComment(comment,
2074             INFO_LDIFMODIFY_APPLIED_DELETE_OF_ANCESTOR.get(
2075                  parentDN.toString()),
2076             entry, false);
2077        entriesUpdated.incrementAndGet();
2078        return null;
2079      }
2080
2081      parentDN = parentDN.getParent();
2082    }
2083
2084
2085    // See if there are any modify change records that target the entry.  If so,
2086    // then apply those modifications.
2087    Entry updatedEntry = entry;
2088    final AtomicBoolean isUpdated = new AtomicBoolean(false);
2089    final List<String> errors = new ArrayList<>();
2090    final List<LDIFModifyChangeRecord> modRecords =
2091         modifyChangeRecords.remove(entryDN);
2092    if (modRecords != null)
2093    {
2094      for (final LDIFModifyChangeRecord r : modRecords)
2095      {
2096        updatedEntry = applyModification(updatedEntry, r, isUpdated, resultCode,
2097             comment);
2098      }
2099    }
2100
2101
2102    // See if the entry was targeted by a modify DN operation.  If so, then
2103    // rename the entry and see if there are any follow-on modifications.
2104    final ObjectPair<DN,List<LDIFChangeRecord>> modDNRecords =
2105         modifyDNAndSubsequentChangeRecords.remove(entryDN);
2106    if (modDNRecords != null)
2107    {
2108      for (final LDIFChangeRecord r : modDNRecords.getSecond())
2109      {
2110        if (r instanceof LDIFModifyDNChangeRecord)
2111        {
2112          final LDIFModifyDNChangeRecord modDNChangeRecord =
2113               (LDIFModifyDNChangeRecord) r;
2114          updatedEntry = applyModifyDN(updatedEntry, entryDN,
2115               modDNRecords.getFirst(), modDNChangeRecord.deleteOldRDN());
2116          createChangeRecordComment(comment,
2117               INFO_LDIFMODIFY_APPLIED_MODIFY_DN.get(), r, false);
2118          isUpdated.set(true);
2119        }
2120        else
2121        {
2122          updatedEntry = applyModification(updatedEntry,
2123               (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment);
2124        }
2125      }
2126    }
2127
2128
2129    // See if there is an add change record that targets the same entry.  If so,
2130    // then the add won't be processed but maybe subsequent changes will be.
2131    final List<LDIFChangeRecord> addAndMods =
2132         addAndSubsequentChangeRecords.remove(entryDN);
2133    if (addAndMods != null)
2134    {
2135      for (final LDIFChangeRecord r : addAndMods)
2136      {
2137        if (r instanceof LDIFAddChangeRecord)
2138        {
2139          resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS);
2140          createChangeRecordComment(comment,
2141               ERR_LDIFMODIFY_NOT_ADDING_EXISTING_ENTRY.get(), r, true);
2142        }
2143        else
2144        {
2145          updatedEntry = applyModification(updatedEntry,
2146               (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment);
2147        }
2148      }
2149    }
2150
2151
2152    if (isUpdated.get())
2153    {
2154      entriesUpdated.incrementAndGet();
2155    }
2156    else
2157    {
2158      if (comment.length() > 0)
2159      {
2160        appendComment(comment, StaticUtils.EOL, false);
2161        appendComment(comment, StaticUtils.EOL, false);
2162      }
2163      appendComment(comment, INFO_LDIFMODIFY_ENTRY_NOT_UPDATED.get(), false);
2164    }
2165
2166    return updatedEntry;
2167  }
2168
2169
2170
2171  /**
2172   * Creates a copy of the provided entry with the given modification applied.
2173   *
2174   * @param  entry               The entry to be updated.  It must not be
2175   *                             {@code null}.
2176   * @param  modifyChangeRecord  The modify change record to apply.  It must not
2177   *                             be {@code null}.
2178   * @param  isUpdated           A value that should be updated if the entry is
2179   *                             successfully modified.  It must not be
2180   *                             {@code null}.
2181   * @param  resultCode          A reference to the final result code that
2182   *                             should be used for the tool.  This may be
2183   *                             updated if an error occurred during processing
2184   *                             and no value is already set.  It must not be
2185   *                             {@code null}, but is allowed to have no value
2186   *                             assigned.
2187   * @param  comment             A buffer that should be updated with any
2188   *                             comment to be included in the output, even if
2189   *                             the entry is not altered.  It must not be
2190   *                             {@code null}, but it may be empty.
2191   *
2192   * @return  The entry with the modifications applied, or the original entry if
2193   *          an error occurred while applying the change.
2194   */
2195  @NotNull()
2196  private Entry applyModification(@NotNull final Entry entry,
2197                     @NotNull final LDIFModifyChangeRecord modifyChangeRecord,
2198                     @NotNull final AtomicBoolean isUpdated,
2199                     @NotNull final AtomicReference<ResultCode> resultCode,
2200                     @NotNull final StringBuilder comment)
2201  {
2202    try
2203    {
2204      final Entry updatedEntry = Entry.applyModifications(entry,
2205           (! strictModifications.isPresent()),
2206           modifyChangeRecord.getModifications());
2207      createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_MODIFY.get(),
2208           modifyChangeRecord, false);
2209      isUpdated.set(true);
2210      return updatedEntry;
2211    }
2212    catch (final LDAPException e)
2213    {
2214      Debug.debugException(e);
2215      resultCode.compareAndSet(null, e.getResultCode());
2216      createChangeRecordComment(comment,
2217           ERR_LDIFMODIFY_ERROR_APPLYING_MODIFY.get(
2218                String.valueOf(e.getResultCode()), e.getMessage()),
2219           modifyChangeRecord, true);
2220      return entry;
2221    }
2222  }
2223
2224
2225
2226  /**
2227   * Creates a copy of the provided entry with the given new DN.
2228   *
2229   * @param  entry         The entry to be renamed.  It must not be
2230   *                       {@code null}.
2231   * @param  originalDN    A parsed representation of the original DN for the
2232   *                       entry.  It must not be {@code null}.
2233   * @param  newDN         A parsed representation of the new DN for the entry.
2234   *                       It must not be {@code null}.
2235   * @param  deleteOldRDN  Indicates whether the old RDN values should be
2236   *                       removed from the entry.
2237   *
2238   * @return  The updated entry with the new DN and any other associated
2239   *          changes.
2240   */
2241  @NotNull()
2242  private Entry applyModifyDN(@NotNull final Entry entry,
2243                              @NotNull final DN originalDN,
2244                              @NotNull final DN newDN,
2245                              final boolean deleteOldRDN)
2246  {
2247    final Entry copy = entry.duplicate();
2248    copy.setDN(newDN);
2249
2250    final RDN oldRDN = originalDN.getRDN();
2251    if (deleteOldRDN && (oldRDN != null))
2252    {
2253      for (final Attribute a : oldRDN.getAttributes())
2254      {
2255        for (final byte[] value : a.getValueByteArrays())
2256        {
2257          copy.removeAttributeValue(a.getName(), value);
2258        }
2259      }
2260    }
2261
2262    final RDN newRDN = newDN.getRDN();
2263    if (newRDN != null)
2264    {
2265      for (final Attribute a : newRDN.getAttributes())
2266      {
2267        for (final byte[] value : a.getValueByteArrays())
2268        {
2269          copy.addAttribute(a);
2270        }
2271      }
2272    }
2273
2274    return copy;
2275  }
2276
2277
2278
2279  /**
2280   * Writes the provided LDIF record to the LDIF writer.
2281   *
2282   * @param  ldifWriter  The writer to which the LDIF record should be written.
2283   *                     It must not be {@code null}.
2284   * @param  ldifRecord  The LDIF record to be written.  It must not be
2285   *                     {@code null}.
2286   * @param  comment     The comment to include as part of the LDIF record.  It
2287   *                     may be {@code null} or empty if no comment should be
2288   *                     included.
2289   *
2290   * @throws  IOException  If an error occurs while attempting to write to the
2291   *                       LDIF writer.
2292   */
2293  private void writeLDIFRecord(@NotNull final LDIFWriter ldifWriter,
2294                               @NotNull final LDIFRecord ldifRecord,
2295                               @Nullable final CharSequence comment)
2296          throws IOException
2297  {
2298    if (suppressComments.isPresent() || (comment == null) ||
2299         (comment.length() == 0))
2300    {
2301      ldifWriter.writeLDIFRecord(ldifRecord);
2302    }
2303    else
2304    {
2305      ldifWriter.writeLDIFRecord(ldifRecord, comment.toString());
2306    }
2307  }
2308
2309
2310
2311  /**
2312   * Appends the provided comment to the given buffer.
2313   *
2314   * @param  buffer   The buffer to which the comment should be appended.
2315   * @param  comment  The comment to be appended.
2316   * @param  isError  Indicates whether the comment represents an error that
2317   *                  should be added to the error list if it exists.  It should
2318   *                  be {@code false} if the comment is not an error, or if it
2319   *                  is an error but should not be added to the list of error
2320   *                  messages (e.g., because a message will be added through
2321   *                  some other means).
2322   */
2323  private void appendComment(@NotNull final StringBuilder buffer,
2324                             @NotNull final String comment,
2325                             final boolean isError)
2326  {
2327    buffer.append(comment);
2328    if (isError && (errorMessages != null))
2329    {
2330      errorMessages.add(comment);
2331    }
2332  }
2333
2334
2335
2336  /**
2337   * Writes the provided comment to the LDIF writer.
2338   *
2339   * @param  ldifWriter  The writer to which the comment should be written.  It
2340   *                     must not be {@code null}.
2341   * @param  comment     The comment to be written.  It may be {@code null} or
2342   *                     empty if no comment should actually be written.
2343   * @param  isError     Indicates whether the comment represents an error that
2344   *                     should be added to the error list if it exists.  It
2345   *                     should be {@code false} if the comment is not an error,
2346   *                     or if it is an error but should not be added to the
2347   *                     list of error messages (e.g., because a message will be
2348   *                     added through some other means).
2349   *
2350   * @throws  IOException  If an error occurs while attempting to write to the
2351   *                       LDIF writer.
2352   */
2353  private void writeLDIFComment(@NotNull final LDIFWriter ldifWriter,
2354                                @Nullable final CharSequence comment,
2355                                final boolean isError)
2356          throws IOException
2357  {
2358    if (! (suppressComments.isPresent() || (comment == null) ||
2359         (comment.length() == 0)))
2360    {
2361      ldifWriter.writeComment(comment.toString(), false, true);
2362    }
2363
2364    if (isError && (errorMessages != null) && (comment != null))
2365    {
2366      errorMessages.add(comment.toString());
2367    }
2368  }
2369
2370
2371
2372  /**
2373   * Appends a comment to the provided buffer for the given LDIF record.
2374   *
2375   * @param  buffer   The buffer to which the comment should be appended.  It
2376   *                  must not be {@code null}.
2377   * @param  message  The message to include before the LDIF record.  It must
2378   *                  not be {@code null}.
2379   * @param  record   The LDIF record to include in the comment.
2380   * @param  isError  Indicates whether the comment represents an error that
2381   *                  should be added to the error list if it exists.  It should
2382   *                  be {@code false} if the comment is not an error, or if it
2383   *                  is an error but should not be added to the list of error
2384   *                  messages (e.g., because a message will be added through
2385   *                  some other means).
2386   */
2387  private void createChangeRecordComment(@NotNull final StringBuilder buffer,
2388                                         @NotNull final String message,
2389                                         @NotNull final LDIFRecord record,
2390                                         final boolean isError)
2391  {
2392    final int initialLength = buffer.length();
2393    if (initialLength > 0)
2394    {
2395      buffer.append(StaticUtils.EOL);
2396      buffer.append(StaticUtils.EOL);
2397    }
2398
2399    buffer.append(message);
2400    buffer.append(StaticUtils.EOL);
2401
2402    final int wrapCol;
2403    if (wrapColumn.isPresent() && (wrapColumn.getValue() > 20) &&
2404         (wrapColumn.getValue() <= 85))
2405    {
2406      wrapCol = wrapColumn.getValue() - 10;
2407    }
2408    else
2409    {
2410      wrapCol = 75;
2411    }
2412
2413    for (final String line : record.toLDIF(wrapCol))
2414    {
2415      buffer.append("     ");
2416      buffer.append(line);
2417      buffer.append(StaticUtils.EOL);
2418    }
2419
2420    if (isError && (errorMessages != null))
2421    {
2422      if (initialLength == 0)
2423      {
2424        errorMessages.add(buffer.toString());
2425      }
2426      else
2427      {
2428        errorMessages.add(buffer.toString().substring(initialLength));
2429      }
2430    }
2431  }
2432
2433
2434
2435  /**
2436   * Writes a wrapped version of the provided message to standard error.  If an
2437   * {@code errorList} is also available, then the message will also be added to
2438   * that list.
2439   *
2440   * @param  message  The message to be written.  It must not be {@code null].
2441   */
2442  private void wrapErr(@NotNull final String message)
2443  {
2444    wrapErr(0, WRAP_COLUMN, message);
2445    if (errorMessages != null)
2446    {
2447      errorMessages.add(message);
2448    }
2449  }
2450
2451
2452
2453  /**
2454   * Writes the provided message and sets it as the completion message.
2455   *
2456   * @param  isError  Indicates whether the message should be written to
2457   *                  standard error rather than standard output.
2458   * @param  message  The message to be written.
2459   */
2460  private void logCompletionMessage(final boolean isError,
2461                                    @NotNull final String message)
2462  {
2463    completionMessage.compareAndSet(null, message);
2464
2465    if (isError)
2466    {
2467      wrapErr(message);
2468    }
2469    else
2470    {
2471      wrapOut(0, WRAP_COLUMN, message);
2472    }
2473  }
2474
2475
2476
2477  /**
2478   * {@inheritDoc}
2479   */
2480  @Override()
2481  @NotNull()
2482  public LinkedHashMap<String[],String> getExampleUsages()
2483  {
2484    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
2485
2486    examples.put(
2487         new String[]
2488         {
2489           "--sourceLDIF", "original.ldif",
2490           "--changesLDIF", "changes.ldif",
2491           "--targetLDIF", "updated.ldif"
2492         },
2493         INFO_LDIFMODIFY_EXAMPLE.get("changes.ldif", "original.ldif",
2494              "updated.ldif"));
2495
2496    return examples;
2497  }
2498}