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