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