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